Модуль 22

Оптимізація ререндерів і React.memo

Ось готовий урок, створений спеціально для тебе у стилі Девіда Малана (CS50).


🎓 CS50: React. Оптимізація ререндерів і React.memo

Привіт, світе! Радий бачити вас знову.

Сьогодні ми зазирнемо «під капот» React і розберемо тему, яка відрізняє простого кодера від інженера, що піклується про продуктивність. Ми говоримо про оптимізацію ререндерів.


1. 🔥 Вступ: Чому ваш додаток «гальмує»?

Уявіть, що ви — художник. Я прошу вас намалювати портрет сім'ї: тато, мама і дитина. Ви витрачаєте години, малюєте кожну деталь. Чудово!

Але раптом тато вирішує змінити краватку з червоної на синю. Ви, як сумлінний художник (або як React за замовчуванням), берете і... викидаєте стару картину і малюєте ВСЮ сім'ю з нуля. Маму, яка не змінилася. Дитину, яка сидить так само. І тата з новою краваткою.

Звучить абсурдно, чи не так? Навіщо перемальовувати маму, якщо змінився тільки тато?

У світі React це відбувається постійно. Коли батьківський компонент оновлюється, всі його діти оновлюються теж. Автоматично. Завжди.

Чому це проблема? 1. Батарея телефону: Користувач заходить на ваш сайт, і його телефон починає грітися, як піч. 2. Lagging (гальмування): Інтерфейс стає «в'язким» при скролі або вводі тексту.

Чи можемо ми навчити нашого «художника» бути розумнішим? Чи можемо ми сказати: "Гей, мама і дитина не змінилися — залиш їх у спокої"?

Відповідь — так. І інструмент для цього — React.memo.


2. 🧠 Теорія: Фейс-контроль для компонентів

Давайте розберемося, як це працює, без нудних формул.

Як працює React за замовчуванням?

React сповідує принцип: "Краще перестрахуватися, ніж щось пропустити". Якщо батько (Parent) каже "Я змінився!", React каже всім дітям (Children): "Перемальовуйтесь!".

Що робить React.memo?

React.memo — це компонент вищого порядку (HOC). Але давайте простіше.

Уявіть, що React.memo — це охоронець (фейс-контроль) на вході у ваш компонент. Коли React хоче перемалювати компонент, цей охоронець зупиняє його і перевіряє вхідні дані (props):

👮‍♂️ Охоронець (React.memo): "Стій! Твої пропси (props) змінилися з минулого разу?"

🤖 React: "Емм... ні, пропси ті самі: name="Alice", age={25}."

👮‍♂️ Охоронець: "Тоді проходь повз. Не чіпай цей компонент. Використовуй результат попереднього рендеру."

Це і є мемоізація (запам'ятовування результату).

🔑 Що треба запам'ятати залізобетонно:

  1. React.memo працює через поверхневе порівняння (shallow comparison).
  2. Він перевіряє тільки props. Якщо змінився стан (state) всередині самого компонента — він все одно перемалюється.
  3. Це інструмент оптимізації, а не логіки. Не використовуйте його, щоб запобігти виконанню коду, використовуйте його, щоб пришвидшити інтерфейс.

3. 🧪 Приклади: Від болю до перемоги

Давайте подивимось на код.

Сцена 1: Проблема (Зайві рухи)

У нас є Parent (лічильник) і Child (просто виводить текст).

// ChildComponent.jsx
const ChildComponent = ({ name }) => {
  console.log("Rendering Child..."); // Слідкуйте за консоллю!
  return <div>Привіт, {name}!</div>;
};

// ParentComponent.jsx
export default function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Лічильник: {count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>

      {/* Ми передаємо фіксоване ім'я */}
      <ChildComponent name="React Student" />
    </div>
  );
}

Що тут відбувається? Коли ви натискаєте кнопку +1: 1. count змінюється. 2. Parent ререндериться. 3. ChildComponent теж ререндериться, і ви бачите в консолі "Rendering Child...", хоча ім'я "React Student" не змінилося!

Сцена 2: Рішення з React.memo

Огортаємо наш компонент у магічну функцію.

import React, { memo } from 'react';

// Огортаємо компонент у memo
const ChildComponent = memo(({ name }) => {
  console.log("Rendering Child...");
  return <div>Привіт, {name}!</div>;
});

// Parent залишається тим самим...

Результат: Тепер, коли ви тиснете +1 у батька: 1. Parent ререндериться. 2. React доходить до ChildComponent. 3. memo перевіряє: name було "React Student", стало "React Student". Змін немає. 4. Ререндеру дитини НЕМАЄ. Консоль мовчить.

Ми щойно зекономили ресурси браузера! 🎉


Сцена 3: Пастка (Чому це не працює?)

А тепер увага. Це те, де помиляються 90% новачків. Уявіть, що ми передаємо в Child не рядок, а об'єкт або функцію.

// ParentComponent.jsx
export default function Parent() {
  const [count, setCount] = useState(0);

  // УВАГА: Цей об'єкт створюється НАНОВО при кожному рендері Parent!
  const styleConfig = { color: 'red' }; 

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+1</button>
      {/* Передаємо об'єкт у мемоізований компонент */}
      <ChildComponent name="Student" styleConfig={styleConfig} />
    </div>
  );
}

Питання до вас: Чи допоможе тут React.memo? ... ... Відповідь: Ні! Компонент буде ререндеритись.

Чому? Тому що в JavaScript { color: 'red' } !== { color: 'red' }. Це різні посилання в пам'яті. Кожен рендер батька створює новий об'єкт. memo бачить нове посилання і пропускає рендер. (Але про те, як це лікувати, ми поговоримо в наступному уроці про useMemo).


4. 🛠 Практична частина

Час забруднити руки кодом. Ось ваше завдання:

Завдання 1: "Детектив" Створіть простий додаток, де є поле вводу (input) для тексту і великий важкий список (просто компонент, який у циклі виводить 1000 div-ів). * Проблема: Коли ви друкуєте в input, список гальмує (бо він перемальовується на кожну літеру). * Рішення: Використайте React.memo для списку. Переконайтеся через console.log, що список більше не ререндериться при вводі тексту.

Завдання 2: "Зламай мене" Візьміть працюючий приклад із Завдання 1. Передайте в список пропсу onClick={() => console.log('click')}. * Подивіться, що memo перестало працювати. * Спробуйте пояснити самому собі вголос, чому так сталося (згадайте про функції та посилання).

Завдання 3: "А що, якщо..." Що буде, якщо огорнути в memo компонент, який приймає children?

const Box = memo(({ children }) => <div>{children}</div>);
  • Спробуйте використати його: <Box><p>Hello</p></Box>.
  • Чи допоможе тут memo? (Спойлер: JSX теж створює об'єкти, подумайте про це).

5. 💡 Мислення розробника (Pro Tips)

Ви можете подумати: "Круто! Я тепер обгорну ВЕСЬ свій додаток у React.memo. Продуктивність полетить у космос!"

Стривайте.

Чому не варто "мемоізувати" все підряд?

  1. Це не безкоштовно. Порівняння пропсів (prevProps vs nextProps) займає час процесора. Якщо компонент легкий (наприклад, проста кнопка), витрати на порівняння можуть бути більшими, ніж просто його перемалювати.
  2. Читабельність коду. Зайві обгортки ускладнюють код.

Як думає Senior Developer:

  • "Вимірюй, а не вгадуй". Використовуйте React DevTools Profiler. Якщо ви бачите, що компонент ререндериться і це займає багато часу (жовтий колір на графіку) — тоді застосовуйте memo.
  • "Мемоізуй листя, а не гілки". Частіше за все memo потрібен для компонентів нижнього рівня (великі таблиці, графіки, списки), які знаходяться всередині компонентів, що часто оновлюються.

6. 🧩 Підсумок

Отже, що ми сьогодні дізналися?

  1. React за замовчуванням ререндерить все дерево вниз від місця зміни.
  2. React.memo — це охоронець, який каже "Стоп", якщо пропси не змінилися.
  3. Він працює через поверхневе порівняння.
  4. Ми не використовуємо його всюди, лише там, де є реальні проблеми з продуктивністю.

🔮 Що далі?

Ви бачили проблему в "Сцені 3", де об'єкти та функції ламали нашу оптимізацію? Це класична пастка.

У наступному уроці ми познайомимося з двома найкращими друзями React.memo — хуками useCallback та useMemo. Вони допоможуть нам "заморожувати" функції та об'єкти, щоб наш охоронець міг спокійно виконувати свою роботу.

А поки що — це був CS50... тобто, урок з React. Побачимось!