Що оптимізує компілятор React та що залишає на вашій совісті
Зрозумійте, яку роботу з продуктивністю автоматизує React Compiler, чому повільні API та великі бандли залишаються вашою відповідальністю, та як безпечно впровадити його у існуючу базу коду React.
Компілятор React усуває багато випадків ручної мемоізації, що спонукає вважати його засобом для вирішення проблеми повільних додатків. Однак це не так: він оптимізує лише процес відображення, який контролює React, і нічого більше. Нижче ви побачите, де проходить ця межа, які проблеми з повільністю все ще потребують участі інженера, та як активувати компілятор у існуючому кодовому базисі без виникнення проблем.
Протягом тривалого часу поради щодо продуктивності React складалися з короткого списку інструментів:
useMemo()
useCallback()
React.memo()
Компонент перерендерувався занадто часто, тож його обгорнули. Обчислення здавалося трудомістким, тож його зберегли у кеші. Колбек викликався у дочірній компонент, тож його стабілізували. Кожне рішення полягало у ручній мемоізації з використанням масиву залежностей для його підтримки.
Компілятор React змінює цей стандарт. Версія 1.0 вважається стабільною, і рекомендація команди React щодо нового коду — дозволити компілятору самостійно займатися мемоїзацією, замість того щоб вручну використовувати useMemo та useCallback. Ознайомтесь із поточною документацією React для отримання найновіших порад щодо її використання.
Суть можна сформулювати одним реченням: компілятор може зробити роботу React ефективнішою, але він не впливає на операції, які відбуваються поза React.
Що насправді робить компілятор
Під час збирання проекту компілятор аналізує компоненти та хуків та вставляє механізми мемоїзації там, де, за його аналізом, це є корисним. Розглянемо компонент, який фільтрує список товарів перед його відображенням:
function ProductList({ products }) {
const visible = products.filter(
product => product.inStock
);
Фільтрований масив потім безпосередньо передається до дочірнього компонента:
return <List items={visible} />;
}
До появи компілятора розробник, який орієнтувався на продуктивність, часто явно кешував фільтр, щоб List отримував ту саму посилання на масив до моменту зміни значення products:
const visible = useMemo(
() => products.filter(
product => product.inStock
),
[products]
);
З використанням компілятора зазвичай не потрібно писати такий код. Він сам визначає залежності, може кешувати дані точніше, ніж це роблять вручну написані хуки, і не залежить від правильного переліку залежностей з вашого боку. React називає це автоматичним кешуванням компонентів та хуків.
Проте зверніть увагу на контекст цього опису – кожен елемент списку є конструкцією React.
Ментальна модель: оптимізатор відображення
Корисно уявляти компілятор як інструмент, який працює над двома типами завдань у React та оптимізує обидва:
React Compiler
│
┌───────────┴───────────┐
▼ ▼
Component work Hook calculations
│ │
▼ ▼
memoization memoization
│ │
└───────────┬───────────┘
▼
fewer unnecessary
React updates
Кількість зайвих перерендерингів може суттєво зменшитися. Однак найважливішим у діаграмі є те, що вона не показує.
Повільні бекенди — поза її досяжністю
Розгляньмо асинхронний компонент, який чекає на відповідь API перед відображенням панелі керування:
async function Dashboard() {
const data = await fetch(
"https://api.example.com/dashboard"
);
Як тільки дані надходять, вони передаються у інтерфейс користувача:
return <DashboardUI data={data} />;
}
Якщо цей ендпоїнт виконується протягом 800 мс, компілятор не зможе скоротити цей час до 50 мс. Він може лише спростити процес перетворення даних у інтерфейс користувача в React, але не прискорити реакцію бекенду. Ось приблизна часова шкала:
Browser
│
▼
React
│
▼
API request ──────────► 800ms
│
▼
Response
│
▼
React render
Мемоізація використовується майже виключно на останньому етапі. Якщо більша частина часу витрачається на очікування мережі, ви оптимізуєте неправильний рівень; рішеннями є швидші запити, кешування, менші відповіді чи паралельне отримання даних. Щоб відокремити час обробки через мережу від часу на клієнті, дивіться Діагностика проблем продуктивності React поза межами часу відповіді API.
JavaScript, який не варто було додавати
Те саме правило стосується розміру пакета. Уявіть сторінку входу, яка завантажує важку бібліотеку для створення графіків, редактор тексту з розширеними функціями та SDK для аналітики ще до того, як хтось відкриє екрани, які їх використовують:
Initial page
│
├── React
├── UI
├── Chart library
├── Editor
└── Analytics SDK
Компілятор не видаляє жодних залежностей; кожен байт все одно завантажується, парсується та виконується. Допомагає розділення коду, наприклад завантаження екрана аналітики за потреби:
const Analytics = lazy(
() => import("./Analytics")
);
Тепер код завантажується лише тоді, коли функціонал дійсно відображається, а оточуючий межовий елемент Suspense показує альтернативний варіант поки відбувається завантаження. Це рішення, пов’язане з архітектурою бандлів, і жодна мемоїзація не може його замінити.
Де виникають непорозуміння
Поширена ситуація: додаток працює повільно, хтось увімкнув компілятор, і команда очікує саме такого результату:
Slow app
│
▼
React Compiler
│
▼
Fast app
Це занадто просто. Повільність має кілька причин, і компілятор охоплює лише одну з них:
SLOW APP
│
┌───────────────┼───────────────┐
▼ ▼ ▼
React work Network Browser
│ │ │
▼ ▼ ▼
Compiler API/cache JS/rendering
│ │ │
▼ ▼ ▼
Optimized Engineer Engineer
Компілятор — це лише один інструмент у наборі засобів для покращення продуктивності, а не весь цей набір. Мережева затримка та витрати браузера, такі як обробка скриптів та формування макету, все одно потребують участі людини.
Чи варто видаляти існуючі виклики useMemo?
Не автоматично. У новому коді офіційним стандартом є дозволити компілятору використовувати мемоїзацію. Для існуючого коду React рекомендує залишити поточну мемоїзацію без змін або видалити її лише після ретельних тестів. Причина цього полягає у поведінці коду, а не лише у продуктивності: значення, яке було змемоїзоване та використовується як залежність у Effect, визначає час його повторного виконання, тому зміна способу мемоїзації може вплинути на час виконання побічних ефектів.
Тож стримуйте бажання проводити таку чистку всього кодового базису:
useMemo → DELETE
useCallback → DELETE
React.memo → DELETE
Безпечніший підхід — поступовий, із вимірюванням на кожному етапі:
Existing app
│
▼
Enable compiler
│
▼
Run tests
│
▼
Profile
│
▼
Remove unnecessary
manual memoization
carefully
Компілятор існує для того, щоб у майбутньому зменшити необхідність вручну застосовувати мемоїзацію, а не для того, щоб одразу оголосити кожну існуючу оптимізацію неправильною.
Компілятор як інструмент перевірки якості коду
Перевага, якій приділяється менше уваги: компілятор включає етапи перевірки, засновані на правилах React, а його інтеграція з инструментом лінтингу може виявляти подібні проблеми:
set-state-in-render
set-state-in-effect
unsafe ref access
React зазначає, що ці діагнози можуть виявити баги, які вже існують у додатку. Оскільки компілятор ігнорує код, який суперечить його припущенням, попередження також показують, які компоненти не оптимізовані, що робить його частиною щоденного циклу зворотного зв’язку, а не просто інструментом для покращення продуктивності.
Спочатку проаналізуйте, потім оберіть рішення
Якщо інтерфейс здається повільним, вимірюйте його продуктивність перед тим, як використовувати будь-які інструменти, і дозвольте результатам вимірювань визначити проблему:
SLOW UI
│
▼
PROFILE
│
┌────────┼────────┐
▼ ▼ ▼
React Network Browser
work │ │
│ │ │
▼ ▼ ▼
Compiler API/cache JS/render
│
▼
Targeted fix
Потім підберіть рішення відповідно до того, що показує інструмент профілювання:
- Непотрібне повторне відрендерування компонентів у React: тут може допомогти React Compiler.
- Повільний API: покращіть кінцеву точку або стратегію отримання даних.
Основні висновки
У старій моделі потрібно було вказувати React, що саме потрібно запам’ятовувати; у новій моделі ви можете писати правильний код для React та довіряти компілятору більшу частину роботи з відображення. Це підвищує цінність однієї навички — знання того, де насправді витрачається час.
- Компілятор оптимізує ту роботу, яку розуміє, а саме процес відображення контенту у React та обчислення хуків.
- Він не може зменшити об’єм відповіді API, прискорити запит до бази даних, видалити скрипт від стороннього постачальника чи стиснути зображення розміром 5 МБ.
- Він не може врятувати архітектуру, яка створює зайву роботу скрізь.
- У існуючому коді увімкніть цю функцію, протестуйте, проаналізуйте її роботу, і лише тоді видаліть ручне зберігання даних у пам’яті.
- Розглядайте її діагностику як безкоштовні звіти про помилки, пов’язані з порушенням правил React.
Не запитуйте, чи увімкнений компілятор, а де додаток витрачає свій час; саме для цього існує компілятор — щоб контролювати частку роботи React.