nmk

Практична робота №9 (2 години)

Тема: Аналіз продуктивності. Використання панелі Profiler у React DevTools для ідентифікації “важких” компонентів та їх оптимізації.

Мета: Навчитися ідентифікувати проблеми продуктивності (“вузькі місця”) у React-додатках на етапі виконання (Runtime); опанувати інструмент React Profiler для запису та аналізу життєвого циклу (фази Commit та Render) компонентів; практично застосувати техніки оптимізації — мемоїзацію обчислень (useMemo), мемоїзацію функцій (useCallback) та поверхове порівняння пропсів (React.memo) для ліквідації зайвих рендерів.

Необхідні інструменти: Браузер (Chrome/Edge), розширення React Developer Tools, редактор VS Code, локально запущений проєкт.

План заняття

  1. Штучне створення проблеми продуктивності (Heavy Computation) у існуючому компоненті.
  2. Запис та аналіз сесії рендерингу за допомогою вкладки React Profiler.
  3. Оптимізація обчислень за допомогою хука useMemo.
  4. Синтетичне створення проблеми зайвих перемалювань списку (Рендер дочірніх компонентів).
  5. Використання компонентів вищого порядку (React.memo) та хука useCallback для оптимізації списку.

Хід виконання роботи

1. Створення штучної проблеми (Важкі обчислення)

Давайте зламаємо наш додаток. Відкрийте компонент “Стрічка новин” або “Профіль юзера” з попередніх практик. Додамо в нього “важку математичну функцію”, яка симулює уповільнення 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>
  );
};

Спробуйте ввести літери у поле пошуку. Ви помітите, що клавіатура “лагає” — літери з’являються з величезною затримкою.

2. Діагностика в React Profiler

Нам потрібно довести, що проблема саме в рендері, за допомогою інструментів розробника (адже в реальному житті код не такий очевидний).

  1. Відкрийте F12 і перейдіть на вкладку ⚛️ Profiler (сусідня від Components).
  2. Натисніть синю кнопку “Record” (Круглик), щоб почати запис.
  3. Надрукуйте в інпуті слово “Тест”.
  4. Натисніть червону кнопку “Stop” (Квадрат), щоб зупинити запис.

Аналіз “Flamegraph” (Графіка полум’я): Ви побачите жовті та зелені прямокутники (коміти). Натисніть на один з прямокутників NewsFeed. У правій панелі “Render duration” буде написано час рендеру. (наприклад, 312.4 ms). Для плавного UI нормою вважається 16 ms (щоб видати 60 FPS). Висновок: Наш компонент NewsFeed перераховується повністю занадто довго.

3. Оптимізація обчислень (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!

4. Штучна проблема з дітьми (Рендер списку)

Тепер змоделюємо іншу популярну помилку: “Зайві рендери дочірніх компонентів”.

// 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, якщо ви надрукуєте хоч одну букву у якомусь формовому інпуті, ВЕСЬ список почне перемальовуватися (можете перевірити консоль). Це марнотратство, бо пости не змінилися!

5. Оптимізація списку (React.memo + useCallback)

  1. Спочатку скажемо ActivityItem: “Перемальовуйся, ТІЛЬКИ якщо твої props змінилися”:
// Обертаємо експорт у React.memo (HOC)
export default React.memo(ActivityItem);
  1. Поверніться до Dashboard. Але проблема не зникла! Тому що функція onLike={handleLike} створюється наново (в пам’яті це вже інший об’єкт-функція) при кожному рендері Dashboard. React.memo бачить, що надійшла “Нова” функція в onLike і вирішує, що треба перемалювати компонент!

  2. Кешуємо саму функцію в батьківському 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).

Завдання для самостійного виконання

  1. В єдиному репозиторії відкрийте вкладку Profiler, увійдіть в “Налаштування” (Шестірня розширення DevTools) -> General. Увімкніть галочку “Highlight updates when components render”.
  2. Поклацайте по вашому додатку (перейдіть по сторінках, повводьте текст). Ви повинні побачити кольорові прямокутники моргання навколо елементів екрану, які браузер перераховує на льоту.
  3. Знайдіть компонент з вашої системи, який перемальовується дарма (блимає, хоча не має ніяк реагувати на вашу дію), і виправте це за допомогою описаних вище технік React.memo + useCallback.
  4. Вручну штучно уповільніть свій додаток: оберіть якийсь масив даних та відрендерете його 10 000 разів (у циклі Array(10000).fill().map(...)). Зробіть це через useState звичайним способом і через useMemo, порівнявши просідання FPS (падіння кадрів) під час швидких кліків або набору тексту.

Контрольні запитання

  1. Поясніть принципову різницю в області застосування між двома інструментами оптимізації від React: хуком useMemo та компонентом вищого порядку (HOC) React.memo().
  2. У вкладці React Profiler, в чому різниця між “Жовтим” (або червонішим) та “Зеленим” або “Сірим” кольором графіка полум’я (Flamegraph) під час запису сесії рендерингу?
  3. Чому використання React.memo() разом із передачею callback-функцій як пропсів (напр. onClick={handleClick}) майже завжди вимагає обов’язкового використання useCallback на рівні батьківського компонента (поясніть через концепцію “Referential Equality” або рівності посилань в JavaScript)?
  4. Чи існують накладні витрати на саму роботу внутрішніх алгоритмів хуків useMemo та useCallback? Наведіть об’єктивний приклад, коли “Оптимізація” простого рядка чи базового обчислення принесе більше мінусів (падіння продуктивності), ніж плюсів.
  5. Що таке функція ідентифікації причини перемальовування в React Profiler (“Why did this render?”), і в якій вкладці інструмента розробника її можна знайти для аналізу непотрібних оновлень (вказати конкретно)? Який масив залежностей (dependency array) ви вкажете для useCallback , якщо всередині зафіксованої функції ви звертаєтеся до значення стейту [count, setCount], і чому саме такий?