Как ускорить Core Web Vitals на Astro
Astro изначально хорош в производительности: статические страницы, минимум JS, серверный рендер по умолчанию. Но «хорошо» и «отлично» — разные вещи. Я недавно делал аудит контентного сайта на Astro 5: исходно LCP 2.4 с, INP 280 мс, CLS 0.18. После двух недель работы — LCP 1.1 с, INP 90 мс, CLS 0.02. Расскажу, что и в каком порядке менял.
Базовые метрики Core Web Vitals
Прежде чем оптимизировать, договоримся о словаре:
- LCP (Largest Contentful Paint) — время до отрисовки самого большого видимого элемента. Цель: ≤2.5 с.
- INP (Interaction to Next Paint) — время отклика на пользовательское взаимодействие. Цель: ≤200 мс.
- CLS (Cumulative Layout Shift) — насколько прыгает контент при загрузке. Цель: ≤0.1.
Меряем в реальной аудитории через Search Console (Core Web Vitals report) или CrUX dataset. Лабораторные замеры (Lighthouse, PageSpeed) полезны для отладки, но не отражают реальные цифры.
LCP: первая большая работа
На том проекте LCP-элементом была обложка статьи — изображение 1200×600 пикселей. Что я сделал:
1. Перевести на современные форматы
---
import { Image } from 'astro:assets';
import cover from '../assets/cover.jpg';
---
<Image
src={cover}
alt="Обложка"
widths={[400, 800, 1200]}
sizes="(max-width: 800px) 100vw, 800px"
formats={['avif', 'webp']}
loading="eager"
fetchpriority="high"
/>Astro Image сам сгенерирует AVIF и WebP, fallback на JPG. На моих данных AVIF дал на 35-40% меньший вес по сравнению с WebP. Атрибут fetchpriority="high" заставляет браузер загружать обложку с высоким приоритетом — ускоряет LCP на 200-400 мс.
2. Preload критичных ресурсов
Шрифт, который используется в LCP-элементе, надо предзагрузить:
<link
rel="preload"
href="/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossorigin
/>Если шрифт недоступен, браузер сначала рисует системный, потом перерисовывает в кастомный — это и LCP, и CLS.
3. Inline критичный CSS
Astro по умолчанию инлайнит критичный CSS, но если у тебя есть глобальные стили из @import в node_modules — они часто грузятся отдельным файлом. Проверь:
pnpm astro build
ls -la dist/_astro/*.cssЕсли файлов CSS больше двух — посмотри, что туда попадает. На моём проекте я выкинул один CSS-фреймворк, который тащил 80 КБ ради двух классов.
INP: основа — меньше JS
INP меряет, насколько быстро страница откликается на действие пользователя. На Astro это редко проблема, но бывает.
1. Убрать тяжёлые острова
На том проекте было два client:load компонента в шапке — поиск и переключатель темы. Каждый тащил ~30 КБ JS, и на медленных устройствах гидрация съедала первые 200 мс.
Заменил на client:idle для поиска и client:only="react" для темы (идея: тема нужна только когда пользователь её жмёт, до этого работает CSS). INP упал с 280 до 130 мс.
2. Long tasks
Если у тебя в js обработчик клика делает много синхронной работы (фильтрация большого списка, форматирование), браузер не успевает отрисовать ответ за 200 мс.
Решение — requestAnimationFrame для разбиения работы или scheduler.yield():
async function handleClick() {
setLoading(true);
await scheduler.yield();
const result = heavyFilter(items, filter);
setItems(result);
setLoading(false);
}Атрибут scheduler.yield() — нативный API в современных браузерах, который отдаёт управление в браузер, чтобы тот успел отрисовать.
CLS: невидимая, но злая
CLS — самая обидная метрика. Контент уже на экране, но прыгает, когда подгружается баннер или картинка.
1. Размеры изображений
Каждый <img> и <Image> должен иметь явные width и height или aspect-ratio. Astro Image ставит размеры автоматически — это работает.
Для внешних URL пиши руками:
<img src="https://cdn.example.com/photo.jpg" alt="..." width="800" height="600" loading="lazy" />2. Шрифты с font-display: swap
По умолчанию это даёт CLS — пока шрифт не загрузился, браузер показывает fallback. Когда подменяет — текст «прыгает».
Решение: font-display: optional + правильный fallback с похожими метриками. Или size-adjust в @font-face:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap;
size-adjust: 105%;
ascent-override: 90%;
}Эти overrides точно подгоняют fallback к реальному шрифту, и подмена не вызывает прыжка. Цифры подбираются через инструменты типа font-style-matcher.
3. Резервируй место для динамики
Если у тебя баннер-cookie, который появляется через 200 мс — он сдвинет всю страницу. Резервируй место заранее или показывай через overlay (position: fixed).
Server timing и кеш
Если у тебя SSR — обращай внимание на TTFB (Time To First Byte). LCP не может быть хорошим, если сервер отвечает за секунду.
// astro.config.mjs
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
vite: {
server: {
headers: { 'Server-Timing': 'cache;dur=12' },
},
},
});На уровне Nginx или CDN кешируй HTML страниц на минуту-две. Это ускоряет TTFB до сотен миллисекунд.
View Transitions и LCP
Если используешь Astro View Transitions, у тебя SPA-навигация. Проверь, что переход не блокирует LCP — иногда плавный fade на 300 мс задерживает отрисовку. Лечится более коротким durations или явным prefetch.
Третьи стороны
Самый частый виновник плохих метрик — сторонние скрипты: аналитика, чаты, баннеры. На том проекте Hotjar тащил 280 КБ и съедал 80 мс на обработку.
Стратегии:
- Загружать через
partytownв отдельном worker'е. - Откладывать через
deferили загружать после первого взаимодействия. - Удалять то, чем реально не пользуются.
Чек-лист, который я применяю
- Открыть DevTools → Performance → запись типичного сценария.
- Записать LCP, INP, CLS в реальной аудитории через Search Console.
- Проверить размеры всех
<img>и<Image>. - Подключить шрифты через preload + size-adjust.
- Минимизировать
client:loadкомпоненты. - Включить кеш на уровне CDN или сервера.
- Аудит сторонних скриптов.
- Прогнать Lighthouse и сравнить.
Что копать дальше
Web Vitals — это не «один раз настроил и забыл». На контентных сайтах метрики ползут вверх с каждой новой статьёй (больше изображений, больше CSS, больше скриптов). Я раз в три месяца делаю мини-аудит и фиксаю регрессии. Это занимает 2-4 часа и держит сайт в зелёной зоне.