Среднее время удержания пользователя на сайте сократилось до 15 секунд. Каждые 100 мс задержки снижают конверсию на 7%. В такой реальности оптимизация производительности становится не «желательным улучшением», а вопросом выживания продукта. Рассмотрим практические техники, которые дадут измеримый результат, а не просто пополнят коллекцию оптимизационных флагов в Webpack.
От диагноза к лечению: точные метрики вместо догадок
Прежде чем оптимизировать, нужен количественный анализ. Lighthouse
в Chrome DevTools — отправная точка, но не истина в последней инстанции. Для полной картины замеряем:
- Core Web Vitals: LCP, FID, CLS
- Вес страницы: сравнение с рекомендованными 1.5 МБ для мобильных
- Время до первого байта (TTFB) сервера
Пример анализа через web-vitals
:
import {getLCP, getFID, getCLS} from 'web-vitals';
getLCP(console.log);
getFID(console.log);
getCLS(console.log);
Но синтетические тесты в DevTools часто врут. Добавьте полевые данные через:
# Real User Monitoring (RUM) через Google Analytics
npm install @google-analytics/web-vitals
Старайтесь собирать 75-й перцентиль по реальным пользователям — именно эти цифры влияют на ранжирование в Google.
Реальное сжатие ресурсов: не очевидные решения
Изображения: не просто image-webpack-loader
Форматы AVIF и WebP покрывают 85% браузеров, но их генерация требует правильной настройки:
# .htaccess для Apache
<IfModule mod_setenvif.c>
SetEnvIf Accept "image/avif" avif
SetEnvIf Accept "image/webp" webp
</IfModule>
<FilesMatch ".(jpg|jpeg|png|gif)$">
RewriteCond %{ENV:avif} =1
RewriteCond %{REQUEST_FILENAME}.avif -f
RewriteRule ^(.+).(jpe?g|png|gif)$ $1.$2.avif [T=image/avif]
RewriteCond %{ENV:webp} =1
RewriteCond %{REQUEST_FILENAME}.webp -f
RewriteRule ^(.+).(jpe?g|png|gif)$ $1.$2.webp [T=image/webp]
</FilesMatch>
Для фоновых изображений используйте CSS Gradients + SVG-фильтры вместо растровых файлов. Однажды заменив фоновый JPEG 500 КБ на 2 КБ CSS, мы сократили LCP с 4.2s до 1.1s.
JavaScript: атомарный подход к бандлам
Webpack SplitChunks — основа, но добавьте:
// webpack.config.js
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-debounce)[\\/]/,
chunks: 'all',
}
}
}
}
Группируйте не по размеру, а по частоте обновления. Библиотеки типа react
меняются реже вашего кода — их нужно изолировать. Разделение на app.js
и vendor.js
устарело: создайте чанки для функциональных модулей (авторизация, каталог товаров).
Весит меньше ≠ работает быстрее. После сжатия brotli 200 КБ и 20 КБ кода могут выполняться с одинаковой скоростью — иногда лучше отказаться от разделения ради снижения числа запросов.
Ленивость как добродетель: стратегии загрузки
Скелетные компоненты для динамических импортов
Старый подход:
const ProductModal = lazy(() => import('./ProductModal'));
При ленивой загрузке модального окна пользователь видит «провал» на 200-500 мс. Решение — скелетные плейсхолдеры с такой же layout структурой:
<Suspense fallback={
<div style={{width: '300px', height: '400px', backgroundColor: '#f0f0f0'}} />
}>
<ProductModal />
</Suspense>
Скелеты снижают визуальный CLS на 40-60%, что критично для SEO.
Предзагрузка для критического пути
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
<link rel="prefetch" href="/search-results-chart.js" as="script">
Но предзагрузка ведёт к конкуренции за bandwidth. Используйте Priority Hints:
<img src="hero.jpg" importance="high">
<script src="analytics.js" importance="low">
Приоритезация вдвое сокращает время рендеринга надъядерного контента.
Кэширование: когда persistence вредит
Service Worker — не панацея. При неправильной стратегии пользователи получают битые версии месяцами. Паттерн «Cache, falling back to network» опасен. Используйте:
// sw.js
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('v1').then(cache =>
fetch(event.request)
.then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
})
.catch(() => caches.match(event.request))
)
);
});
Эта «сетевая с откатом на кэш» стратегия гарантирует свежесть данных. Для статики — версионирование URL (main.abcd123.js
), чтобы кэш инвалидировался автоматически.
Серверные заголовки требуют тонкой настройки:
Cache-Control: public, max-age=31536000, immutable
Для API-ответов, где PUT
или DELETE
могут менять состояние:
Cache-Control: no-cache, max-age=0, must-revalidate
Но no-cache
всё равно отправляет запрос на валидацию. Если данные обновляются реже раза в час, ставьте max-age=3600, stale-while-revalidate=86400
— браузер покажет кэш, но фоново обновит.
Производительность как процесс, а не пункт в чек-листе
Разовые оптимизации эффектны, но недолговечны. Внедрите в процесс:
- Budgets в Lighthouse CI: ограничения на размеры JS/CSS
- Визуальную регрессионную систему (Chromatic, Percy)
- Автоматическое алертинг при деградации RUM-метрик
Производительность — это не борьба с килобайтами. Это проектирование архитектуры, где каждый компонент рендерится минимально необходимыми ресурсами. Иногда удаление 20 строк кода даёт больший эффект, чем введение сложной алгоритмической оптимизации. Тестируйте, измеряйте, и пусть ваши показатели FID всегда будут ниже 100 мс.