Performance

🏠

Модуль 9: Оптимизация и производительность

Core Web Vitals, SEO, доступность, безопасность — всё что нужно для production-ready сайта.

Core Web Vitals

Что это?

Core Web Vitals — ключевые метрики Google для оценки пользовательского опыта.

Метрика Хорошо Нужна работа Плохо
LCP (Largest Contentful Paint) ≤ 2.5s 2.5s - 4s > 4s
INP (Interaction to Next Paint) ≤ 200ms 200ms - 500ms > 500ms
CLS (Cumulative Layout Shift) ≤ 0.1 0.1 - 0.25 > 0.25

Почему важно?

  • SEO — Core Web Vitals влияют на ранжирование в Google
  • Конверсия — быстрые сайты конвертируют лучше
  • UX — пользователи ожидают мгновенного отклика

LCP (Largest Contentful Paint)

Что измеряет?

Время отрисовки самого большого видимого элемента (обычно hero-изображение или заголовок).

Как оптимизировать

<!-- 1. Preload критичных ресурсов -->
<link rel="preload" as="image" href="hero.webp" fetchpriority="high">
<link rel="preload" as="font" href="font.woff2" type="font/woff2" crossorigin>

<!-- 2. Приоритизация LCP-изображения -->
<img src="hero.webp" 
     fetchpriority="high" 
     loading="eager"
     decoding="async"
     alt="Hero">

<!-- 3. Критический CSS inline -->
<style>
  /* Только стили для above-the-fold */
  .hero { ... }
</style>

<!-- 4. Остальной CSS асинхронно -->
<link rel="preload" as="style" href="../courses.css" onload="this.rel='stylesheet'">

Оптимизация сервера

# Nginx: включить сжатие и кэширование
gzip on;
gzip_types text/html text/css application/javascript image/svg+xml;

# HTTP/2 push
location / {
    http2_push /styles/critical.css;
    http2_push /images/hero.webp;
}

# CDN и кэширование
Cache-Control: public, max-age=31536000, immutable

Чек-лист LCP

  • TTFB (Time to First Byte) < 600ms
  • Критический CSS inline или preload
  • LCP-изображение с fetchpriority="high"
  • Оптимизированные изображения (WebP, AVIF)
  • CDN для статики
  • HTTP/2 или HTTP/3

CLS (Cumulative Layout Shift)

Что измеряет?

Визуальную стабильность — насколько элементы "прыгают" во время загрузки.

Причины CLS

  • Изображения без размеров
  • Динамически добавляемый контент
  • Web fonts (FOUT/FOIT)
  • Реклама и встраиваемые блоки

Как исправить

<!-- 1. ВСЕГДА указывайте размеры изображений -->
<img src="photo.jpg" width="800" height="600" alt="...">

<!-- Или используйте aspect-ratio в CSS -->
<style>
  img { 
    aspect-ratio: 16 / 9;
    width: 100%;
    height: auto;
  }
</style>

<!-- 2. Резервируйте место для динамического контента -->
<div class="ad-slot" style="min-height: 250px;">
  <!-- Реклама загрузится сюда -->
</div>

<!-- 3. Шрифты без FOUT -->
<style>
  @font-face {
    font-family: 'MyFont';
    src: url('font.woff2') format('woff2');
    font-display: swap; /* или optional */
  }
</style>
/* 4. CSS containment для изолированных секций */
.widget {
  contain: layout;
}

/* 5. Скелетон для загрузки */
.skeleton {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

INP (Interaction to Next Paint)

Что измеряет?

Отзывчивость — время от взаимодействия пользователя до визуального отклика.

Как оптимизировать

// 1. Разбивайте долгие задачи
//  Плохо
function processItems(items) {
  items.forEach(item => heavyProcessing(item));
}

//  Хорошо: разбить на чанки
async function processItems(items) {
  for (const item of items) {
    heavyProcessing(item);
    
    // Дать браузеру "дышать"
    await new Promise(r => setTimeout(r, 0));
    // или: await scheduler.yield(); (новый API)
  }
}

// 2. Web Workers для тяжёлых вычислений
const worker = new Worker('heavy-task.js');
worker.postMessage(data);
worker.onmessage = (e) => updateUI(e.data);

// 3. requestIdleCallback для некритичных задач
requestIdleCallback(() => {
  // Аналитика, предзагрузка, несрочные обновления
});
// 4. Debounce и Throttle
const handleScroll = throttle(() => {
  // Обработка скролла
}, 100);

const handleInput = debounce((value) => {
  // Поиск
}, 300);

// 5. Passive event listeners
element.addEventListener('scroll', handler, { passive: true });
element.addEventListener('touchstart', handler, { passive: true });

// 6. CSS вместо JS где возможно
//  JS для анимации
element.style.transform = `translateX(${x}px)`;

//  CSS transition
element.classList.add('moved');

Оптимизация изображений

Современные форматы

Формат Сжатие Поддержка Использование
AVIF Лучшее Chrome, Firefox Фото, сложные изображения
WebP Отличное Все современные Универсальный
JPEG Хорошее Все Fallback для фото
PNG Среднее Все Прозрачность, иконки
SVG Все Иконки, логотипы, графика

Responsive Images

<!-- picture для art direction и форматов -->
<picture>
  <source type="image/avif" 
          srcset="image-400.avif 400w,
                  image-800.avif 800w,
                  image-1200.avif 1200w"
          sizes="(max-width: 600px) 100vw, 50vw">
  <source type="image/webp" 
          srcset="image-400.webp 400w,
                  image-800.webp 800w,
                  image-1200.webp 1200w"
          sizes="(max-width: 600px) 100vw, 50vw">
  <img src="image-800.jpg" 
       srcset="image-400.jpg 400w,
               image-800.jpg 800w,
               image-1200.jpg 1200w"
       sizes="(max-width: 600px) 100vw, 50vw"
       alt="Описание"
       loading="lazy"
       decoding="async"
       width="800"
       height="600">
</picture>

Ленивая загрузка

<!-- Нативная ленивая загрузка -->
<img src="image.jpg" loading="lazy" alt="...">

<!-- LCP изображение — НЕ lazy! -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="...">

Оптимизация шрифтов

Preload и font-display

<!-- Preload критичных шрифтов -->
<link rel="preload" as="font" href="font.woff2" type="font/woff2" crossorigin>

<style>
@font-face {
  font-family: 'Inter';
  src: url('inter.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap; /* Показать fallback, потом заменить */
  /* font-display: optional; — не показывать если не загрузился быстро */
}

/* Размер fallback-шрифта для уменьшения CLS */
@font-face {
  font-family: 'Inter';
  src: url('inter.woff2') format('woff2');
  font-display: swap;
  size-adjust: 100.6%;
  ascent-override: 95%;
  descent-override: 22%;
}
</style>

Subset шрифтов

/* Google Fonts с подмножеством */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap&text=АБВабв0123');

/* Или локально с pyftsubset */
/* pyftsubset font.ttf --text="АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя0123456789" */

JavaScript Performance

Code Splitting

// Динамический импорт
const module = await import('./heavy-module.js');

// React lazy loading
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

// Route-based splitting
const routes = {
  '/': () => import('./pages/Home'),
  '/about': () => import('./pages/About'),
  '/contact': () => import('./pages/Contact')
};

Tree Shaking

//  Импортирует всю библиотеку
import _ from 'lodash';
_.debounce(fn, 300);

//  Импортирует только нужное
import debounce from 'lodash/debounce';
debounce(fn, 300);

//  Или из ES-modules версии
import { debounce } from 'lodash-es';

Defer и Async

<!-- async: загрузка параллельно, выполнение сразу -->
<script async src="analytics.js"></script>

<!-- defer: загрузка параллельно, выполнение после DOM -->
<script defer src="app.js"></script>

<!-- module: автоматически defer -->
<script type="module" src="app.js"></script>

CSS Performance

Critical CSS

<!-- Критический CSS inline -->
<style>
  /* Только above-the-fold стили */
  :root { --primary: #6c63ff; }
  body { margin: 0; font-family: system-ui; }
  .hero { min-height: 100vh; }
</style>

<!-- Остальной CSS асинхронно -->
<link rel="preload" as="style" href="../courses.css" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="../courses.css"></noscript>

CSS Containment

/* contain: изолирует перерисовку */
.card {
  contain: layout style paint;
  /* или */
  contain: content; /* layout + style + paint */
}

/* content-visibility: пропустить рендеринг вне viewport */
.section {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px; /* Примерная высота */
}

Оптимизация селекторов

/*  Медленные селекторы */
div > ul > li > a.link { }
[class*="btn-"] { }
*:not(.active) { }

/*  Быстрые селекторы */
.nav-link { }
.btn-primary { }
.card { }

Кэширование

HTTP кэширование

# Статические ресурсы с хэшем в имени
Cache-Control: public, max-age=31536000, immutable

# HTML страницы
Cache-Control: no-cache, must-revalidate

# API ответы
Cache-Control: private, max-age=300

# ETag для валидации
ETag: "abc123"
If-None-Match: "abc123"  # → 304 Not Modified

Service Worker

// sw.js
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = ['/app.js', '/styles.css', '/offline.html'];

self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(STATIC_ASSETS))
  );
});

self.addEventListener('fetch', (e) => {
  e.respondWith(
    caches.match(e.request)
      .then(cached => cached || fetch(e.request))
      .catch(() => caches.match('/offline.html'))
  );
});

SEO оптимизация

Мета-теги

<head>
  <title>Заголовок страницы — Бренд</title>
  <meta name="description" content="Описание до 160 символов">
  <meta name="robots" content="index, follow">
  <link rel="canonical" href="https://example.com/page">
  
  <!-- Open Graph -->
  <meta property="og:title" content="Заголовок">
  <meta property="og:description" content="Описание">
  <meta property="og:image" content="https://example.com/image.jpg">
  <meta property="og:url" content="https://example.com/page">
  <meta property="og:type" content="website">
  
  <!-- Twitter Card -->
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:title" content="Заголовок">
  <meta name="twitter:description" content="Описание">
  <meta name="twitter:image" content="https://example.com/image.jpg">
</head>

Structured Data

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "Заголовок статьи",
  "author": {"@type": "Person", "name": "Автор"},
  "datePublished": "2025-01-30",
  "image": "https://example.com/image.jpg"
}
</script>

Sitemap и robots.txt

<!-- sitemap.xml -->
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/</loc>
    <lastmod>2025-01-30</lastmod>
    <changefreq>weekly</changefreq>
    <priority>1.0</priority>
  </url>
</urlset>
# robots.txt
User-agent: *
Allow: /
Disallow: /admin/
Disallow: /api/

Sitemap: https://example.com/sitemap.xml

Доступность (a11y)

Основные принципы

<!-- 1. Семантический HTML -->
<header>...</header>
<nav>...</nav>
<main>...</main>
<article>...</article>
<footer>...</footer>

<!-- 2. Иерархия заголовков -->
<h1>Главный заголовок</h1>
<h2>Подзаголовок</h2>
<h3>Подподзаголовок</h3>

<!-- 3. Альтернативный текст -->
<img src="chart.png" alt="График роста продаж: +25% за год">
<img src="decoration.png" alt="" role="presentation">

<!-- 4. Формы с label -->
<label for="email">Email</label>
<input type="email" id="email" required aria-describedby="email-hint">
<span id="email-hint">Мы не будем спамить</span>

<!-- 5. Кнопки и ссылки -->
<button aria-label="Закрыть">✕</button>
<a href="/page">Читать далее о продукте X</a> <!-- НЕ "Читать далее" -->

ARIA

<!-- Роли -->
<div role="alert">Ошибка сохранения!</div>
<div role="dialog" aria-modal="true">...</div>

<!-- Состояния -->
<button aria-expanded="false" aria-controls="menu">Меню</button>
<nav id="menu" aria-hidden="true">...</nav>

<!-- Live regions -->
<div aria-live="polite">Загрузка завершена</div>
<div aria-live="assertive">Ошибка!</div>

Фокус и клавиатура

/* Видимый фокус */
:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* Skip link */
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
}

.skip-link:focus {
  top: 0;
}

Безопасность

HTTP заголовки

# Content Security Policy
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;

# Защита от кликджекинга
X-Frame-Options: DENY

# XSS защита
X-Content-Type-Options: nosniff

# HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

# Referrer
Referrer-Policy: strict-origin-when-cross-origin

XSS защита

//  Опасно
element.innerHTML = userInput;

//  Безопасно
element.textContent = userInput;

//  Санитизация
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);

Инструменты

Анализ производительности

  • Lighthouse — встроен в DevTools
  • PageSpeed Insights — реальные данные
  • WebPageTest — детальный анализ
  • Chrome DevTools Performance — профилирование

Бандл анализ

  • webpack-bundle-analyzer
  • source-map-explorer
  • bundlephobia.com — размер npm пакетов

Доступность

  • axe DevTools
  • WAVE
  • Lighthouse Accessibility

Практика

Проведите полный аудит любого сайта:

  • Lighthouse: Performance, Accessibility, SEO, Best Practices
  • Core Web Vitals через PageSpeed Insights
  • Составьте план оптимизации

Настройки