Оптимизация шрифтов в 2026: variable fonts, font-display, subsetting и устранение CLS

Шрифты — самая недооценённая причина плохих Core Web Vitals в 2026 году. Разбираем, как variable fonts, font-display, size-adjust, subsetting и 103 Early Hints полностью убирают CLS и FOIT и ускоряют LCP на 30–60%.

Шрифты — наверное, самая недооценённая причина плохих 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
/>

Три правила, на которых регулярно спотыкаются:

  1. Атрибут crossorigin обязателен, даже если шрифт лежит на том же домене. Без него браузер сделает второй запрос — и весь смысл preload отправится в мусорку.
  2. Preload — только тот subset и то начертание, которые реально нужны above-the-fold. «Preload всё подряд» не работает: вы просто отгрызаете полосу у LCP-изображения.
  3. 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, прекрасно индексируется поисковиками и читается скрин-ридерами. Единственная реальная опасность — перепутать диапазоны и случайно отрезать символы, которые на странице действительно встречаются.

Об авторе Editorial Team

Our team of expert writers and editors.