Шрифты и FOIT/FOUT: как убрать прыжки текста при загрузке
Заголовок сначала невидим, потом моргает системным шрифтом и уезжает на две строки. Разбираем, откуда берутся FOIT и FOUT, чем платит каждая стратегия font-display и как убрать layout shift.

Открываете страницу — блок с заголовком пустой полсекунды, потом текст появляется системным шрифтом, а ещё через мгновение подменяется фирменным и уезжает вниз на строку. Это два разных дефекта: FOIT (Flash of Invisible Text) — пустое место, пока грузится шрифт, и FOUT (Flash of Unstyled Text) — мигание запасным шрифтом с последующим прыжком. Оба бьют по метрике CLS в Core Web Vitals и раздражают пользователя. Разберём, какая стратегия font-display что даёт, чем платит каждая, и как свести сдвиг макета к нулю через size-adjust. Актуально для всех современных браузеров на конец 2024 года.
Откуда берутся прыжки
Когда парсер встречает текст, для которого объявлен ещё не загруженный веб-шрифт, у браузера три варианта поведения: показать пустоту и ждать (FOIT), сразу показать запасной шрифт и подменить его позже (FOUT) или не показывать веб-шрифт вовсе, если он опоздал. Что именно произойдёт, задаётся дескриптором font-display внутри @font-face.
Прыжок возникает не из-за самого факта подмены, а из-за того, что метрики запасного и целевого шрифта не совпадают. Разная высота строки, разная ширина символов — и после подмены абзац занимает другое число строк. Браузер сдвигает всё, что ниже. Это и есть layout shift.
Как посчитать масштаб проблемы
Откройте DevTools → вкладка Performance, запишите загрузку страницы и найдите события Layout Shift. В отчёте Lighthouse сдвиги из-за шрифтов видны в диагностике «Avoid large layout shifts» с указанием конкретных элементов. Целевое значение CLS — не выше 0.1.
Пять значений font-display
Дескриптор делит время загрузки на два интервала: блокирующий период (block period), когда текст невидим, и период подмены (swap period), когда показан запасной шрифт и он ещё может быть заменён на веб-шрифт. Значения различаются длиной этих интервалов.
| Значение | Блокирующий период | Что видит пользователь | Риск |
|---|---|---|---|
auto | на усмотрение браузера | обычно как block | непредсказуемо |
block | короткий (около 3 с), затем бесконечный swap | пустота, потом веб-шрифт | FOIT, текст невидим до загрузки |
swap | отсутствует | сразу запасной, потом веб-шрифт | FOUT, прыжок при подмене |
fallback | очень короткий (около 100 мс), затем короткий swap | краткая пустота, запасной, возможна подмена | компромисс, шрифт может не примениться |
optional | очень короткий, swap отсутствует | запасной; веб-шрифт применится только при мгновенной загрузке | нулевой CLS, но шрифт часто не грузится в первый визит |
Точная длительность блокирующего периода не стандартизирована жёстко и слегка различается между движками, поэтому в таблице приведены типичные значения из спецификации CSS Fonts. Проверяйте поведение в целевых браузерах.
FOIT прячет проблему в невидимости, FOUT — выносит её на экран прыжком. Ни то ни другое не лечит причину: несовпадение метрик шрифтов. Лечит его size-adjust.
Подход 1: swap плюс подгонка метрик
Самое надёжное сочетание: показываем текст сразу запасным шрифтом (swap), но подгоняем метрики запасного так, чтобы он занимал ровно то же место, что и веб-шрифт. Тогда подмена не двигает макет.
Механизм — дескрипторы size-adjust, ascent-override, descent-override и line-gap-override внутри @font-face для локального запасного шрифта. Вы объявляете «виртуальный» шрифт на базе системного и правите его метрики под целевой.
/* Целевой веб-шрифт */
@font-face {
font-family: "Inter";
src: url("/fonts/inter.woff2") format("woff2");
font-display: swap;
}
/* Запасной на базе системного, подогнанный под метрики Inter */
@font-face {
font-family: "Inter Fallback";
src: local("Arial");
size-adjust: 107%; /* масштаб глифов под целевой */
ascent-override: 90%; /* высота над базовой линией */
descent-override: 22%;
line-gap-override: 0%;
}
body {
/* сначала подогнанный запасной, потом веб-шрифт */
font-family: "Inter", "Inter Fallback", sans-serif;
}
Значения процентов подбираются под конкретную пару шрифтов, наугад их не выставить. Их считают из таблиц метрик обоих шрифтов. Считать вручную не нужно: инструмент fontaine и утилита capsize генерируют дескрипторы автоматически, а Next.js делает это для локальных шрифтов через next/font из коробки. Названия конкретных дескрипторов сверяйте с разделом CSS Fonts Module Level 5 — ascent-override и родственные поддерживаются в актуальных Chrome, Firefox и Safari, но в старых сборках их может не быть.
Подход 2: preload критичного шрифта
Даже идеально подогнанный запасной — это всё равно мелькание. Если шрифт нужен в первом экране, дайте браузеру начать его загрузку раньше, чем он дойдёт до CSS:
<link
rel="preload"
href="/fonts/inter.woff2"
as="font"
type="font/woff2"
crossorigin
>
Атрибут crossorigin обязателен даже для шрифтов со своего домена — без него браузер запросит файл повторно, и preload не сработает. Не грузите так все начертания подряд: preload конкурирует за пропускную способность с критичными ресурсами. Один-два файла для видимого текста — потолок.
Подход 3: optional для второстепенного текста
Если веб-шрифт — украшение, а не бренд, font-display: optional даёт нулевой CLS: браузер либо успевает загрузить шрифт мгновенно (обычно из кеша при повторном визите), либо оставляет запасной и не подменяет его на этой загрузке. Прыжка не будет никогда, но в первый визит пользователь может вообще не увидеть ваш шрифт.
Когда что выбирать
- Заголовки и брендовый текст в первом экране —
swapс подгонкой метрик плюс preload. Шрифт важен, прыжок недопустим. - Основной текст статьи —
swapс подгонкой метрик, без preload, если шрифт не в первом экране. - Декоративные акценты, иконочные шрифты для неключевого UI —
optional. Не жалко, если не примерится. - Никогда —
blockбез веских причин: это FOIT и медленный первый показ контента.
Что ломается на практике
- Забыли crossorigin в preload. Шрифт качается дважды, preload бесполезен. В Network видно два запроса к одному файлу.
- size-adjust подобран для одного начертания. Regular совпал, а bold всё равно прыгает: у жирного начертания свои метрики, дескрипторы нужны отдельно на каждое.
- Вариативный шрифт с несколькими осями. Подгонка метрик усложняется — проверяйте на всех используемых значениях осей, а не только на дефолтном.
- Иконочный шрифт со swap. До подмены на месте иконок видны прямоугольники или буквы запасного шрифта. Для иконок лучше SVG или
optional. - Кеш браузера обманывает при тесте. После первого визита шрифт в кеше и грузится мгновенно — прыжка не видно. Тестируйте в режиме с отключённым кешем и с троттлингом сети (Slow 3G).
Что делать дальше
Соберите список всех начертаний, реально используемых на странице, и уберите лишние — каждое лишнее начертание это отдельная загрузка и отдельная подгонка. Сгенерируйте дескрипторы метрик через fontaine или capsize для каждой пары «веб-шрифт → системный запасной». Настройте preload только для шрифтов первого экрана. После деплоя измерьте CLS в поле через отчёт Chrome UX Report или веб-виталы в аналитике: лабораторный Lighthouse не всегда совпадает с реальными устройствами и сетями пользователей.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: MDN: @font-face — font-display, size-adjust, ascent-override, web.dev: Best practices for fonts
Частые вопросы
А если у пользователя старый браузер без ascent-override?
Почему после подгонки метрик текст всё равно немного прыгает?
Не сработал preload — шрифт всё равно грузится поздно. В чём дело?
Что лучше для CLS — swap или optional?
Нужно ли вообще заморачиваться, если шрифт кешируется после первого визита?
Помогает ли self-hosting шрифтов вместо Google Fonts?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.