Junior: питання на співбесіді з теми «Продуктивність»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Коли змінюється стан компонента, React перерендерює його і всіх нащадків. Перш ніж обгортати все в memo, варто змінити структуру - часто цього досить.
1. Опустити стан туди, де він потрібен (state colocation):
// було: введення в пошук перерендерює всю сторінку
function Page() {
const [query, setQuery] = useState('');
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Results query={query} />
<HeavySidebar /> {/* рендериться на кожну літеру */}
</>
);
}
// стало: стан живе в компоненті, якому він потрібен
function Search() {
const [query, setQuery] = useState('');
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Results query={query} />
</>
);
}
function Page() {
return (
<>
<Search />
<HeavySidebar /> {/* більше не залежить від введення */}
</>
);
}
2. Передати важкий вміст як children:
function Collapsible({ children }) {
const [open, setOpen] = useState(false);
return (
<section>
<button onClick={() => setOpen(!open)}>Розгорнути</button>
{open && children}
</section>
);
}
<Collapsible>
<HeavyChart />
</Collapsible>
Коли змінюється open, React перерендерює Collapsible, але children - це JSX, створений батьком, і він не змінився. HeavyChart не рендериться заново.
Інші принципи з документації React:
- не оновлювати стан в ефектах без потреби - ланцюжки «ефект встановлює стан - рендер - ефект» множать рендери;
- обчислювати похідні значення під час рендеру, а не зберігати копію в стані;
- прибирати зайві залежності ефектів - функції й об'єкти, створені в тілі компонента, змінюються на кожен рендер.
Рендер - не обов'язково проблема. Рендер - це виклик функції й порівняння результату; DOM змінюється лише там, де результат відрізняється. Оптимізувати варто тоді, коли профілювання показує помітні затримки, а не «про всяк випадок».
React Compiler автоматично додає мемоізацію, але структурні рішення лишаються корисними: чим менше компонентів залежить від стану, що часто змінюється, тим менше роботи взагалі.
Докладніше в документації: memo: чи варто додавати memo всюди
Початкове значення useState використовується лише при першому рендері. Але вираз, переданий аргументом, обчислюється на кожному рендері - бо це звичайний виклик функції компонента.
function Editor() {
// parseDraft викликається на КОЖНОМУ рендері, хоча результат потрібен один раз
const [draft, setDraft] = useState(parseDraft(localStorage.getItem('draft')));
// ...
}
Якщо обчислення дороге (розбір великого JSON, читання localStorage, побудова структури з тисяч елементів), компонент сповільнюється на кожне введення в поле.
Лінива ініціалізація - передати функцію, а не її результат:
const [draft, setDraft] = useState(() => parseDraft(localStorage.getItem('draft')));
React викличе функцію лише при першому рендері. Зверніть увагу на різницю:
useState(createInitialTodos()); // викликається щоразу
useState(createInitialTodos); // передано саму функцію - лише раз
useState(() => createInitialTodos(userId)); // з аргументом - через обгортку
Те саме для useReducer - третій аргумент-ініціалізатор:
const [state, dispatch] = useReducer(reducer, userId, createInitialState);
Для useRef вбудованої лінивої ініціалізації немає - роблять вручну:
const playerRef = useRef<VideoPlayer | null>(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer(); // створюється один раз
}
Що варто знати:
- у StrictMode функція-ініціалізатор у режимі розробки викликається двічі - вона має бути чистою (без запитів, підписок, побічних ефектів);
- для дешевих значень (
useState(0),useState([]),useState(props.initial)) лінива ініціалізація не потрібна - виграшу немає; - ініціалізація з props відбувається один раз:
useState(props.value)не оновиться, колиprops.valueзміниться. Якщо значення має стежити за props, стан не потрібен - обчислюйте під час рендеру, а для «скидання» стану використайтеkey.
Докладніше в документації: useState: уникнення повторного створення початкового стану
За замовчуванням збирач кладе весь код застосунку в один файл: користувач, що відкрив головну сторінку, завантажує й код адмінки, редактора, графіків. Розділення коду (code splitting) виносить частини в окремі файли, що завантажуються за потреби.
lazy - компонент, код якого завантажується при першому рендері:
import { lazy, Suspense } from 'react';
const MarkdownEditor = lazy(() => import('./MarkdownEditor'));
function PostForm() {
const [editing, setEditing] = useState(false);
return (
<>
<button onClick={() => setEditing(true)}>Редагувати</button>
{editing && (
<Suspense fallback={<p>Завантаження редактора...</p>}>
<MarkdownEditor />
</Suspense>
)}
</>
);
}
Збирач (Vite) бачить динамічний import() і виносить MarkdownEditor з усіма залежностями в окремий файл.
Suspense показує fallback, поки код завантажується. Одна межа Suspense може охоплювати кілька лінивих компонентів.
Правила:
lazy- на верхньому рівні модуля, не всередині компонента. Інакше на кожен рендер створюється новий компонент - стан губиться, і код завантажується знову;- модуль має експортувати компонент за замовчуванням (
export default). Для іменованого експорту - проміжний модуль чиimport('./x').then((m) => ({ default: m.Editor })); - помилка завантаження (мережа, файл зник після деплою) кидається як виняток - потрібна межа помилок (Error Boundary), щоб показати повідомлення й кнопку «Оновити».
Що варто виносити:
- сторінки й маршрути - найприродніша межа; маршрутизатори (React Router, TanStack Router) мають для цього вбудовану підтримку;
- важкі бібліотеки - редактори, графіки, карти, PDF;
- рідко потрібне - модальні вікна налаштувань, експорт.
Не варто дробити кожен компонент: багато дрібних файлів - багато запитів і затримки «водоспадом». Межі - за сценаріями використання.
Попереднє завантаження: щоб користувач не чекав після кліку, імпорт можна запустити заздалегідь - при наведенні на кнопку: onMouseEnter={() => import('./MarkdownEditor')}. Повторний import() використає вже завантажений модуль.
Inertia розділяє код за сторінками автоматично, якщо в resolve використовується import.meta.glob без eager: true.
React Developer Tools - розширення браузера (Chrome, Firefox, Edge) з двома вкладками: Components і Profiler.
Components - дерево компонентів зі станом, props і хуками кожного. Корисне для продуктивності тим, що:
- показує, які props отримав компонент - видно, коли передається новий об'єкт чи функція на кожен рендер;
- у налаштуваннях можна ввімкнути «Highlight updates when components render» - компоненти, що перерендерились, підсвічуються на сторінці. Одразу видно, що введення в поле перемальовує всю сторінку.
Profiler - запис сесії і аналіз:
- натиснути «Record», виконати повільну дію (ввести текст, відкрити список), зупинити запис;
- для кожного коміту (застосування змін у DOM) - діаграма flamegraph: які компоненти рендерились і скільки часу зайняли;
- Ranked - компоненти, відсортовані за часом рендеру;
- клік на компонент - скільки разів і чому він рендерився.
«Why did this render?» - у налаштуваннях Profiler увімкнути «Record why each component rendered»: DevTools покаже причину - змінився стан, змінились певні props, змінився контекст, перерендерився батько.
На що дивитися:
- компоненти, що рендеряться, хоча їх дані не змінились - кандидати на опускання стану, розділення контексту чи мемоізацію;
- один повільний компонент з великим власним часом - важке обчислення в рендері;
- багато комітів на одну дію - ланцюжки оновлень стану в ефектах.
Значок React Compiler: компоненти, оптимізовані компілятором, позначені в DevTools міткою «Memo ✨» - видно, де компілятор спрацював, а де ні.
Важливо:
- вимірюйте production-збірку: режим розробки повільніший (додаткові перевірки, StrictMode) - час у ньому завищений. Для профілювання production потрібна спеціальна збірка з увімкненим профілюванням;
- вкладка Performance браузера доповнює React DevTools: показує, що поза React (мережа, розкладка, стилі) займає час. У нових версіях React додає в неї власні доріжки (React Performance tracks) з етапами рендеру;
- спершу виміряти, потім оптимізувати - інтуїція щодо того, що повільне, часто помиляється.