Сначала измерения: почему ранняя оптимизация затрудняет работу приложений Next.js
Узнайте, как преждевременная мемоизация, широкие границы клиента и кэши на разных уровнях усложняют приложения Next.js, и как подход, ориентированный на измерение производительности, помогает сохранять их скорость.
Страница 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
Иногда задержка становится заметной именно на фронтенде, но не там, где она начинается. Сталкиваясь с интерфейсом, который требует нескольких секунд, прежде чем станет использоваемым, команда может начать оптимизировать отрисовку, использовать мемоизацию компонентов или изменять структуру состояния клиента. Эти изменения могут сэкономить несколько миллисекунд работы браузера, пока страница всё ещё ждёт двухсекундного запроса к базе данных или ответа от конечной точки, возвращающего гораздо больше данных, чем требуется интерфейсу.
Представьте жизненный цикл такого запроса: браузер отправляет его, он проходит через промежуточные компоненты и механизмы авторизации, сервер обращается к бэкенду или базе данных, данные возвращаются обратно, и только тогда React выполняет отрисовку. Если большая часть времени уходит на ожидание ответа, незначительное ускорение последнего шага почти не помогает.
То же самое касается слишком больших объемов данных, последовательных сетевых запросов, которые можно было бы выполнять параллельно, дорогостоящих проверок авторизации, перегруженных сервисов и запросов без индексации. Ни одна из этих проблем не решается путем уменьшения частоты отрисовки компонентов. Полезно провести тщательное исследование всего процесса запроса, чтобы выяснить, куда уходит время. Узкие места не зависят от границ команд или от того, кто является владельцем кода.
Простые системы дольше остаются быстрыми
Многие быстрые приложения внутри себя не представляют особой сложности. Они поставляются в довольно небольших пакетах, имеют четко определенные границы отрисовки, минимизируют количество данных на стороне клиента, загружают данные предсказуемым образом, и их архитектура понятна новым разработчикам без необходимости анализа сложных технических решений.
Такая простота оказывается выгодной по мере роста приложения. Когда поток данных очевиден, легко выявить ресурсоемкие операции. Когда границы работы клиента четко определены, становится ясно, за что отвечает браузер. Когда правила кэширования немногочисленны и однозначны, производственные проблемы легче диагностировать.
Умная оптимизация привлекательна отчасти потому, что демонстрирует технические навыки, но каждый механизм становится чем-то, что будущим разработчикам необходимо понимать, отлаживать, сохранять или в конечном итоге удалять. Всё это не противоречит оптимизации. Это аргумент в пользу самой простой реализации, соответствующей реальным требованиям к производительности, при этом дополнительная сложность добавляется лишь тогда, когда измерения показывают, что простая конструкция уже не справляется со своей задачей. Немного менее изощренная система, которую гораздо проще понимать, обычно работает лучше со временем.
Рассматривайте показатели как сигналы, а не как цели
Инструменты для тестирования производительности ценны, потому что позволяют оценивать ранее невидимые характеристики. Проблемы начинаются тогда, когда повышение показателей становится важнее, чем улучшение самого продукта.
Высокий результат теста Lighthouse не гарантирует хорошего дизайна взаимодействия, удобной для обслуживания архитектуры, надежного поведения в производственных условиях или быстрого выполнения задач, которые важны для пользователей. Измерения в лабораторных условиях проводятся при контролируемых предпосылках; реальные посетители используют разные устройства, сети, объемы данных, состояния аутентификации и пути навигации. Именно по этой причине данные, собранные в реальных сессиях, такие как Core Web Vitals, являются полезным дополнением.
Это не делает лабораторные показатели незначимыми; это меняет способ их использования:
- Если показатель указывает на реальную проблему, необходимо её исследовать.
- Если изменение повышает оценку, но при этом увеличивает сложность, в то время как пользователи почти ничего не замечают, стоит задуматься о цене такого компромисса.
- Каждый показатель должен быть связан с поведением, видимым для пользователя и описуемым словами.
Цель — не приложение, которое отлично выглядит в тестах производительности. Цель — приложение, которое позволяет людям выполнять свою работу без ненужных задержек и препятствий.
Рабочий процесс, ориентированный на измерения
Если объединить эти идеи, устойчивый цикл будет выглядеть следующим образом:
- Определите проблему, с которой сталкивается пользователь, и показатель, отражающий её.
- Измерьте текущее значение как в лабораторных условиях, так и, по возможности, в реальных условиях использования.
- Проследите весь путь запроса и его обработки, чтобы выявить основную причину затрат.
- Сначала попробуйте внести изменения, сокращающие объём работы: уменьшить количество зависимостей, сузить границы клиента, уменьшить размер данных, ускорить запрос.
- Обратитесь к методам мемоизации, дополнительной кэширования или пользовательской обработки только в том случае, если простого устранения недостатков недостаточно.
- Снова измерьте показатели и оставьте изменение только если полученная выгода оправдывает затраты на его поддержку.
Граница клиента — на листе, не на странице
Преждевременный ход — директива 'use client' на странице: она утаскивает загрузку данных в браузерный бандл. Оставьте страницу серверным компонентом и пометьте только тот компонент, которому нужны события или API браузера.
// 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, хранение компонентов на сервере, упрощение состояния, удаление избыточных запросов, устранение медленных вызовов на бэкенде или удаление оптимизаций, которые стоят дороже, чем приносят пользы.
- Некоторые приложения действительно нуждаются в сложных стратегиях кэширования или отрисовки, но такое решение должно приниматься на основе измерений и четкой диагностики.
- Самым быстрым приложением часто бывает то, которое выполняет наименьшее количество ненужных операций.