Шрифты
Шрифт весит немного, но стоит дорого: он умеет прятать текст на секунды и двигать вёрстку в момент подмены. Разберём, как этого не допустить.
FOIT (Flash of Invisible Text) — период, когда текст уже свёрстан, но не показан, потому что браузер ждёт шрифт. FOUT (Flash of Unstyled Text) — текст показан системным шрифтом и позже перерисовывается вашим.
Зачем этим заниматься
Веб-шрифт — единственный ресурс, который по умолчанию имеет право спрятать контент. Браузер натыкается на @font-face, начинает качать файл и до трёх секунд держит текст невидимым. Это прямое попадание в LCP: если ваш заголовок — самый крупный элемент экрана, метрика фиксируется не тогда, когда посчитан layout, а тогда, когда приехал шрифт.
Вторая беда приходит следом. Текст всё-таки показали фолбэком, шрифт догрузился, буквы поменяли ширину — и весь блок под ними подпрыгнул. Это CLS, причём тот его вид, который замечают даже люди, никогда не слышавшие про Core Web Vitals.
Плюс банальный вес: полный набор Inter в четырёх начертаниях с кириллицей и латиницей легко даёт 400–600 КБ. Урезать это до 60–80 КБ — работа на полчаса.
Формат: только woff2
Не надо коллекционировать форматы. WOFF2 — это шрифт, сжатый Brotli, он на 20–30% легче старого WOFF и поддерживается всеми браузерами, которые вообще способны открыть современный сайт. TTF и OTF в вебе не нужны: это несжатые исходники, отдавать их пользователю — то же самое, что отдавать BMP вместо JPEG.
@font-face {
font-family: "Inter";
src: url("/fonts/inter-400-cyrillic.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter";
src: url("/fonts/inter-400-latin.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+2000-206F, U+2122;
}
Что здесь важно. Одно семейство Inter описано двумя правилами с разными unicode-range. Браузер скачает только тот файл, символы из которого реально встретились на странице: англоязычная страница не получит кириллицу, и наоборот. Это встроенный механизм, никакого JS.
Подмножества: выкидываем то, чего нет на сайте
Шрифт вроде Roboto или Inter содержит тысячи глифов: греческий, вьетнамский, знаки валют, стрелки, полный набор диакритики. На вашем сайте из них используются, дай бог, три сотни. Обрезка (subsetting) выкидывает лишнее из самого файла.
pip install fonttools brotli
pyftsubset Inter-Regular.ttf \
--unicodes="U+0000-00FF,U+0400-045F,U+2116" \
--layout-features="kern,liga" \
--flavor=woff2 \
--output-file=inter-400-subset.woff2
Типичный результат: 320 КБ TTF превращаются в 35–45 КБ WOFF2. Ключ --layout-features оставляет только кернинг и лигатуры, всё остальное OpenType-хозяйство удаляется.
Отдельно про Google Fonts. Их CSS-эндпоинт уже отдаёт нарезанные по unicode-range подмножества, так что вес там приличный. Проблема в другом: это внешний домен, а значит лишний DNS + TLS-хендшейк на критическом пути, и надежда на «общий кэш» давно мертва — браузеры разделяют кэш по сайтам. Скачайте файлы и положите к себе. Одной третьей стороной меньше.
Preload: сокращаем цепочку
Чтобы браузер узнал о шрифте, ему нужно: получить HTML, найти ссылку на CSS, скачать CSS, распарсить его, применить правила и обнаружить, что текст на странице подпадает под @font-face. Только тогда стартует загрузка шрифта. Это три круга по сети подряд.
preload сокращает цепочку до одного круга — запрос уходит сразу из <head>, параллельно с CSS:
<link rel="preload"
href="/fonts/inter-400-cyrillic.woff2"
as="font"
type="font/woff2"
crossorigin>
Атрибут crossorigin здесь обязателен, даже если файл лежит на вашем же домене. Причина в том, что шрифты грузятся в анонимном CORS-режиме; без crossorigin предзагруженный файл окажется в другом «отсеке» кэша, и браузер скачает его второй раз. В консоли вы увидите предупреждение о неиспользованном preload — это ровно оно.
И мера: предзагружайте один-два файла, не больше. Preload — это очередь без очереди, он отбирает полосу у картинок и CSS. Обычно достаточно основного начертания (400) для того языка, на котором написан контент.
font-display: кто кого ждёт
Жизнь шрифта делится на три периода: block (текст невидим, ждём), swap (показан фолбэк, готовы подменить) и failure (шрифт объявлен неудавшимся и больше не применяется в этой загрузке).
| Значение | Период блокировки | Период подмены | Когда брать |
block | ~3 с (текст невидим) | бесконечный | практически никогда |
swap | около 0 | бесконечный | основной текст, заголовки |
fallback | ~100 мс | ~3 с | компромисс, когда FOUT раздражает дизайнера |
optional | ~100 мс | нет | второстепенный шрифт, который не жалко не показать |
По умолчанию (auto) браузеры ведут себя как block — то есть прячут текст. Поэтому font-display надо ставить осознанно всегда.
swap — рабочая лошадка: текст виден сразу, LCP не ждёт шрифт. Цена — прыжок вёрстки при подмене. optional — самый жёсткий и самый честный к пользователю вариант: на медленной сети браузер просто оставит системный шрифт, а ваш докачает в фон и применит при следующем заходе. Ноль CLS, ноль задержки. Для шрифта в теле статьи это часто лучше, чем кажется дизайнеру.
Как убрать прыжок при подмене
FOUT прыгает потому, что у фолбэка другие метрики: другая высота строчных, другая ширина символов. Это чинится описанием фолбэка как отдельного @font-face с подкрученными метриками:
@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", system-ui, sans-serif;
}
Подобрав size-adjust, вы заставляете Arial занимать ровно столько же места, сколько займёт Inter. Подмена происходит — прыжка нет. Цифры для популярных пар шрифтов считает утилита fontaine, а Next.js и Nuxt делают это автоматически.
Системный шрифт как честный вариант
Ноль килобайт, ноль запросов, ноль FOIT, ноль CLS. Текст рисуется тем шрифтом, который пользователь и так видит во всех приложениях на своём устройстве.
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
}
Так живут GitHub и админки половины SaaS-сервисов. Если брендинг не держится на конкретной гарнитуре — а он обычно не держится, — это лучший тюнинг производительности из возможных: вы просто удаляете проблему вместо того, чтобы её оптимизировать.
Как это работает
Шрифт — ленивый ресурс по природе. Правило @font-face само по себе ничего не качает: файл запрашивается, только когда браузер находит на странице текст, который под это правило попадает (нужное семейство, начертание, диапазон символов). Отсюда два неочевидных следствия.
Первое: если вы подключили шесть начертаний, а в вёрстке используются два, скачаются два — и это хорошо. Но если где-то в глубине CSS кто-то поставил font-weight: 300 одному элементу, приедет и третий файл. Проверять надо не CSS, а вкладку Network.
Второе: браузер умеет синтезировать отсутствующие начертания. Просите font-weight: 500, а подключены только 400 и 700 — браузер не скачает ничего лишнего, он просто искусственно утолщит 400. Выглядит это грязно, а на глаз ошибку почти не поймаешь, пока не сравнишь с настоящим Medium.
Частые ошибки
- Подключены Regular, Italic, Medium, SemiBold, Bold, Black — «на всякий случай». Два начертания закрывают 95% макетов.
preloadбезcrossorigin: файл качается дважды, в консоли висит предупреждение.@import url(...)в CSS вместо<link>в<head>— добавляет ещё один последовательный круг по сети.- Шрифт иконок с
font-display: swap: пока он едет, вместо иконок пляшут квадратики и буквы. Иконки давно принято отдавать как SVG. - Оставили
font-displayпо умолчанию — получили трёхсекундный FOIT на плохом 3G. - Отдали TTF на 300 КБ, потому что «конвертер не поставился».
- Предзагрузили все шесть файлов: они отобрали полосу у LCP-картинки, и стало хуже, чем было.
Итоги
- Только
woff2. Остальные форматы — исторический балласт. - Режьте подмножества:
pyftsubsetиunicode-rangeуменьшают файл в 5–8 раз. font-display: swapдля контентных шрифтов,optional— там, где важнее нулевой CLS.preloadс обязательнымcrossoriginи только на один-два ключевых файла.- Прыжок при подмене лечится метриками фолбэка (
size-adjust,ascent-override), а не отказом отswap. - Системный стек — не поражение, а самый быстрый шрифт в мире.