Модуль 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
- Составьте план оптимизации