← Назад к вопросам

Как будешь искать причину замедления скролла таблицы?

2.0 Middle🔥 141 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Диагностика и оптимизация замедленного скролла таблицы

Замедленный скролл таблицы — очень распространённая проблема с производительностью. Это не случайность, а результат конкретных причин. Вот мой алгоритм диагностики и решения.

Шаг 1: Использование DevTools Performance

Первое, что я делаю — открываю Chrome DevTools и записываю профиль производительности:

// 1. Открыть Chrome DevTools (F12)
// 2. Перейти на вкладку Performance
// 3. Нажать Record
// 4. Скроллить таблицу 3-5 секунд
// 5. Нажать Stop

// На графике FPS (кадры в секунду) должны быть > 60 fps
// Если < 30 fps - есть проблема производительности

// В главной панели я ищу:
// - "Rendering" (красные полосы = проблема)
// - "Layout" (перепросчёты позиций)
// - "Paint" (перерисовка элементов)
// - "Composite" (композиция слоёв)

Что смотреть на графике:

  • Жёлтая полоса = JavaScript выполнение
  • Зелёная полоса = Rendering (Layout + Paint)
  • Фиолетовая полоса = Composite

Шаг 2: Выявление основной причины

Проблема 1: Тяжелый JavaScript

// ПЛОХО - JavaScript блокирует скролл
function handleScroll() {
  const visibleRows = calculateVisibleRows(); // Дорогая операция
  const filteredData = filterData(); // Еще одна дорогая операция
  const sortedData = sortData(); // И ещё одна
  render(sortedData);
}

tableElement.addEventListener('scroll', handleScroll);

Решение: Дроссельное ограничение (throttle)

// ХОРОШО - ограничить частоту вызовов
function throttle(func, limit) {
  let inThrottle;
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

const handleScroll = throttle(() => {
  console.log('Scroll event');
}, 100); // Максимум один раз в 100ms

tableElement.addEventListener('scroll', handleScroll);

Проблема 2: Дорогие DOM операции (Layout Thrashing)

// ПЛОХО - часто читаем и пишем в DOM (вызывает рассчитать layout)
function updateRows() {
  for (let i = 0; i < rows.length; i++) {
    rows[i].style.top = (i * 40) + 'px'; // Запись
    const height = rows[i].offsetHeight; // Чтение - вызывает recalc!
  }
}

Решение: Батчировать чтения и записи

// ХОРОШО - сначала прочитать всё, потом написать
function updateRows() {
  // Фаза чтения
  const heights = rows.map(r => r.offsetHeight);
  
  // Фаза записи
  rows.forEach((row, i) => {
    row.style.top = (i * 40) + 'px';
  });
}

// Или использовать requestAnimationFrame
function updateRowsOptimized() {
  requestAnimationFrame(() => {
    rows.forEach((row, i) => {
      row.style.top = (i * 40) + 'px';
    });
  });
}

Шаг 3: React-специфичные проблемы

Проблема: Ненужные re-renders

// ПЛОХО - каждый скролл вызывает re-render всех строк
function TableComponent({ rows }) {
  return (
    <div onScroll={handleScroll}>
      {rows.map(row => (
        <TableRow key={row.id} row={row} /> // Вся таблица ре-рендерится!
      ))}
    </div>
  );
}

// ХОРОШО - мемоизировать строки
const TableRow = React.memo(({ row }) => (
  <div className="row">{row.name}</div>
));

function TableComponent({ rows }) {
  const handleScroll = useCallback(throttle(() => {
    // ...
  }, 100), []);
  
  return (
    <div onScroll={handleScroll}>
      {rows.map(row => (
        <TableRow key={row.id} row={row} />
      ))}
    </div>
  );
}

Проблема: Виртуализация не используется

// ПЛОХО - рендерить 1000 строк сразу
function LargeTable({ items }) {
  return (
    <div>
      {items.map(item => (
        <div key={item.id} style={{height: '40px'}}>
          {item.name}
        </div>
      ))}
    </div>
  );
}

// ХОРОШО - использовать виртуализацию
import { FixedSizeList } from 'react-window';

function LargeTable({ items }) {
  return (
    <FixedSizeList
      height={600} // Видимая высота
      itemCount={items.length}
      itemSize={40} // Высота каждой строки
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {items[index].name}
        </div>
      )}
    </FixedSizeList>
  );
}

Шаг 4: CSS-проблемы (Painting)

Проблема: Дорогие CSS свойства

/* ПЛОХО - дорогие для браузера при скролле */
.row {
  box-shadow: 0 4px 6px rgba(0,0,0,0.1); /* Может вызывать repaint */
  border-radius: 8px; /* Может замедлить */
  filter: blur(5px); /* ОЧЕНЬ дорого! */
}

/* ХОРОШО - оптимизированные свойства */
.row {
  background-color: white;
  border: 1px solid #ddd;
  /* box-shadow и filter только для выделенных строк */
}

.row:hover {
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

Решение: Использовать will-change и contain

/* will-change подсказывает браузеру, что элемент изменится */
.row {
  will-change: transform;
  contain: layout style paint; /* Изолировать элемент для оптимизации */
}

/* Трансформация работает через GPU (быстро) */
.row {
  transform: translateZ(0); /* Включить GPU ускорение */
}

Шаг 5: Network и Data Binding

Проблема: Скролл вызывает API запросы

// ПЛОХО - каждый скролл = новый запрос
function useTableScroll(rows) {
  useEffect(() => {
    const handleScroll = async () => {
      const newData = await fetch('/api/table?offset=...');
      setRows([...rows, ...newData]); // Причина замедления!
    };
    
    element.addEventListener('scroll', handleScroll);
  }, [rows]);
}

// ХОРОШО - ограничить и дебаунс
function useTableScroll() {
  const [rows, setRows] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  
  const loadMore = useCallback(
    debounce(async (offset) => {
      if (isLoading) return;
      setIsLoading(true);
      const data = await fetch(`/api/table?offset=${offset}`);
      setRows(prev => [...prev, ...data]);
      setIsLoading(false);
    }, 300),
    [isLoading]
  );
  
  return { rows, loadMore };
}

Полный Checklist Диагностики

// 1. Performance Timeline - ищем где теряются FPS
const marker = performance.mark('scroll-start');
// ... скролл ...
performance.mark('scroll-end');
performance.measure('scroll', 'scroll-start', 'scroll-end');
console.log(performance.getEntriesByType('measure'));

// 2. Подсчитать re-renders в React
function useRenderCount(name) {
  const renders = useRef(0);
  useEffect(() => {
    renders.current++;
    console.log(`${name} rendered ${renders.current} times`);
  });
}

// 3. Профилировать функции
function slowFunction() {
  console.time('slowFunc');
  // ... код ...
  console.timeEnd('slowFunc');
}

// 4. Использовать DevTools Rendering Stats
// Chrome DevTools -> More -> Rendering -> Paint flashing
// Зелёные вспышки = покраска, должны быть только при скролле!

// 5. Проверить Composite Layers
// DevTools -> More -> Layers
// Должны быть слои, которые не пересчитываются при скролле

Типичные решения

ПроблемаРешение
JavaScript блокирует скроллthrottle/debounce, requestAnimationFrame
Layout thrashingБатчировать reads/writes
Ненужные re-rendersReact.memo, useMemo, useCallback
Рендер 1000+ строкВиртуализация (react-window)
Дорогой CSSwill-change, contain, GPU acceleration
API запросы при скроллеДебаунс, infinite scroll pagination

Инструменты которые я использую

// react-window - виртуализация
import { FixedSizeList } from 'react-window';

// lodash - throttle/debounce
import { throttle, debounce } from 'lodash';

// Chrome DevTools Performance
// Firefox DevTools Inspector
// Lighthouse Performance audit

// Perftools.dev для визуализации профиля
performance.measureUserAgentSpecificMemory();

Итог

Мой алгоритм поиска причины замедления скролла:

  1. Performance Timeline - определить, что именно медленное
  2. React DevTools Profiler - количество re-renders
  3. Chrome DevTools Rendering - paint flashing и слои
  4. Code review - throttle, memo, виртуализация
  5. Профилирование кода - найти узкие места
  6. Применить решение - обычно это комбинация техник
  7. Проверить FPS - должно быть > 60 FPS

В 95% случаев проблема одна из: дорогие re-renders, отсутствие виртуализации, layout thrashing или неправильный дроссель scroll events.