> Как оптимизировать обработку кликов на ячейки большой таблицы в веб-приложении (JavaScript)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: Exness, HolyWeb

Стек: JavaScript

> Пример ответа

Короткий ответ

Для оптимизации обработки кликов на ячейки большой таблицы используйте делегирование событий на контейнере таблицы вместо привязки обработчика к каждой ячейке. Дополнительно применяйте виртуализацию (например, react-window или Intersection Observer) для рендеринга только видимых строк, debounce/throttle для частых кликов и мемоизацию обработчиков. Избегайте layout thrashing, группируя чтение/запись DOM через requestAnimationFrame.

Подробное объяснение

Основная проблема при обработке кликов на большой таблице - производительность. Привязка обработчика к каждой ячейке (например, через addEventListener или onClick в React) создаёт N слушателей, что увеличивает потребление памяти и время инициализации. При скролле или ререндере это может вызывать задержки.

Делегирование событий решает проблему: один обработчик на <table> или <tbody> ловит все клики через всплытие. Внутри определяем цель (event.target) и проверяем, является ли она ячейкой или её потомком. Это снижает количество слушателей до 1.

Виртуализация (например, react-virtualized или react-window) рендерит только видимые строки, что уменьшает количество DOM-узлов и, соответственно, потенциальных целей для кликов. Это критично для таблиц с 1000+ строк.

Debounce/throttle применяется, если клики вызывают дорогие операции (например, запрос к API). Например, при двойном клике на ячейку для редактирования - throttle на 300ms предотвращает множественные вызовы.

Мемоизация в React (useCallback, useMemo) предотвращает пересоздание функций-обработчиков при каждом рендере, что важно при передаче колбэков в дочерние компоненты.

Layout thrashing возникает, когда после клика мы читаем и пишем DOM (например, измеряем позицию ячейки и меняем стиль). Решение - группировать операции через requestAnimationFrame или использовать getBoundingClientRect один раз.

На практике

  1. Делегирование: вешаем обработчик на <table> или контейнер. Проверяем event.target.closest('td') или event.target.matches('td, th'). Для React - используем onClick на родителе.

  2. Виртуализация: в React - react-window с FixedSizeList или VariableSizeList. Для нативных таблиц - библиотеки типа ag-Grid или Handsontable.

  3. Throttle: если клик вызывает fetch - оборачиваем в lodash.throttle с задержкой 300-500ms.

  4. Мемоизация: в функциональных компонентах - useCallback для обработчика, useMemo для данных.

  5. Избегаем layout thrashing: при клике читаем getBoundingClientRect один раз, затем пишем стили. Если нужно несколько чтений - используем requestAnimationFrame.

Пример кода

JAVASCRIPT
// Делегирование событий на таблице
const table = document.querySelector('#large-table');
table.addEventListener('click', (event) => {
const cell = event.target.closest('td');
if (!cell) return;
// Получаем данные ячейки
const rowIndex = cell.parentElement.rowIndex;
const colIndex = cell.cellIndex;
console.log(`Clicked cell [${rowIndex}, ${colIndex}]`);
// Дорогая операция с throttle
handleCellClick(rowIndex, colIndex);
});
// Throttle для дорогих операций
const handleCellClick = _.throttle((row, col) => {
fetch(`/api/cell?row=${row}&col=${col}`);
}, 300);
JSX
// React с виртуализацией и мемоизацией
import { FixedSizeList } from 'react-window';
function LargeTable({ rows }) {
const handleClick = useCallback((rowIndex, colIndex) => {
// обработка клика
}, []);
const Row = useCallback(({ index, style }) => (
<div style={style}>
{rows[index].map((cell, colIndex) => (
<span key={colIndex} onClick={() => handleClick(index, colIndex)}>
{cell}
</span>
))}
</div>
), [rows, handleClick]);
return (
<FixedSizeList height={600} itemCount={rows.length} itemSize={50}>
{Row}
</FixedSizeList>
);
}

Как отвечать на собеседовании

Начните с ключевой проблемы - производительность при большом количестве обработчиков. Сразу предложите делегирование как базовое решение. Затем добавьте виртуализацию для таблиц с 1000+ строк. Упомяните throttle/debounce для частых кликов и мемоизацию в React. Если спросят про layout thrashing - объясните, как группировать DOM-операции. Приведите пример кода с делегированием и виртуализацией. Завершите trade-off: делегирование усложняет определение конкретной ячейки (нужен closest), но выигрыш в памяти значителен.

Что проверяет интервьюер

  • Понимание event delegation и всплытия событий
  • Знание проблем производительности DOM (память, layout thrashing)
  • Опыт с виртуализацией и библиотеками (react-window, ag-Grid)
  • Умение применять throttle/debounce в контексте UI
  • Навыки оптимизации React (useCallback, useMemo)
  • Способность видеть trade-off между простотой и производительностью

Типичные ошибки

  • Привязка обработчика к каждой ячейке через forEach или map - создаёт N слушателей
  • Игнорирование всплытия и попытка повесить обработчик на document (может ловить клики вне таблицы)
  • Отсутствие проверки event.target - обработчик срабатывает на любом клике внутри таблицы (например, на скролле)
  • Забывают про мемоизацию в React - при каждом рендере создаются новые функции
  • Использование виртуализации без учёта высоты строк (FixedSizeList для переменной высоты ломает скролл)
  • Чтение и запись DOM в одном цикле событий без requestAnimationFrame - вызывает layout thrashing

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью