Производительность фронтенда: от слепых зон при ревью кода до показателей продукта
Узнайте, почему одного прохождения проверки кода недостаточно, какие именно показатели Core Web Vitals имеют значение, а также как измерять и устранять проблемы с производительностью React в реальных условиях.
Код прошел проверку
Ваш запрос на объединение кода без проблем. Логика работает корректно. Все тесты пройдены. Его одобрил старший инженер.
Вы выпускаете изменения в четверг днем.
Утром в пятницу приходит сообщение от менеджера продукта: «Люди говорят, что приложение работает медленно».
Вы запускаете Chrome DevTools. На вашем MacBook Pro с домашним оптоволоконным подключением, где Chrome работает без десятка расширений, всё выглядит плавно.
Но у ваших пользователей другая конфигурация.
Многие из них используют смартфоны среднего класса на Android, выпущенные несколько лет назад, с подключением 4G, которое часто переходит на 3G. Они могут находиться в Джакарте, Лагосе или Баку — местах, где одна лишь задержка сети может добавлять от 200 до 400 миллисекунд к каждому запросу.
Ваше приложение заставляет их сидеть и ждать.
Причины этого разрыва
Большинство разработчиков фронтенда работают в почти идеальных условиях, а затем выпускают продукт в гораздо более хаотичной среде. Именно в этом несоответствии и закладываются проблемы с производительностью.
Вот что ваша повседневная среда разработки скрывает от вас:
Ограничение скорости работы CPU. У вашего разработочного компьютера достаточно вычислительных мощностей. Инструменты Chrome DevTools позволяют имитировать замедление работы CPU в 4 или 6 раз, но почти никто не тратит время на его включение.
Условия сети. Тестирование на localhost означает отсутствие задержек. Настоящие пользователи сталкиваются с временем передачи данных от 100 до 500 миллисекунд. Загрузка данных, которая кажется мгновенной на вашем компьютере, может заморозить интерфейс на целую секунду после его запуска.
Размер пакета. При написании кода добавление библиотек кажется безобидным — видимых затрат нет. Однако в реальных условиях та же зависимость может увеличить размер исходного пакета на 80 КБ, и пользователь с соединением 3G вынужден загрузить всё это прежде, чем что-либо отобразится.
Время парсинга JavaScript. Загрузка пакета на устройство — это лишь первый шаг. Затем браузер должен его проанализировать и запустить. На более слабом оборудовании один только парсинг пакета размером 500 КБ может занять от 3 до 4 секунд.
Все эти факторы приводят к реальному разрыву между тем, как ваше приложение кажется вам, и тем, как оно воспринимается людьми, которые им фактически пользуются — и этот разрыв обычно остается незаметным до тех пор, пока жалобы не вынудят его проявиться.
Почему производительность — это решение, связанное с продуктом
Инженеры фронтенда часто относят вопросы производительности к «техническим деталям». Менеджеры продуктов обычно полностью игнорируют их, пока ситуация не превращается в чрезвычайную.
Ни один из этих подходов не является эффективным.
Вопросы производительности должны обсуждаться в рамках разработки продукта, поскольку они влияют на результаты, которые компания действительно отслеживает.
Доходы. Amazon сообщает, что каждые дополнительные 100 миллисекунд задержки стоят их примерно 1% от оборота. При доходе в миллиард долларов в день это составляет 10 миллионов долларов за каждые 100 мс. У более мелких компаний абсолютные цифры меньше, но закономерность остаётся прежней.
Удержание пользователей. Более половины посетителей с мобильных устройств — 53% — покидают страницу, загружающуюся более чем за 3 секунды. Они редко подают жалобы; они просто уходят и больше не возвращаются.
SEO. С 2021 года Google учитывает показатели Core Web Vitals в своем алгоритме ранжирования. Медленная работа сайта приводит к тому, что он опускается ниже в результатах поиска, из-за чего его находят меньше пользователей.
Доступность. Скорость также является вопросом равенства. Люди, использующие устаревшие устройства и медленные соединения, в основном проживают в странах с развивающейся экономикой и относятся к группам с низким доходом. Медленное приложение фактически исключает часть вашей аудитории.
Как только вы сосредоточите внимание на доходах, удержании пользователей, видимости в поиске и доступности, производительность перестает казаться необязательной и становится основным требованием.
Показатели, которые действительно важны
Невозможно исправить то, что не измеряется, и невозможно измерить то, что не определено. Именно поэтому общий словарный запас для оценки производительности становится крайне важным.
Core Web Vitals от Google в настоящее время представляют собой самую надежную систему для оценки этого показателя.
LCP — время отрисовки крупнейшего содержимого
Этот показатель отслеживает время, необходимое для отрисовки крупнейшего видимого элемента на странице — по сути, момент, когда пользователь ощущает, что страница «загрузилась».
Хорошо: менее 2,5 секунд. Требует улучшения: от 2,5 до 4 секунд. Плохо: более 4 секунд.
Типичные причины: слишком крупные или неоптимизированные изображения, ресурсы, мешающие отрисовке, и медленные ответы сервера.
INP — время от реакции пользователя до следующей отрисовки
Этот показатель измеряет задержку между действием пользователя — кликом, нажатием или набором символов — и моментом визуальной реакции экрана. В 2024 году он заменил FID (First Input Delay) в качестве стандартного показателя интерактивности.
Хорошо: менее 200 мс. Требует улучшения: от 200 до 500 мс. Плохо: более 500 мс.
Типичные причины: интенсивные вычисления на основном потоке и синхронная работа, блокирующая отрисовку.
CLS — накопленное смещение макета
Этот показатель отражает степень неожиданного смещения контента во время загрузки страницы. Значения идут от 0 (нет смещений) вверх, причем значения выше 1 считаются серьезными.
Хорошо: менее 0,1. Требует улучшения: от 0,1 до 0,25. Плохо: более 0,25.
Типичные причины: отсутствие у изображений явно указанных ширины и высоты, динамическое вставление контента после загрузки и использование веб-шрифтов, загружаемых поздно.
Как измерять: ваши инструменты
Lighthouse (начните с этого)
Откройте Chrome DevTools, перейдите на вкладку Lighthouse и запустите аудит для мобильного профиля с включенным ограничением скорости работы.
Lighthouse возвращает оценку от 0 до 100 по критериям производительности, доступности, SEO и наилучших практик. Его особая ценность заключается в том, что он объясняет, почему получена именно такая оценка, и указывает, что следует исправить в первую очередь.
# Or run it from the CLI for CI/CD integration
npm install -g lighthouse
lighthouse https://yourapp.com --output html --output-path report.html
Важно: каждый раз запускайте Lighthouse в режиме инкогнито. Установленные расширения браузера могут исказить результаты.
React DevTools Profiler
Это, пожалуй, самый малоиспользуемый инструмент среди разработчиков React, несмотря на то что он один из самых информативных.
Чтобы им воспользоваться, откройте React DevTools, перейдите на вкладку Profiler, нажмите «Записать», взаимодействуйте с приложением, а затем остановите запись.
Результатом является график пламени, отображающий каждую процедуру отрисовки — какие компоненты были активированы, что их запустило и сколько времени заняла каждая из них.
What to look for:
- Components rendering more than they should
- Renders triggered by unrelated state changes
- Expensive components re-rendering on every keystroke
Библиотека Web Vitals
Если вы хотите оценить производительность вашего приложения у реальных пользователей, а не в локальной среде DevTools, для этого подходит библиотека web-vitals:
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(metric => {
// Send to your analytics service
console.log('LCP:', metric.value);
});onINP(metric => {
console.log('INP:', metric.value);
});onCLS(metric => {
console.log('CLS:', metric.value);
});
Такой подход позволяет получить данные, собранные в реальных сессиях пользователей, а не цифры, полученные в искусственных лабораторных условиях.
Повторная отрисовка, о которой вы не знали
Одна из самых коварных проблем с производительностью React не проявляется явно. Обычно она выглядит так:
// ❌ Problem: selecting the full user object
function Header() {
const user = useSelector(state => state.user);
return <div>{user.name}</div>;
}
Этот компонент будет перерисовываться каждый раз, когда меняется любое свойство внутри state.user, независимо от того, читает ли компонент это свойство на самом деле. Если объект user содержит двадцать полей, и пять из них часто меняются, компонент Header будет перерисовываться в пять раз чаще, чем необходимо.
// ✅ Fix: select only what you need
function Header() {
const name = useSelector(state => state.user.name);
return <div>{name}</div>;
}
Благодаря этой изменении компонент Header реагирует только на изменения в свойстве name. Это простая правка, но её влияние на производительность реально. Та же логика применима и к Context:
// ❌ Problem: consuming the full context
function ThemeButton() {
const { theme, user, notifications } = useAppContext();
return <button className={theme}>Click</button>;
}
// ✅ Fix: split contexts by update frequency
const ThemeContext = createContext();
const UserContext = createContext();function ThemeButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}
Задержанная загрузка: прекратите отправку кода, который не нужен пользователям
Частой ошибкой в проектах на React является загрузка всего пакета приложения сразу при первом открытии страницы — включая код маршрутов, которые посетитель ещё не открывал и, возможно, никогда не откроет.
// ❌ Problem: all routes loaded upfront
import CheckoutPage from './pages/CheckoutPage';
import AdminDashboard from './pages/AdminDashboard';
import SettingsPage from './pages/SettingsPage';
function App() {
return (
<Routes>
<Route path="/checkout" element={<CheckoutPage />} />
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
);
}
// ✅ Fix: lazy load each route
import { lazy, Suspense } from 'react';
const CheckoutPage = lazy(() => import('./pages/CheckoutPage'));
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/checkout" element={<CheckoutPage />} />
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Suspense>
);
}
Благодаря такой конфигурации каждый маршрут становится отдельным блоком, поэтому пользователи загружают только тот код, который необходим для страницы, которую они фактически просматривают. В более крупных приложениях одна лишь эта мера может сократить размер начального пакета на 40–60 процентов.
Когда НЕ стоит оптимизировать: ловушка useMemo
Большинство руководств по производительности пропускают этот момент: чрезмерная оптимизация на раннем этапе может негативно повлиять на код.
useMemo и useCallback имеют свои издержки — они требуют выделения памяти и сравнения зависимостей при каждой отрисовке. Если использовать их не там, где нужно, скорость может стать даже ниже, чем раньше.
// ❌ Unnecessary — this calculation is not expensive
function UserCard({ user }) {
const displayName = useMemo(
() => `${user.firstName} ${user.lastName}`,
[user.firstName, user.lastName]
);
return <div>{displayName}</div>;
}
// ✅ Just compute it — string concatenation is instant
function UserCard({ user }) {
const displayName = `${user.firstName} ${user.lastName}`;
return <div>{displayName}</div>;
}
// ✅ useMemo IS worth it here — genuinely expensive calculation
function DataGrid({ rows, filters }) {
const filteredRows = useMemo(
() => rows.filter(row => matchesAllFilters(row, filters)),
[rows, filters]
);
return <Table rows={filteredRows} />;
}
Основной принцип: сначала проведите профилирование, затем только оптимизируйте, а после этого снова измерьте производительность, чтобы убедиться, что исправление помогло. Не полагайтесь на интуицию.
Если инструмент профилирования никогда не отмечает тот или иной компонент как узкое место, оставьте его без кэширования — добавляемая сложность того не стоит.
Оптимизация изображений: самые простые решения
Изображения часто являются основной причиной медленной загрузки страниц, но в то же время это одни из самых простых проблем для решения.
// ❌ Unoptimized: full-size image, no lazy loading
<img src="/hero-image.png" />
// ✅ Optimized: modern format, explicit dimensions, lazy loading
<img
src="/hero-image.webp"
width={1200}
height={600}
loading="lazy"
decoding="async"
alt="Hero image"
/>
Несколько привычек могут существенно повлиять на результат:
Замените форматы PNG или JPEG на WebP или AVIF. WebP обычно сокращает размер файла на 25–35 процентов по сравнению с JPEG при сохранении подобного качества. AVIF компрессирует ещё лучше, хотя поддержка браузерами пока не настолько широкая.
Всегда указывайте атрибуты ширины и высоты явно. Это предотвращает изменение макета браузером после завершения загрузки изображения, что напрямую способствует улучшению показателя CLS.
Применяйте loading="lazy" ко всему, что находится ниже области просмотра. Тогда браузер будет ждать с загрузкой этого изображения до тех пор, пока пользователь не начнет прокручивать страницу, чтобы увидеть его.
Добавьте decoding="async" к изображениям, которые не являются критически важными для первоначального отображения. Это позволяет браузеру декодировать изображение в отдельном потоке, вместо того чтобы блокировать процесс отрисовки.
Реальные компромиссы
Ни одна техника оптимизации производительности не является бесплатной. Вот честный разбор того, что вы приходится жертвовать:
Разделение кода по маршрутам сокращает размер начального пакета, но вызывает небольшую задержку при первом переходе пользователя на новый маршрут. Задержанная загрузка изображений ускоряет первоначальную загрузку страницы, но приводит к тому, что изображения появляются только при прокрутке. Использование функции useMemo для выполнения ресурсоемких вычислений сокращает количество перерисовок, но увеличивает сложность кода и затрудняет его восприятие. Кэширование с помощью Service Worker делает повторные посещения практически мгновенными, но требует сложной логики аннулирования кэша. Отрисовка на стороне сервера или генерация статического контента обеспечивают быструю отрисовку страницы и улучшают SEO, однако они требуют более мощной серверной инфраструктуры и усложняют процесс активации интерактивности.
Основной принцип, лежащий в основе всего этого: никогда не оптимизируйте что-либо, если вы еще не измерили соответствующий показатель.
Оценка Lighthouse в 95 ничего не говорит о том, хороший ли опыт получают реальные пользователи. Оснастите свое приложение библиотекой Web Vitals, соберите данные от фактических посетителей, выявьте настоящий узкий место, устраните именно его, а затем снова измерьте результаты, чтобы убедиться в эффективности решения.
Практический чек-лист
Прежде чем выпустить какую-либо значимую функцию, пройдитесь по этому списку:
Performance Checklist
─────────────────────
□ Run Lighthouse on mobile with throttling (target score: 90+)
□ Check bundle size with webpack-bundle-analyzer or source-map-explorer
□ Verify all routes are lazy loaded
□ Confirm images use WebP/AVIF with explicit dimensions
□ Profile with React DevTools — no unnecessary re-renders
□ Check Core Web Vitals in production with web-vitals library
□ Test on a real mobile device, not just DevTools emulation
# Install bundle analyzer
npm install --save-dev webpack-bundle-analyzer
# Or for Vite
npm install --save-dev rollup-plugin-visualize
Заключение
Работа над производительностью — это не последняя спешка перед запуском, и это не дополнительный слой, который добавляют, когда функция уже работает. Это дисциплина — набор привычек и инструментов, встроенных в ваш ежедневный рабочий процесс.
Инженеры, создающие самые быстрые приложения, не обязательно талантливее тех, кто создаёт медленные. Они просто более последовательны в измерениях. Они знают, какой инструмент подходит для решения той или иной проблемы, и внедрили в себя простой цикл: сначала проводят анализ, исправляют только то, на что указывают данные, затем снова измеряют, чтобы убедиться, что это помогло.
Приложение может пройти все проверки кода, но всё равно разочаровать настоящих пользователей.
Измеряйте. Анализируйте. Исправляйте то, что действительно важно.
Связанные статьи
- Сокращение размера React-компонентов с использованием подхода Prop-Drilling и God Components — Узнайте семь конкретных шаблонов рефакторинга для разбиения объёмных React-компонентов путем изоляции состояния, загрузки данных, разрешений и логики загрузки, а не простого разделения файлов.