Тема: Аналіз продуктивності. Використання панелі Profiler у React DevTools для ідентифікації “важких” компонентів та їх оптимізації.
Мета: Навчитися ідентифікувати проблеми продуктивності (“вузькі місця”) у React-додатках на етапі виконання (Runtime); опанувати інструмент React Profiler для запису та аналізу життєвого циклу (фази Commit та Render) компонентів; практично застосувати техніки оптимізації — мемоїзацію обчислень (useMemo), мемоїзацію функцій (useCallback) та поверхове порівняння пропсів (React.memo) для ліквідації зайвих рендерів.
Необхідні інструменти: Браузер (Chrome/Edge), розширення React Developer Tools, редактор VS Code, локально запущений проєкт.
useMemo.React.memo) та хука useCallback для оптимізації списку.Давайте зламаємо наш додаток. Відкрийте компонент “Стрічка новин” або “Профіль юзера” з попередніх практик. Додамо в нього “важку математичну функцію”, яка симулює уповільнення 3D-рендеру чи аналізу мільйонів рядків.
// src/pages/NewsFeed.jsx (Фрагмент)
import { useState } from "react";
// Це ДУЖЕ повільна функція.
// Вона займає ~100-300 мілісекунд при кожному своєму виклику.
const generateHeavyAnalytics = (num) => {
console.log("Запуск важких обчислень...");
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += num;
}
return result;
};
const NewsFeed = () => {
const [inputValue, setInputValue] = useState("");
const [analyticsNumber, setAnalyticsNumber] = useState(1); // Число для аналітики
// ВИКЛИК ПОМИЛКИ: Ми викликаємо функцію ТУТ, тобто при КОЖНОМУ рендері NewsFeed
const analyticsResult = generateHeavyAnalytics(analyticsNumber);
return (
<div style=>
<h1>Стрічка новин</h1>
<p>Результат аналітики: {analyticsResult}</p>
{/* Простий інпут. Він зберігає свій текст у стейт inputValue */}
<input
placeholder="Пошук (друкуйте повільно)..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</div>
);
};
Спробуйте ввести літери у поле пошуку. Ви помітите, що клавіатура “лагає” — літери з’являються з величезною затримкою.
Нам потрібно довести, що проблема саме в рендері, за допомогою інструментів розробника (адже в реальному житті код не такий очевидний).
Аналіз “Flamegraph” (Графіка полум’я):
Ви побачите жовті та зелені прямокутники (коміти). Натисніть на один з прямокутників NewsFeed.
У правій панелі “Render duration” буде написано час рендеру. (наприклад, 312.4 ms). Для плавного UI нормою вважається 16 ms (щоб видати 60 FPS).
Висновок: Наш компонент NewsFeed перераховується повністю занадто довго.
useMemo)Проблема полягає в тому, що коли ми вводимо текст в інпут, оновлюється inputValue. Це викликає новий рендер NewsFeed. Новий рендер зумовлює виклик кожної функції всередині компонента, зокрема нашої generateHeavyAnalytics.
Вирішимо це кешуванням:
// Змінюємо виклик у NewsFeed.jsx:
import { useState, useMemo } from "react";
// ...
const [inputValue, setInputValue] = useState("");
const [analyticsNumber, setAnalyticsNumber] = useState(1);
// ОПТИМІЗАЦІЯ: React запам'ятає результат цієї функції.
// Він запустить її ЗНОВУ ТІЛЬКИ ЯКЩО зміниться [analyticsNumber].
// Якщо змінюється [inputValue] (ми друкуємо) -> функція НЕ виконується.
const analyticsResult = useMemo(() => {
return generateHeavyAnalytics(analyticsNumber);
}, [analyticsNumber]);
// ...
Спробуйте знову писати в інпут і запишіть ще одну сесію у Profiler. Час рендеру має впасти до < 1 ms!
Тепер змоделюємо іншу популярну помилку: “Зайві рендери дочірніх компонентів”.
// src/components/ActivityItem.jsx (З Практичної №5)
import React from "react";
const ActivityItem = ({ title, onLike }) => {
// Якби цей компонент перемальовувався, він би блимав або виводив це:
console.log(`ActivityItem (${title}) відрендерився!`);
return (
<div>
{title} <button onClick={onLike}>Like</button>
</div>
);
};
export default ActivityItem;
У Dashboard.jsx, де у вас десятки цих ActivityItem, якщо ви надрукуєте хоч одну букву у якомусь формовому інпуті, ВЕСЬ список почне перемальовуватися (можете перевірити консоль). Це марнотратство, бо пости не змінилися!
React.memo + useCallback)ActivityItem: “Перемальовуйся, ТІЛЬКИ якщо твої props змінилися”:// Обертаємо експорт у React.memo (HOC)
export default React.memo(ActivityItem);
Поверніться до Dashboard. Але проблема не зникла! Тому що функція onLike={handleLike} створюється наново (в пам’яті це вже інший об’єкт-функція) при кожному рендері Dashboard. React.memo бачить, що надійшла “Нова” функція в onLike і вирішує, що треба перемалювати компонент!
Кешуємо саму функцію в батьківському Dashboard.jsx:
import { useCallback } from "react";
// ... у Dashboard.jsx
// useCallback фіксує посилання на функцію в пам'яті.
// Вона не буде створюватися наново при кожному рендері Dashboard.
const handleLike = useCallback((id) => {
console.log("Liked", id);
}, []); // Порожній масив, бо вона ні від чого не залежить
// ... рендер
<ActivityItem title="Пост" onLike={handleLike} />;
Запишіть результат у Profiler після натискання клавіш в інпуті. Тепер ActivityItem отримає статус Did not render (сірий колір у DevTools).
React.memo + useCallback.Array(10000).fill().map(...)). Зробіть це через useState звичайним способом і через useMemo, порівнявши просідання FPS (падіння кадрів) під час швидких кліків або набору тексту.useMemo та компонентом вищого порядку (HOC) React.memo().React.memo() разом із передачею callback-функцій як пропсів (напр. onClick={handleClick}) майже завжди вимагає обов’язкового використання useCallback на рівні батьківського компонента (поясніть через концепцію “Referential Equality” або рівності посилань в JavaScript)?useMemo та useCallback? Наведіть об’єктивний приклад, коли “Оптимізація” простого рядка чи базового обчислення принесе більше мінусів (падіння продуктивності), ніж плюсів.dependency array) ви вкажете для useCallback , якщо всередині зафіксованої функції ви звертаєтеся до значення стейту [count, setCount], і чому саме такий?