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 перерендерює всі компоненти, що читають цей контекст, - навіть якщо вони використовують поле, яке не змінилося. 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: компонент підписується лише на потрібне поле й рендериться лише при його зміні.
Що варто пам'ятати: контекст добре підходить для рідко змінюваних даних - тема, мова, поточний користувач, налаштування. Проблеми з продуктивністю зазвичай починаються, коли в контекст кладуть швидко змінюваний стан.
Відрендерити 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 лишається повним. Простий виграш для довгих сторінок з помірною кількістю елементів.
Віртуалізація потрібна, коли весь набір справді має бути доступний прокруткою: логи, таблиці даних, великі чати.
<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 і причинами рендерів.