Головна / Статті / Спочатку вимірюйте: чому рання оптимізація ускладнює запуск додатків Next.js

Спочатку вимірюйте: чому рання оптимізація ускладнює запуск додатків Next.js

Подивіться, як передчасна мемоізація, широкі межі клієнтів та кеші, розташовані один над одним, додають складності додаткам Next.js, та як підхід, що враховує вимірювання на першому місці, допомагає зберігати їхню швидкість.

2401 слів

Сторінка Next.js завантажується за 1,2 секунди, хтось вирішує, що це занадто повільно, і розпочинається цикл оптимізації ще до того, як хтось перегляне профіль. Через кілька місяців у кодовій базі з’являється мемоізація скрізь, динамічні імпорти, які ніхто не тестував, кілька перекриваючих одне одного кешів та користувацькі шляхи відображення, і додаток стає ще складнішим для розуміння, ніж був через повільність. У цьому посібнику пояснюється, чому така практика настільки поширена, які саме звички її спричиняють, та як замінити їх робочим процесом, який ґрунтується на даних та додає складність лише тоді, коли це обґрунтовано числами.

Справна помилка: складність перед доказами

Кожна окрема оптимізація зазвичай здається доцільною під час перегляду коду. useMemo тут, кеш там, розділений пакет для складного компонента. Проблема полягає у їхній кумулятивності: кожен механізм додає нову поведінку кешування, яку потрібно зрозуміти, ще один шлях відображення для дебагування, ще одну межу, про яку потрібно думати, та ще більше коду, пристосованого до певних умов продуктивності, який потрібно підтримувати у робочому стані.

Тож небезпека, від якої потрібно захищатися, — це не брак оптимізацій. Це впровадження нових механізмів до того, як ви з’ясуєте, що саме працює повільно, чому це відбувається та чи змінить його виправлення щось у тому, що помічає користувач. Перш ніж налаштовувати виконання, зробіть систему достатньо прозорою, щоб ви могли її вимірювати.

Проаналізуйте перед тим, як щось змінювати

Найпоширенішою формою цієї помилки є реакція на загальне уявлення про те, що продуктивність має значення. Розробник починає редагувати код без профілювання, не ізолюючи «вузькі місця» та не перевіряючи, чи користувачі очікують на щось конкретне.

Симптоми знайомі:

  • useMemo та useCallback, використані для дешевих обчислень
  • додавання рівнів кешування для даних, отримати які ніколи не було складно
  • розділення коду, застосоване ще до того, як хтось перевірив, які його частини є великими
  • абстракції, створені на основі гіпотетичного майбутнього навантаження
  • логіка відображення, яка розширює умови, щоб уникнути відображення елементів, які ніхто не вимірював

Часто первинна проблема взагалі майже не існує. Справжня робота з покращення продуктивності починається з даних: часу завантаження сторінки, аналізу об’єму коду, запису інформації від профілера React, даних мережевої активності та чіткого уявлення про те, де саме користувачі чекають. Оптимізація має вирішувати конкретну проблему, яку можна спостерігати, а не базуватися на розпливчастих побоюваннях щодо можливої повільності в майбутньому.

Практичне правило полягає у тому, щоб перед зміною коду записати показник, який ви плануєте змінити, та його поточне значення. Якщо ви не можете назвати це число, ви ще не готові до змін у реалізації.

Зазвичай проблема полягає у надмірній кількості JavaScript

Багато випадків повільності фронтенду не є загадковими. Від браузера вимагається завантажувати, парсити, компілювати та виконувати більше скриптів, ніж потрібно сторінці, і на телефонах середнього класу кожен з цих кроків є дуже витратним.

Простий панель керування може поступово накопичувати кілька бібліотек анімацій, великий набір компонентів, потужний менеджер стану, пакети для створення графіків, збірки утиліт та логіку на стороні клієнта для функцій, які могли б залишитися на сервері. Жоден з цих елементів окремо не виглядає загрозливим. Разом вони створюють велику кількість роботи, перш ніж сторінка зможе комфортно відреагувати на вхідні дані. У такий момент команди часто починають прагнути до кращих показників у Lighthouse, ніби для вирішення проблеми потрібна якась хитра стратегія.

Next.js вже надає сильні стандартні налаштування: серверне рендеринг, автоматичне розділення коду за маршрутами та модель, орієнтована на сервер і побудована на React Server Components. Ці стандартні параметри втрачають багато своєї цінності, коли великі частини додатку все одно передаються у браузер. Тож, перш ніж вдаватися до інших технік, перевірте, скільки коду ви передаєте, які залежності домінують у кожному маршруті та чи варта кожна з них своєї ваги. Багато додатків не потребують більш складної оптимізації; їм потрібно менше JavaScript. Щоб детальніше дізнатися, що ще, окрім розміру бандлу, може уповільнити сторінку, перегляньте статтю про те, що насправді уповільнює веб-додаток.

Межі клієнта, які поступово рухаються вгору

Найшвидший спосіб приховати архітектурні переваги App Router — це позначати занадто багато коду як клієнтський. Хтось потребує інтерактивності глибоко в структурі дерева, додає "use client" до батьківського елемента, потім до дідуся цього елемента, і незабаром компоненти, які лише відображають статичний маркування, читають дані з сервера або формують макет, стають частиною клієнтського пакету просто тому, що знаходяться нижче цієї директиви.

Ця зміна впливає не лише на місце виконання коду. Зазвичай це означає:

  • більше JavaScript, яке надсилається до браузера
  • більше роботи з ініціалізацією перед тим, як сторінка стане інтерактивною
  • додатковий клієнтський стан, який потрібно керувати
  • більше місць, де дані сервера та клієнта можуть вийти з синхронності

У цьому є іронія. Багато команд переходять на сучасний Next.js саме для того, щоб отримати архітектуру з акцентом на сервер, а потім поступово перебудовують важкі за ресурсами односторінкові додатки, які намагалися покинути.

Корисне запитання під час проектування: яка найменша частина цього інтерфейсу дійсно потребує браузера? Кнопка «Лайк», випадаючий список чи поле форми можуть потребувати стану на клієнті; картки, списки та сторінки навколо них зазвичай — ні. Якщо чітко визначити межі та, де це можливо, передавати серверно оброблений контент у компоненти клієнта як дочірні елементи, сервер зможе виконувати більше завдань, залишаючи інтерактивні елементи зосередженими. Довгостроково це призводить до менших розмірів пакетів та простішої моделі мислення. Механізми цього описані у статті про те, як React Server Components уникають включення коду до пакета.

Як передчасна оптимізація робить архітектуру крихкою

Робота над продуктивністю перетворюється на проблему технічного обслуговування, коли оптимізації з’являються швидше, ніж хтось може довести їхню корисність. Це зазвичай починається з невеликих кроків: компонент стає мемоїзованим, з’являється власний кеш, додається хук для пропуску процесу відображення, а потім з’являється ще один шар для підтримки консистентності двох елементів стану. На той момент ніщо не здається ризикованим.

Через кілька місяців кодова база містить власні хуки, взаємодія яких важко простежити, правила інвалідації, які розуміють лише кілька осіб, ланцюги мемоїзованих значень, умови відображення, засновані на припущеннях, які більше не є актуальними, а також код синхронізації, який існує переважно тому, що це вимагала попередня оптимізація.

Це має реальну ціну. Процес інтеграції займає більше часу, для виправлення помилок потрібно більше контексту, а невеликі зміни функцій в кінцевому підсумку впливають на механізми, які спочатку були створені для прискорення. Правильне запитання щодо будь-якої оптимізації полягає не в тому, чи покращує вона показники продуктивності, а в тому, чи є це покращення достатньо значним, щоб компенсувати архітектурні витрати, які воно створює. Метою є стабільна продуктивність: додаток, який швидко реагує, при цьому його повсякденний код залишається простим та зрозумілим, а не наповненим хитрощами для прискорення.

Сприймана продуктивність — це проблема користувацького інтерфейсу

Інженери схильні до того, що можна точно виміряти — мілісекунди, розміри пакетів даних, кількість операцій відтворення та оцінки. Користувачі ж сприймають щось більш широке. Зменшення часу завантаження сторінки на 100 мс майже не має значення, якщо навігація заплутана, стани завантаження не надають жодної інформації, елементи керування здаються нереактивними, макет змінюється під час отримання контенту, а важлива дія не дає ознак того, що вона була виконана.

Візьмемо форму, яка потребує два секунди для надсилання. Зменшення часу обробки на сервері до 1,7 секунди — це справжній технічний прогрес. Надання негайної інформації, заборона на подвійне надсилання за допомогою блокування кнопки та відображення чіткого прогресу часто значно покращують користувацький досвід, навіть якщо швидкість обробки запиту залишається такою ж, як і раніше.

Це різниця між виміряною та сприйманою продуктивністю. Люди оцінюють інтерфейс за тим, чи реагує він на їхні дії, чи розуміють вони те, що відбувається, та чи здається він стабільним під час користування. Тому ефективна робота над продуктивністю фронтенду ґрунтується як на дизайні взаємодії, так і на внутрішніх механізмах відображення контенту. Інструменти на кшталт переходів у React, оптимістичних оновлень та станів скелета належать до того ж набору засобів, що й аналіз пакетів.

Кешування допомагає, поки ніхто не може це пояснити

Кешування може принести значні переваги, оскільки система припиняє повторювати дорогі операції. Проблеми виникають тоді, коли команда більше не може визначити, яку версію даних має бачити певний користувач.

Типова ситуація: сторінка стає швидкою, а потім з’являються застарілі дані. Дану запис редагують, один екран оновлюється, тоді як інший продовжує відображати старе значення. У режимі розробки все працює коректно, а в продакшені — ні, і розслідування перетворюється на пошук відповідей на питання про те, який кеш надав відповідь, який шар було скасовано та який запит створив результат.

Великі додатки на Next.js роблять це особливо простим, оскільки повторне використання може відбуватися на багатьох рівнях: у вашому власному коді, у кешуванні даних та маршрутів фреймворку, у окремих викликах fetch, через CDN, у браузері та сервісах на серверній стороні. Додавання ще одного рівня без розуміння того, як вони взаємодіють, може зменшити затримку, але водночас збільшити кількість станів, у яких може перебувати система. Зверніть увагу, що стандартні налаштування кешування в Next.js змінювалися у міжверсійних оновленнях, тому перевіряйте поведінку версії, яку ви використовуєте, у поточній документації, а не покладайтеся на старі посібники.

Здатність до спостереження має йти перед агресивним кешуванням. Для будь-якої кешованої відповіді команда повинна мати можливість відповісти на такі запитання:

  • звідки походить відповідь
  • як довго вона, як очікується, залишатиметься дійсною
  • що її анулює
  • що відбувається, якщо анулювання зазнає невдачі

Кеш, який прискорює роботу системи, але робить її поведінку непередбачуваною, — це не безумовна перевага.

Повільність може зовсім не полягати у React

Іноді затримка стає помітною лише на фронтенді, а не там, де вона починається. Коли інтерфейс потребує кількох секунд, щоб стати придатним для використання, команда може почати оптимізувати процес відображення, зберігати дані компонентів у пам’яті чи переструктурувати стан клієнта. Ці зміни можуть заощадити кілька мілісекунд роботи браузера, поки сторінка все ще чекає на запит до бази даних тривалістю дві секунди чи на відповідь від ендпоїнта, який надсилає значно більше даних, ніж потрібно для відображення.

Багато швидких додатків зсередини є досить простими. Вони постачаються у досить невеликих пакетах, мають чіткі межі відображення, зберігають мінімум інформації про стан клієнта, отримують дані прогнозовано та мають архітектуру, яку новий розробник може зрозуміти без необхідності розбирання складних механізмів.

Ця простота приносить користь у міру зростання додатку. Коли потік даних є очевидним, легко виявити ресурсомісткі операції. Коли межі клієнта вузькі, зрозуміло, за що відповідає браузер. Коли правила кешування є небагатьма та чіткими, проблеми у продакшені легше діагностувати.

Розумна оптимізація є привабливою частково тому, що демонструє технічну майстерність, але кожен механізм стає чимось, що майбутні розробники повинні зрозуміти, виправити, зберегти або зрештою прибрати. Усе це не спростовує необхідності оптимізації. Це аргументує на користь найпростішої реалізації, яка відповідає реальним вимогам до продуктивності, додаваючи складність лише тоді, коли показники показують, що простий дизайн вже не може ефективно функціонувати. Система, яка є трохи менш розумною, але набагато простішою для розуміння, зазвичай слугує краще з часом.

Розглядайте показники як сигнали, а не як цілі

Інструменти для тестування є цінними, тому що дозволяють перевіряти невидимі характеристики. Проблема починається тоді, коли підвищення показників стає важливішим за покращення самого продукту.

Високий результат тесту Lighthouse не гарантує хорошого дизайну взаємодії, зберігання архітектури, надійної роботи в продакшені чи швидкого виконання завдань, які важливі для користувачів. Вимірювання в лабораторних умовах проводяться за контрольованими припущеннями; справжні відвідувачі використовують різні пристрої, мережі, обсяги даних, стани автентифікації та шляхи навігації. Саме тому дані з реальних сесій, такі як Core Web Vitals, є корисним доповненням.

Це не робить лабораторні показники неважливими; це змінює спосіб їх використання:

  • Якщо показник вказує на справжню проблему, дослідіть її.
  • Якщо зміна підвищує оцінку, але додає значну складність, при цьому користувачі майже нічого не помічають, поставте під сумнів цей компроміс.
  • Зв’язуйте кожен показник із поведінкою користувача, яку можна описати.

Метою є не додаток, який чудово виглядає під час тестування, а додаток, який дозволяє людям виконувати свою роботу без зайвих затримок чи перешкод.

Робочий процес, заснований на вимірюваннях

Якщо об’єднати ці ідеї, стабільний цикл виглядає так:

  1. Визначте проблему, з якою стикається користувач, та показник, який її описує.
  2. Виміряйте поточне значення як у лабораторних, так і, за можливості, у реальних умовах.
  3. Відстежте весь шлях запиту, щоб знайти основну причину витрат.
  4. Спробуйте зміни, які спочатку скорочують обсяг роботи: менше залежностей, вужчі межі клієнта, менший об’єм даних, швидші запити.
  5. Звертайтеся до методів мемоїзації, додаткового кешування чи коригованої обробки лише тоді, коли цих змін недостатньо.
  6. Знову виміряйте та залиште зміну лише у разі, якщо отримана користь перевищує витрати на її підтримку.

Межа клієнта — на листку, не на сторінці

Директива 'use client' на сторінці тягне завантаження даних у браузерний бандл. Залиште сторінку серверним компонентом.

// app/dashboard/page.tsx — a Server Component, no 'use client'
import { Chart } from './chart';

export default async function Dashboard() {
  const points = await loadSeries();
  return <Chart points={points} />;
}

Мемоізуйте листок лише після того, як профіль покаже, що дороге саме малювання.

'use client';

export const Chart = ({ points }: { points: number[] }) => {
  return <svg data-count={points.length} />;
};

Основні висновки

  • Найбільш дорогою помилкою щодо продуктивності в Next.js є оптимізація до того, як зрозуміти, що робить система, а не забування про оптимізацію.
  • Більшість проблем з підтримкою коду виникає через накопичену складність: зайвий клієнтський код, багато рівнів кешування, можливі до уникнення процеси ініціалізації та спекулятивні абстракції.
  • Зазвичай краще усунути зайві операції, ніж додавати нові механізми — чи то шляхом використання меншої кількості JavaScript, зберігання компонентів на сервері, спрощення стану, видалення зайвих запитів, виправлення повільних викликів до бекенду чи видалення оптимізацій, які коштують більше, ніж економлять.
  • Деяким додаткам справді потрібні складні стратегії кешування чи відображення, але це рішення має ґрунтуватися на вимірюваннях та чіткій діагностиці.
  • Найшвидшим додатком часто є той, який виконує найменшу кількість зайвих операцій.