Шрифты — наверное, самая недооценённая причина плохих Core Web Vitals. По данным HTTP Archive за начало 2026 года, средняя страница тянет 3–5 шрифтовых файлов общим весом 200–400 КБ, и именно они частенько блокируют первый рендер, ломают LCP и аккуратно подкладывают CLS при подмене fallback на веб-шрифт. Хорошая новость: за последние пару лет экосистема, по сути, получила всё, чтобы закрыть эту тему окончательно — variable fonts с прогрессивной загрузкой, font-display, динамический subsetting через unicode-range, size-adjust, метрики совпадения и 103 Early Hints для preload. Дальше — как собрать всё это в одну работающую стратегию.
Если коротко: к 2026 году тема «шрифты тормозят сайт» — это уже не про браузеры, а про дисциплину фронтенд-команды.
Почему шрифты ломают Core Web Vitals
Прежде чем браться за инструменты, полезно понять, через какие именно механики шрифты гадят метрикам. Их три, и они в некотором смысле бьют по разным фронтам.
1. FOIT блокирует LCP
Если LCP-элементом оказался текстовый блок (заголовок, абзац — что угодно), браузер просто не отрисует его, пока веб-шрифт не загрузится. При условии, конечно, что font-display не настроен явно. Поведение по умолчанию — блокировка до 3 секунд (в Chromium), и это напрямую раздувает LCP. По данным Chrome UX Report за Q1 2026, около 18% сайтов имеют текстовый LCP-кандидат, и для большинства из них шрифт — главный виновник.
2. Подмена шрифта вызывает CLS
Браузер сначала показывает fallback (скажем, Arial), а потом подменяет его на загруженный (Inter). У новых букв другая ширина, другая высота строки, другой x-height. Текст перетекает, блоки ниже сдвигаются — и вот он, классический font-induced CLS. Долгое время единственным «решением» был visibility: hidden, но это, по сути, тот же FOIT, только в профиль.
3. Шрифты конкурируют с критическими ресурсами
Браузер замечает шрифт только после того, как разберёт CSS и наткнётся на элемент, который его использует. На практике это означает задержку 200–800 мс относительно начала навигации — и шрифт встаёт в очередь рядом с JS-бандлом и LCP-картинкой. А там, как правило, кто первый встал — того и тапки.
Variable fonts: одно решение вместо четырёх запросов
В 2026 году поддержка variable fonts на устройствах пользователей перевалила за 97%, и продолжать раздавать отдельные файлы под Regular/Medium/Bold/Italic — это, честно говоря, плохая инженерия. Один variable-шрифт обычно весит на 30–60% меньше суммы четырёх статических начертаний и заодно убирает проблему «нужное начертание ещё не докачалось — текст рендерится не тем весом».
/* Плохо: четыре отдельных файла */
@font-face {
font-family: "Inter";
font-weight: 400;
src: url("/fonts/Inter-Regular.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-weight: 500;
src: url("/fonts/Inter-Medium.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-weight: 700;
src: url("/fonts/Inter-Bold.woff2") format("woff2");
}
/* Хорошо: один variable-файл с диапазоном весов */
@font-face {
font-family: "Inter";
font-weight: 100 900;
font-style: normal;
font-display: swap;
src: url("/fonts/InterVariable.woff2") format("woff2-variations");
}
Variable-версии популярных гарнитур уже есть для всего, что вам, скорее всего, понадобится: Inter Variable, Roboto Flex, Roboto Serif, Source Sans 3 Variable, Recursive, JetBrains Mono Variable. Всё это доступно и в Google Fonts, и через npm-пакеты @fontsource-variable/<имя>.
font-display: выбираем стратегию под метрику
Свойство font-display описывает, что браузер делает в три фазы загрузки: block (текст невидим), swap (виден fallback) и failure (fallback навсегда). Значений всего пять — и выбор напрямую решает, кому жить, LCP или CLS.
swap— fallback показывается сразу, потом подменяется. Хорошо для LCP, плохо для CLS. Самое то, когда LCP — текст.optional— браузер ждёт ~100 мс. Не успел — fallback на всю сессию. Идеален для CLS и для шрифтов «приятно иметь, но без них жить можно».fallback— короткий block (~100 мс), потом ограниченный по времени swap. Такой себе компромисс.block— почти никогда не нужно. Это тот самый FOIT до трёх секунд.auto— поведение по умолчанию, в Chromium равноblock. Просто избегайте.
Рекомендация Chrome team на 2026 год выглядит так: swap для основного текста (CLS снимаем через size-adjust, об этом ниже) и optional для декоративных шрифтов. По моему опыту, это разумный дефолт практически на любом контентном проекте.
size-adjust: убираем CLS без FOIT
Пожалуй, главный сдвиг последних лет — это возможность подогнать fallback-шрифт под метрики веб-шрифта так, чтобы подмена не таскала текст. Делается через четыре дескриптора в @font-face: size-adjust, ascent-override, descent-override и line-gap-override.
Подбирать значения руками — занятие сомнительное (поверьте, я пробовал). Есть Capsize и его свежая обёртка fontaine (2026), которые автоматически считают совпадающие метрики:
// vite.config.js — автоматическая генерация fallback-метрик
import { defineConfig } from "vite";
import { FontaineTransform } from "fontaine";
export default defineConfig({
plugins: [
FontaineTransform.vite({
fallbacks: ["Arial", "system-ui"],
resolvePath: (id) => new URL("./public" + id, import.meta.url),
}),
],
});
После сборки в CSS появится дополнительный @font-face для «fallback Inter», где Arial подкручен так, чтобы абзацы занимали ровно тот же размер, что и в настоящем Inter. CLS от подмены шрифта в итоге — 0.000. Серьёзно, ноль.
Любите всё руками — пожалуйста, вот синтаксис:
@font-face {
font-family: "Inter Fallback";
src: local("Arial");
size-adjust: 107.4%;
ascent-override: 90%;
descent-override: 22.43%;
line-gap-override: 0%;
}
body {
font-family: "Inter", "Inter Fallback", sans-serif;
}
Subsetting через unicode-range
Большинство сайтов исправно скармливают пользователям кириллицу, латиницу, греческий, вьетнамские расширения и Symbol-блоки одним пухлым файлом. Если ваш контент — это, по сути, кириллица, всё лишнее можно отсечь и сэкономить 60–80% веса. Делается это через unicode-range: браузер скачивает нужный subset только тогда, когда соответствующие символы действительно встречаются на странице.
@font-face {
font-family: "Inter";
font-weight: 100 900;
font-display: swap;
src: url("/fonts/Inter-cyrillic.woff2") format("woff2-variations");
unicode-range: U+0400-04FF, U+0500-052F, U+2DE0-2DFF, U+A640-A69F;
}
@font-face {
font-family: "Inter";
font-weight: 100 900;
font-display: swap;
src: url("/fonts/Inter-latin.woff2") format("woff2-variations");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
Сам subsetting удобно делать через glyphhanger (по-прежнему стандарт де-факто) или subfont:
# Установка и subsetting
npm i -g glyphhanger
glyphhanger https://example.com \
--subset=fonts/InterVariable.woff2 \
--formats=woff2 \
--family="Inter"
Preload: ускоряем обнаружение критических шрифтов
Браузер находит шрифт только после того, как разберёт CSS и встретит элемент, который этот шрифт реально использует. Для LCP-текста — это поздно. Лекарство — preload в <head>:
<link
rel="preload"
href="/fonts/InterVariable-cyrillic.woff2"
as="font"
type="font/woff2"
crossorigin
/>
Три правила, на которых регулярно спотыкаются:
- Атрибут
crossoriginобязателен, даже если шрифт лежит на том же домене. Без него браузер сделает второй запрос — и весь смысл preload отправится в мусорку. - Preload — только тот subset и то начертание, которые реально нужны above-the-fold. «Preload всё подряд» не работает: вы просто отгрызаете полосу у LCP-изображения.
preconnectк стороннему CDN не заменяет preload. Первый экономит TCP+TLS, второй — TTFB и RTT обнаружения. Это разные оси.
103 Early Hints для шрифтов
В 2026 году 103 Early Hints, наконец, нормально поддерживаются всеми основными CDN — Cloudflare, Fastly, Akamai, Vercel Edge Network. Отдавайте preload через 103-й ответ, чтобы браузер начал тянуть шрифты ещё до того, как сервер дописал HTML:
HTTP/1.1 103 Early Hints
Link: </fonts/InterVariable-cyrillic.woff2>; rel=preload; as=font; type=font/woff2; crossorigin
Link: </css/critical.css>; rel=preload; as=style
HTTP/1.1 200 OK
Content-Type: text/html
...
В реальных измерениях это вытягивает 100–300 мс из LCP при холодной генерации HTML на edge. Не магия, но ощутимо.
Self-hosting vs Google Fonts CDN
В 2026 году ответ, честно говоря, скучный: self-hosting почти всегда выигрывает. Причины:
- Браузеры давно не шарят кеш между доменами (cache partitioning внедрён ещё в 2020-м), так что аргумент «у всех же закеширован Google Fonts» — миф, которому уже шесть лет.
- Лишний DNS+TCP+TLS до
fonts.gstatic.comнакидывает ~150 мс к TTFB шрифта. - CSS-файл
fonts.googleapis.com/css2— render-blocking и требует ещё одного RTT. - Файлы Google Fonts вы не можете надёжно preload-ить: их URL генерируется внутри CSS.
Берите @fontsource или @fontsource-variable — это npm-пакеты с готовыми subset-файлами и @font-face-правилами, которые встраиваются прямо в ваш бандл:
npm i @fontsource-variable/inter
// в main.ts или _app.tsx
import "@fontsource-variable/inter/cyrillic.css";
import "@fontsource-variable/inter/latin.css";
Проверка: как измерить эффект
Когда оптимизации сделаны, не верьте «на глаз». Проверьте три вещи в Chrome DevTools и WebPageTest.
1. Время до отрисовки шрифта
Во вкладке Performance раскройте Network, найдите шрифтовые запросы и посмотрите, когда они стартуют относительно начала навигации. Цель — старт в первые 200 мс.
2. CLS от шрифтов
Откройте Performance Insights, запишите профиль и посмотрите Layout shift culprits. Если в списке маячат текстовые элементы с пометкой «Web font caused this shift» — значит size-adjust и fallback-метрики ещё не доехали до прод-сборки.
3. LCP-кандидат
В Performance Insights раскройте секцию LCP. Если LCP-элемент — текст, и время рендера выше 1.2 с при FCP в районе 600 мс, в 9 из 10 случаев виноват именно блокирующий шрифт.
// Программное измерение через PerformanceObserver
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name.includes(".woff2")) {
console.log("font load", entry.name,
"started:", entry.startTime.toFixed(0),
"duration:", entry.duration.toFixed(0));
}
}
}).observe({ type: "resource", buffered: true });
Чек-лист готовности 2026
- Один variable-файл вместо четырёх статических начертаний
font-display: swapдля основного текста,optional— для декоративных шрифтовsize-adjustиascent-overrideдля fallback-семейства- Subsetting через
unicode-rangeс отдельными файлами под кириллицу и латиницу - Preload — только для above-the-fold шрифтов, обязательно с
crossorigin - 103 Early Hints на edge для self-hosted шрифтов
- Self-hosting через
@fontsource-variable, без Google Fonts CDN - WOFF2 как единственный формат (97%+ поддержки в 2026)
FAQ
Нужно ли preload-ить все шрифты на странице?
Нет, и пожалуйста, не надо. Preload — для шрифтов, которые реально рендерятся above-the-fold, обычно это один-два файла (основной текст плюс заголовок). Preload «на всякий случай» отнимает полосу у LCP-изображения и сделает только хуже.
Что лучше — font-display: swap или optional?
Зависит от того, что для вас важнее. swap улучшает LCP (текст виден сразу), но даёт визуальный «прыжок» при подмене и риск CLS, если не настроен size-adjust. optional даёт нулевой CLS и никакого FOUT, но на медленной сети пользователь просто не увидит ваш фирменный шрифт. Для контентных сайтов разумный дефолт — swap + size-adjust; для маркетинговых лендингов с декоративными гарнитурами — optional.
Почему Google Fonts замедляет сайт даже с preconnect?
Три причины. Cache partitioning делает невозможным переиспользование кеша между сайтами. CSS-файл fonts.googleapis.com/css2 — render-blocking. И URL шрифтовых файлов нельзя надёжно preload-ить, потому что они зашиты внутрь возвращаемого CSS. Self-hosting закрывает все три проблемы разом и почти всегда даёт +100–300 мс к скорости загрузки шрифтов.
Можно ли использовать system-ui вместо веб-шрифтов?
Да, и по производительности это объективно лучший выбор: ноль сетевых запросов, ноль CLS, мгновенный рендер. Стек font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; отдаёт нативный шрифт каждой ОС. Но если бренд требует своей уникальной гарнитуры — используйте описанные выше оптимизации, чтобы максимально приблизиться к system-ui по скорости.
Как subsetting влияет на SEO и доступность?
Никак, если он сделан правильно. unicode-range заставляет браузер скачивать только нужный subset, но не запрещает текст в других алфавитах — для них используется fallback или другой @font-face. Контент остаётся в DOM, прекрасно индексируется поисковиками и читается скрин-ридерами. Единственная реальная опасность — перепутать диапазоны и случайно отрезать символы, которые на странице действительно встречаются.