Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Middle: питання на співбесіді з теми «Продуктивність»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

React Compiler (стабільна версія 1.0) - інструмент етапу збирання, що автоматично додає мемоізацію в компоненти й хуки. Він аналізує код і кешує JSX, обчислення й функції так, щоб при повторному рендері перераховувалось лише те, що залежить від змінених даних.

До компілятора:

const Products = memo(function Products({ items, onSelect }) {
  const sorted = useMemo(() => [...items].sort(byPrice), [items]);
  const handleClick = useCallback((item) => onSelect(item.id), [onSelect]);
  return sorted.map((item) => <Item key={item.id} onClick={() => handleClick(item)} />);
});

З компілятором - той самий код без memo, useMemo, useCallback. Компілятор мемоізує навіть те, що вручну зробити складно: стрілкову функцію () => handleClick(item) у циклі, яка з ручним useCallback все одно створювалась би щоразу й ламала б memo дочірнього компонента.

На чому фокусується:

  • пропуск каскадних рендерів: якщо props дочірнього компонента не змінилися, він не рендериться;
  • пропуск дорогих обчислень у компонентах і хуках.

Компілятор не мемоізує звичайні функції поза компонентами й хуками і не кешує результати між різними екземплярами компонента.

Правила React, на які він покладається:

  • рендер чистий: компонент для тих самих props і стану повертає той самий результат, без побічних ефектів;
  • props і стан незмінні: не мутувати отримані об'єкти й масиви;
  • значення з хуків незмінні, ref.current не читається під час рендеру;
  • правила хуків (без умовних викликів).

Код, що порушує правила, компілятор пропускає (залишає без оптимізації), а не ламає. Порушення показує eslint-plugin-react-hooks.

Підключення: Babel-плагін babel-plugin-react-compiler (у Vite - через reactCompilerPreset плагіна React), підтримка в Next.js, Expo. Працює з React 17+ (для 17-18 - пакет react-compiler-runtime).

Що змінюється для розробника:

  • нові компоненти пишуться без ручної мемоізації;
  • існуючі useMemo/useCallback можна лишити - компілятор їх враховує; прибирати варто обережно, бо деякі з них впливають на залежності ефектів;
  • структурні оптимізації (віртуалізація, розділення коду, опускання стану) компілятор не замінює.

Докладніше в документації: React Compiler: вступ

Коли змінюється значення провайдера контексту, React перерендерює всі компоненти, що читають цей контекст, - навіть якщо вони використовують поле, яке не змінилося. memo на споживачі від цього не захищає.

Пастка 1 - новий об'єкт на кожен рендер провайдера:

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const login = async (data) => { /* ... */ };

  // новий об'єкт і нова функція при КОЖНОМУ рендері AuthProvider
  return <AuthContext value={{ user, login }}>{children}</AuthContext>;
}

Будь-який рендер провайдера (наприклад, через стан вище в дереві) змушує всіх споживачів рендеритися. Рішення - стабільне значення:

const login = useCallback(async (data) => { /* ... */ }, []);
const value = useMemo(() => ({ user, login }), [user, login]);
return <AuthContext value={value}>{children}</AuthContext>;

React Compiler робить цю мемоізацію автоматично.

Пастка 2 - один контекст для даних, що змінюються з різною частотою. Тема, користувач і стан кошика в одному контексті: додавання товару перерендерює все, що читає тему.

Рішення - розділити контексти:

<ThemeContext value={theme}>
  <UserContext value={user}>
    <CartContext value={cart}>{children}</CartContext>
  </UserContext>
</ThemeContext>

Розділити стан і дії: компоненти, що лише змінюють стан (кнопка «Додати в кошик»), не мають рендеритися, коли стан змінюється:

<CartStateContext value={items}>
  <CartActionsContext value={actions}>   {/* стабільний об'єкт, не змінюється */}
    {children}
  </CartActionsContext>
</CartStateContext>

Коли контексту замало. Контекст - механізм передачі значення, а не сховище з підпискою на частину стану. Для великого стану, що часто змінюється (редактор, складна форма, дані в реальному часі), краще бібліотеки з селекторами - Zustand, Jotai, Redux Toolkit, useSyncExternalStore: компонент підписується лише на потрібне поле й рендериться лише при його зміні.

Що варто пам'ятати: контекст добре підходить для рідко змінюваних даних - тема, мова, поточний користувач, налаштування. Проблеми з продуктивністю зазвичай починаються, коли в контекст кладуть швидко змінюваний стан.

Докладніше в документації: useContext: оптимізація рендерів

Відрендерити 20 000 рядків таблиці - це 20 000 компонентів і сотні тисяч DOM-вузлів. Повільно все: перший рендер, кожне оновлення, прокрутка, пам'ять. memo тут не рятує - проблема в самій кількості.

Віртуалізація (windowing) - рендерити лише рядки, видимі на екрані, плюс невеликий запас. При прокрутці ті самі DOM-вузли отримують нові дані.

import { useVirtualizer } from '@tanstack/react-virtual';

function VacancyList({ items }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 72,   // орієнтовна висота рядка
    overscan: 5,
  });

  return (
    <div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map((row) => (
          <div
            key={row.key}
            style={{ position: 'absolute', top: 0, transform: `translateY(${row.start}px)`, width: '100%' }}
          >
            <VacancyRow vacancy={items[row.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

У DOM постійно ~15 рядків замість 20 000, а висота контейнера дорівнює повній висоті списку - смуга прокрутки виглядає правильно.

Бібліотеки: TanStack Virtual (без готової розмітки, гнучкий), react-window, react-virtuoso (вміє динамічну висоту й «прилипання» до низу для чатів).

Складнощі, які треба врахувати:

  • рядки різної висоти - вимірювати після рендеру (measureElement) і оцінювати заздалегідь;
  • пошук браузера (Ctrl+F) не знайде текст рядків, яких немає в DOM;
  • доступність: зчитувачі екрана бачать лише відрендерені рядки - потрібні aria-rowcount/aria-rowindex;
  • збереження позиції прокрутки при поверненні на сторінку.

Альтернативи, які часто кращі:

  • пагінація чи «завантажити ще» - користувач рідко переглядає 20 000 рядків;
  • фільтри й пошук на сервері - показувати лише релевантне;
  • CSS content-visibility: auto - браузер пропускає розкладку й малювання позаекранних блоків, хоча DOM лишається повним. Простий виграш для довгих сторінок з помірною кількістю елементів.

Віртуалізація потрібна, коли весь набір справді має бути доступний прокруткою: логи, таблиці даних, великі чати.

Докладніше в документації: TanStack Virtual

<Profiler> вимірює час рендеру частини дерева з коду - для автоматичного збору метрик, а не ручного аналізу в DevTools.

import { Profiler, type ProfilerOnRenderCallback } from 'react';

const onRender: ProfilerOnRenderCallback = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
  if (actualDuration > 16) {
    metrics.record('slow-render', { id, phase, actualDuration, baseDuration });
  }
};

<Profiler id="VacancyTable" onRender={onRender}>
  <VacancyTable />
</Profiler>

Параметри колбеку:

  • id - назва профілера (для розрізнення кількох);
  • phase - "mount", "update" чи "nested-update" (оновлення, викликане зміною стану в ефекті під час коміту);
  • actualDuration - скільки мілісекунд зайняв рендер цього дерева в цьому коміті. Якщо мемоізація працює, він помітно менший за baseDuration;
  • baseDuration - оцінка часу рендеру всього дерева без оптимізацій (сума останніх часів рендеру всіх компонентів). Показує «найгірший випадок»;
  • startTime, commitTime - мітки часу початку рендеру й коміту.

Кілька профілерів можна ставити поруч і вкладати - щоб порівняти частини сторінки.

Важливо про production: профілювання додає накладних витрат, тому в звичайній production-збірці Profiler вимкнено - колбек не викликається. Для вимірювання на продакшені потрібна спеціальна збірка з профілюванням (react-dom/profiling), яку роздають частині користувачів чи використовують на тестовому стенді. У режимі розробки цифри завищені й годяться лише для відносних порівнянь.

Для чого використовують:

  • регресійні тести продуктивності в CI: рендер великої таблиці не повинен перевищити поріг;
  • моніторинг важких екранів у реальних користувачів (зі збіркою профілювання);
  • порівняння до й після оптимізації.

Обмеження:

  • Profiler міряє лише час рендеру React, а не завантаження даних, розкладку браузера чи малювання. Для повної картини взаємодії - метрика INP і вкладка Performance браузера;
  • не обгортати кожен компонент - профілер сам додає витрати; ставити на межі великих блоків.

Альтернатива для інтерактивного аналізу - вкладка Profiler у React DevTools з тими самими даними, flamegraph і причинами рендерів.

Докладніше в документації: Profiler