Ось готовий урок, створений за твоїм майстер-промптом у стилі David Malan.
🚀 Тема уроку: Оптимізація продуктивності React-застосунку
Привіт, друзі! Це CS50 (умовно 😉), і сьогодні ми поговоримо про швидкість.
1. 🔥 Вступ: Коли все починає "гальмувати"
Уявіть ситуацію. Ви створили чудовий інтернет-магазин. Все працює: товари додаються, фільтри фільтрують, кошик рахує. Але раптом ви помічаєте дивну річ. Ви починаєте вводити текст у звичайне поле пошуку, а літери з'являються з затримкою у пів секунди. Текст "лагає". Ви скролите список, а він "смикається".
Знайомо? Це найстрашніший сон фронтенд-розробника — проблеми з продуктивністю.
Запитайте себе: * Чому код, який ідеально працював з 10 товарами, "вмирає" на 10 000? * Чому зміна кольору однієї кнопки змушує React перемальовувати всю сторінку?
Аналогія: Уявіть, що ви прийшли в ресторан і замовили склянку води. Офіціант приймає замовлення, біжить на кухню, і... шеф-кухар починає готувати заново все меню ресторану: борщ, стейки, десерти. Він все це готує, викидає, і тільки потім наливає вам воду.
Абсурд? Так. Але саме так часто поводиться React "з коробки", якщо ми не навчимо його бути розумнішим. Сьогодні ми навчимо нашого "шеф-кухаря" (React) готувати тільки те, що замовили.
2. 🧠 Теоретична база (Що там "під капотом"?)
React — це бібліотека, яка любить оновлюватись. Коли змінюється стан (state) або властивості (props), компонент каже: "О, щось змінилось! Я маю перемалювати себе і всіх своїх дітей".
Ось тут криється пастка.
Як це працює (спрощено):
- Trigger: Щось змінилось (наприклад, ви ввели літеру в
input). - Render Phase: React викликає функцію вашого компонента. Він будує нове віртуальне дерево (Virtual DOM).
- Commit Phase: React порівнює нове дерево зі старим. Якщо є різниця — він оновлює реальний DOM у браузері.
У чому проблема? Пункт №2. React може викликати функції тисячі компонентів, витратити час процесора, щоб у підсумку зрозуміти: "А, стоп, візуально нічого не змінилось". Ця робота була марною.
Ключові поняття, які треба знати:
- Re-render (Перерендер): Повторний виклик функції компонента. Це не завжди погано, але це витрачає ресурси.
- Memoization (Мемоізація): Це як "шпаргалка". Якщо ми вже порахували складний приклад (2 + 2 = 4), ми запам'ятовуємо результат. Наступного разу, коли спитають "2 + 2", ми не рахуємо, а просто кажемо "4".
- Virtualization (Віртуалізація): Якщо у вас список з 10 000 товарів, користувач бачить лише 10. Навіщо малювати інші 9 990? Ми малюємо тільки те, що у "вікні" перегляду.
Інтуїтивно: Оптимізація — це мистецтво робити менше роботи. Це мистецтво казати React: "Стій! Тут нічого не змінилось, візьми стару версію".
3. 🧪 Приклади (Від "гальм" до "польоту")
Приклад 1: "Балакучий батько" (Проблема)
У нас є Батьківський компонент (App), який має лічильник, і Дочірній компонент (HeavyComponent), який просто виводить текст.
import React, { useState } from 'react';
// Уявіть, що цей компонент дуже важкий
const HeavyComponent = () => {
console.log("🥵 HeavyComponent рендериться!");
return <div className="heavy">Я дуже важкий компонент</div>;
};
export default function App() {
const [count, setCount] = useState(0);
console.log("App рендериться");
return (
<div>
<h1>Лічильник: {count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* ПИТАННЯ: Чи буде рендеритися цей компонент при натисканні кнопки? */}
<HeavyComponent />
</div>
);
}
Що ви очікуєте побачити в консолі при кліку на кнопку?
Логічно було б, щоб HeavyComponent мовчав, адже він не залежить від count.
Реальність:
При кожному кліку ви бачите: 🥵 HeavyComponent рендериться!.
Чому? Тому що батько (App) оновився, і за замовчуванням він "смикає" всіх дітей.
Приклад 2: Рішення через React.memo
Давайте "заморозимо" важкий компонент. Використаємо React.memo.
// Огортаємо компонент у memo
const HeavyComponent = React.memo(() => {
console.log("🥵 HeavyComponent рендериться!");
return <div className="heavy">Я дуже важкий компонент</div>;
});
Результат: Тепер при кліку на лічильник в App, HeavyComponent мовчить. React перевірив: "Пропси прийшли? Ні. Пропси змінились? Ні. Тоді беру стару версію". Перемога!
Приклад 3: Пастка з об'єктами та функціями (useCallback)
Ускладнимо задачу. Передамо у HeavyComponent функцію.
export default function App() {
const [count, setCount] = useState(0);
// Функція, яку ми передаємо дитині
const handleClick = () => {
console.log('Клік у дитині');
};
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* HeavyComponent все ще загорнутий у React.memo */}
<HeavyComponent onClick={handleClick} />
</div>
);
}
Питання: Ми використали React.memo. Чи буде ре-рендер HeavyComponent при зміні count?
Реальність: Так, буде! 😱
Чому?
У JavaScript {} не дорівнює {}. І function() {} не дорівнює function() {}.
При кожному рендері App створюється нова функція handleClick. Для React.memo це виглядає так: "О, прийшов новий проп onClick! Треба оновлюватись!".
Рішення (useCallback):
import { useCallback } from 'react';
// Тепер функція "запам'ятовується" і не створюється наново
const handleClick = useCallback(() => {
console.log('Клік у дитині');
}, []); // [] означає, що функція ніколи не змінюється
// Тепер HeavyComponent знову спить спокійно!
4. 🛠 Практична частина
Час закотити рукави! Ось ваші завдання.
Завдання 1: "Детектив"
Відкрийте будь-який свій React-проєкт (або створіть простий). Додайте console.log('Render <Name>') у кожен компонент. Поклацайте інтерфейс. Знайдіть компонент, який рендериться, хоча візуально не змінюється.
Завдання 2: "Мемоізація списку"
Створіть список справ (ToDo). Зробіть так, щоб при введенні тексту в інпут для нової задачі, сам список вже існуючих задач не перерендерювався. (Підказка: React.memo для компонента списку).
Завдання 3: "Важкі обчислення"
Створіть компонент, який у циклі рахує факторіал числа 100 000 (або робить іншу важку операцію).
Додайте кнопку, яка просто змінює колір фону (dark mode).
Зробіть так, щоб при зміні теми "важка математика" не запускалася знову.
Інструмент: useMemo.
Завдання 4: "А що, якщо..." (Міні-кейс)
У вас є величезна таблиця користувачів (5000 рядків). Вона гальмує при скролі.
Питання: Чи допоможе тут React.memo?
Відповідь: Скоріше за все ні, бо DOM перевантажений кількістю елементів.
Рішення: Спробуйте знайти бібліотеку для віртуалізації (наприклад, react-window) і реалізувати її.
5. 💡 Мислення як у розробника
Тут новачки часто роблять помилку, яку називають "Передчасна оптимізація" (Premature Optimization).
Типові помилки:
- Огортати ВСЕ в
memo: "Про всяк випадок загорну все".- Чому це погано: Порівняння пропсів теж коштує ресурсів. Іноді швидше просто перемалювати легкий компонент, ніж порівнювати його пропси.
- Забувати масив залежностей: Використовувати
useMemoабоuseCallback, але забути додати змінну в[]. Це призводить до складних багів, коли дані не оновлюються.
Як думає Senior:
- Спочатку виміряй: Не оптимізуй наосліп. Використовуй React DevTools (Profiler). Запиши сесію, подивись, який компонент горить жовтим/червоним і скільки часу займає рендер.
- Локалізуй стейт: Якщо при введенні в input гальмує вся сторінка, можливо, треба просто винести цей input в окремий компонент? Тоді перерендерюватиметься тільки він, і ніякі
memoне будуть потрібні. - Структура даних: Іноді проблема не в React, а в тому, що ви фільтруєте масив з мільйона елементів прямо в
render. Винесіть це в Web Worker або оптимізуйте алгоритм.
6. 🧩 Підсумок
Отже, що ми сьогодні зробили?
Ми перестали бути марнотратними.
* Ми дізналися, що React за замовчуванням перемальовує все дерево вниз.
* Ми навчилися зупиняти це за допомогою React.memo (для компонентів).
* Ми навчилися стабілізувати функції та об'єкти за допомогою useCallback та useMemo.
* Ми зрозуміли, що спочатку треба знайти проблему профайлером, а потім її лікувати.
Тепер ви вмієте: Робити застосунки, які "літають", навіть на слабких телефонах. Ви економите батарею користувача і нерви замовника.
Наступного разу: Ваш застосунок швидкий, як блискавка ⚡. Але чи такий він безпечний? На наступному уроці ми поговоримо про Безпеку в React (XSS, валідація даних) і дізнаємось, як не дати хакерам зламати ваш магазин.
А поки що — пишіть код, оптимізуйте з розумом і до зустрічі!