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

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.

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

React Developer Tools - розширення браузера (Chrome, Firefox, Edge) з двома вкладками: Components і Profiler.

Components - дерево компонентів зі станом, props і хуками кожного. Корисне для продуктивності тим, що:

  • показує, які props отримав компонент - видно, коли передається новий об'єкт чи функція на кожен рендер;
  • у налаштуваннях можна ввімкнути «Highlight updates when components render» - компоненти, що перерендерились, підсвічуються на сторінці. Одразу видно, що введення в поле перемальовує всю сторінку.

Profiler - запис сесії і аналіз:

  1. натиснути «Record», виконати повільну дію (ввести текст, відкрити список), зупинити запис;
  2. для кожного коміту (застосування змін у DOM) - діаграма flamegraph: які компоненти рендерились і скільки часу зайняли;
  3. Ranked - компоненти, відсортовані за часом рендеру;
  4. клік на компонент - скільки разів і чому він рендерився.

«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) з етапами рендеру;
  • спершу виміряти, потім оптимізувати - інтуїція щодо того, що повільне, часто помиляється.

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