Какие ключевые техники оптимизации производительности Frontend вы знаете?

SeniorFrontend инструменты #tooling #performance

Вопрос

Какие основные техники оптимизации производительности frontend-приложения вы используете и как измеряете результат?

Короткий ответ

Основные рычаги — уменьшить объём и стоимость загружаемого/выполняемого кода (code splitting, lazy loading, tree shaking), уменьшить вес медиа (оптимизация изображений) и переиспользовать уже загруженное (кэширование). Результат измеряется не "на глаз", а метриками Core Web Vitals через Lighthouse/RUM.

Подробный ответ

Code splitting и lazy loading. Вместо одного огромного бандла приложение разбивается на чанки по роутам/фичам, которые подгружаются по требованию (import() под капотом сборщика). Пользователь скачивает код только для того экрана, который реально открыл, а не всё приложение целиком. То же самое применимо к тяжёлым библиотекам, нужным не сразу (редакторы, чарты, модалки) — их подгружают динамически по событию.

Tree shaking. Сборщик (Rollup/webpack/esbuild) на основе статического анализа ESM-импортов выкидывает из бандла код, который реально нигде не используется. Работает надёжно только с ESM (import/export) и без побочных эффектов на верхнем уровне модуля — CommonJS (require) статически не анализируется так же хорошо, поэтому "утяжеляет" бандл сильнее.

Оптимизация изображений. Самая частая причина тяжёлых страниц — картинки. Меры: современные форматы (WebP/AVIF вместо PNG/JPEG), srcset/sizes для отдачи нужного разрешения под устройство, loading="lazy" для картинок вне первого экрана, явные width/height (или aspect-ratio) чтобы избежать layout shift при догрузке, и CDN с автоматической ресайз/сжатием "на лету" (Nuxt Image, Cloudinary и т.п.).

Стратегии кэширования. HTTP-кэш на статике с иммутабельными именами файлов (хэш в имени → Cache-Control: max-age=31536000, immutable), Service Worker для offline/повторных визитов, HTTP/2-3 и предзагрузка критичных ресурсов (<link rel="preload">, dns-prefetch, preconnect). На уровне данных — кэширование ответов API (SWR/React Query, Nuxt useAsyncData с ключами).

Измерение. Субъективное ощущение "стало быстрее" не годится — нужны метрики:

  • Lighthouse — синтетический аудит (в devtools или CI), даёт числовую оценку и конкретные рекомендации по каждому пункту.
  • Core Web Vitals — реальные пользовательские метрики Google:
    • LCP (Largest Contentful Paint) — как быстро отрисовался самый крупный видимый элемент.
    • INP (Interaction to Next Paint, сменил FID) — отзывчивость на взаимодействия пользователя.
    • CLS (Cumulative Layout Shift) — насколько сильно "прыгает" вёрстка при загрузке.
  • Важно различать синтетические измерения (Lighthouse, лабораторные условия) и RUM (Real User Monitoring — метрики, собранные у реальных пользователей через web-vitals библиотеку и отправленные в аналитику), потому что реальная сеть/устройства пользователей часто сильно хуже лабораторных.

Пример

// Lazy loading тяжёлого компонента (Vue)
const ChartWidget = defineAsyncComponent(() =>
  import('./ChartWidget.vue')
)
<!-- Оптимизация изображения: формат, размеры, ленивая загрузка -->
<img
  src="/hero-800.webp"
  srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w"
  sizes="(max-width: 600px) 400px, 800px"
  width="800"
  height="450"
  loading="lazy"
  alt="Hero"
/>
// Сбор реальных метрик Core Web Vitals (RUM) и отправка в аналитику
import { onLCP, onINP, onCLS } from 'web-vitals'

function sendToAnalytics(metric) {
  navigator.sendBeacon('/analytics', JSON.stringify(metric))
}

onLCP(sendToAnalytics)
onINP(sendToAnalytics)
onCLS(sendToAnalytics)
# Cache-Control для иммутабельных статических ассетов с хэшем в имени
Cache-Control: public, max-age=31536000, immutable

Дополнительные вопросы

  • Чем INP отличается от устаревшей метрики FID и почему её ввели?
  • Как разница между лабораторными (Lighthouse) и полевыми (RUM) метриками влияет на приоритизацию оптимизаций?
  • Какие типичные причины высокого CLS и как их устранить?