В веб‑производительности нет «серебряной пули», но грамотная работа со шрифтами даёт очень осязаемый выигрыш. Шрифты часто грузятся с высокой приоритетностью, попадают под политику cross-origin, создают FOIT/FOUT эффекты, а ещё просыпаются через месяц с протухшим кэшем. В этой статье разложим по полочкам связку fonts + preload + CORS + Cache-Control + font-display и покажем устойчивые конфигурации для Nginx и Apache. Если вам нужен полный root‑доступ для тонкой настройки Nginx/Apache, возьмите облачный VDS.
Как браузер грузит шрифт и почему тут важны CORS, preload и cache-control
Большинство проектов используют WOFF2 как основной формат шрифтов: он сжат, поддерживается современными браузерами и даёт лучшую performance. Браузер встречает в CSS правило @font-face, сопоставляет его со стилями на странице и решает, когда запросить файл. Момент запроса критичен: если шрифт нужен «над фолдом», задержка даёт заметное моргание текста (FOUT) или задержку отображения (FOIT).
Три ключа к скорости и стабильности:
- preload — подсказка браузеру «нужно это прямо сейчас». Работает, только если
as="font"и указан корректныйcrossoriginдля cross-origin сценариев. - CORS — шрифты относятся к «чувствительным» ресурсам; при загрузке с другого источника без корректных заголовков получим блокировку либо «таинственные» повторные запросы.
- Cache-Control — долгий кеш с
immutableи версионирование в имени файла предотвращают лишние сетевые обращения и «случайные» 304.
Если вы видите в консоли предупреждения вроде «The resource was preloaded but not used» или «No 'Access-Control-Allow-Origin' header», значит связка preload + CORS + @font-face настроена несимметрично.
Preload шрифтов без двойной загрузки
Минимальный и правильный пример для WOFF2:
<link rel="preload" as="font" type="font/woff2" href="/fonts/Inter-roman.var.woff2" crossorigin>
Ключевые детали:
as="font"— чтобы браузер назначил верный приоритет и использовал правильный кеш.type="font/woff2"— помогает браузеру быстрее подтвердить тип контента.crossorigin— обязательно, если файл шрифта загружается с иного origin или если на шрифт будут распространяться CORS‑ограничения (например, при строгих заголовках безопасности). Для шрифтов почти всегда достаточно анонимного режима (без куки и кредов), то есть простоcrossoriginилиcrossorigin="anonymous".
Чтобы избежать double fetch (двойной запрос), URL в preload должен совпадать с URL в @font-face побайтно: без различий в регистре, без дополнительных query‑параметров и с тем же набором CORS‑атрибутов. Если CSS грузит шрифт с crossorigin, а preload — без него, браузер посчитает это разными запросами.
Связка с @font-face
@font-face {
font-family: "Inter";
src: url("/fonts/Inter-roman.var.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
Совет: если используете несколько начертаний или сабсеты, делайте отдельные preload только для тех файлов, которые действительно нужны «над фолдом». Остальные пускай грузятся лениво по факту применения CSS.

font-display: как перестать бояться FOIT/FOUT
font-display управляет поведением текста, пока шрифт не загружен:
swap— показывает системный шрифт сразу, после загрузки подменяет на кастомный. Лучший выбор для длинного текста, максимальная читабельность и отзывчивость.fallback— короткая фаза блокировки (обычно до 100 мс), затем показ фолбэка, затем подмена. Компромисс между «не мигать» и «сохранить метрики».optional— браузер может вообще не грузить шрифт при неблагоприятной сети. Хорошо для второстепенных шрифтов или когда метрики очень чувствительны.block— блокировка на длительное время. Сегодня почти не используется: риск «пустого» текста.auto— по умолчанию, непредсказуемо для UX.
Практика для текста контента — swap или optional (если метрики и дизайн выдерживают). Для иконок‑шрифтов чаще важнее отсутствие «ломающихся» значков, поэтому многие выбирают fallback. И всё же иконки лучше выносить в SVG.
CORS для шрифтов: почему без него всё ломается
Шрифты подпадают под политику cross-origin. Если вы раздаёте их с иного домена или субдомена (например, со статического хоста или CDN), браузеру нужны корректные заголовки CORS, иначе запросы будут блокироваться или повторяться иной политикой. В большинстве случаев достаточно выдать:
Access-Control-Allow-Origin: конкретный origin, запрашивающий ресурс;Vary: Origin: чтобы кеш корректно различал версии для разных источников;- при необходимости —
Cross-Origin-Resource-Policyдля согласования с политикой изоляции (если включали строгие заголовки безопасности).
Не используйте
Access-Control-Allow-Origin: *совместно с куки/креденшелами. Шрифтам куки не нужны — оставляйте запрос анонимным и не добавляйтеAccess-Control-Allow-Credentials.
Nginx: CORS только для шрифтов и только для нужных сайтов
map $http_origin $cors_allowed {
default 0;
~*^https?://(www\.)?example\.com$ 1;
~*^https?://static\.example\.com$ 1;
}
server {
# ... остальная конфигурация
location ~* \.(woff2|woff|ttf)$ {
# Типы на всякий случай (обычно уже настроены глобально)
types { font/woff2 woff2; font/woff woff; application/font-sfnt ttf; }
if ($cors_allowed) {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Vary "Origin" always;
# Если используете строгую изоляцию, можно явно разрешить кросс-доступ
add_header Cross-Origin-Resource-Policy "cross-origin" always;
}
# Долгий кеш для версионированных файлов
add_header Cache-Control "public, max-age=31536000, immutable" always;
expires 1y;
}
}
Значение $http_origin отражается только для шрифтов, и только если домен входит в белый список. Это снижает риски, связанные с «отражением» Origin.
Apache: CORS и кеш для WOFF2
<IfModule mod_mime.c>
AddType font/woff2 .woff2
AddType font/woff .woff
AddType application/font-sfnt .ttf
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(woff2|woff|ttf)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
# Разрешаем CORS только для своих доменов
SetEnvIfNoCase Origin "^https?://(www\.)?example\.com$" ORIGIN_OK=$0
SetEnvIfNoCase Origin "^https?://static\.example\.com$" ORIGIN_OK=$0
<FilesMatch "\.(woff2|woff|ttf)$">
Header set Vary "Origin"
Header set Access-Control-Allow-Origin "%{ORIGIN_OK}e" env=ORIGIN_OK
Header set Cross-Origin-Resource-Policy "cross-origin"
</FilesMatch>
</IfModule>
Вариант с отражением Origin безопасен, когда применяется только к статике шрифтов и у вас есть белый список доменов. Если раздаёте шрифты строго с того же домена, CORS не нужен вовсе. Для дополнительных паттернов загляните в шпаргалку по CORS‑заголовкам для Nginx и Apache.
Cache-Control для шрифтов: долго, неизменно и с версионированием
Шрифты — идеальные кандидаты для «долгого» кеша. Базовая стратегия:
- Включаем
Cache-Control: public, max-age=31536000, immutableдля файлов с хешем в имени:Inter-roman.var.06b0f1.woff2. - При изменении файла — меняем имя хеша, и браузер качает новый вариант без конфликтов с кешем.
- Оставляем
ETag— неплохо для CDN и ручной проверки, но приimmutableон редко используется браузером.
Сжатие: WOFF2 уже сжат, поэтому gzip или brotli для него не применяются. Убедитесь, что ваш сервер не пытается компрессовать эти расширения — это только тратит CPU и не меняет размер.
Nginx: исключаем сжатие и настраиваем заголовки
http {
# ...
gzip on;
gzip_types text/css application/javascript;
# .woff2 НЕ добавляем в gzip_types
}
server {
location ~* \.(woff2|woff|ttf)$ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
expires 1y;
}
}
Apache: кеширование шрифтов
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
ExpiresByType application/font-sfnt "access plus 1 year"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(woff2|woff|ttf)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
</IfModule>
Чтобы избежать предупреждений о смешанном контенте и получить HTTP/2/3, держите шрифты на HTTPS. Если сертификата ещё нет — оформите SSL-сертификаты.
Preload через Link header и 103 Early Hints
Если трудно править HTML, можно подсказать preload заголовком на уровне сервера. Бонус: при поддержке Early Hints (код 103) браузер начнёт тянуть шрифт ещё до основного ответа.
location = / {
add_header Link "</fonts/Inter-roman.var.woff2>; rel=preload; as=font; type=font/woff2; crossorigin" always;
# ...
}
Убедитесь, что этот путь совпадает с @font-face, иначе снова будет двойная загрузка. Подробнее о приоритетах загрузки и Early Hints — в заметке про preload, приоритеты и кеш.
Как выбрать, что именно preload-ить
Preload — это высокий приоритет загрузки. Если переборщить, забьёте канал и ухудшите TTFB/TTI. Выбираем минимум: основной романный шрифт для латиницы/кириллицы, используемый «над фолдом». Для остальных начертаний и скриптов полагайтесь на загрузку по требованию.
Хороший тон — разбить шрифт на сабсеты с unicode-range, чтобы браузер не тянул лишнее для страницы на одном языке.
@font-face {
font-family: "InterSubset";
src: url("/fonts/Inter-latin.woff2") format("woff2");
unicode-range: U+000-5FF;
font-display: swap;
}
@font-face {
font-family: "InterSubset";
src: url("/fonts/Inter-cyrillic.woff2") format("woff2");
unicode-range: U+0400-04FF;
font-display: swap;
}
В таком случае preload имеет смысл делать только для того сабсета, который реально встретится на первом экране. Этот подход уменьшает размер первой пачки данных и заметно улучшает performance на мобильной сети.
Типичные ошибки и как их избежать
- Разные URL в preload и @font-face. Решение: унифицируйте путь и параметры, включая query‑строку.
- Отсутствует
crossoriginв preload при cross-origin раздаче. Решение: добавьтеcrossoriginи корректные CORS‑заголовки на сервере. - Шрифт с коротким кешем без
immutable. Решение: версионируйте файл и увеличьте TTL. - Слишком много preload. Решение: ограничьтесь критичным шрифтом «над фолдом».
- Gzip для WOFF2. Решение: исключите расширение из компрессии.
- Строгие заголовки безопасности блокируют кросс‑ресурсы. Если используете изоляцию, проверьте
Cross-Origin-Resource-Policyи совместимость с CORS.
Отладка: что смотреть в DevTools и через curl
В DevTools обратите внимание на:
- Initiator: у прелоада будет «link preload», у обычной загрузки — CSS.
- Priority: после preload должен быть высокий приоритет.
- Headers: убедитесь, что
content-typeкорректный, естьCache-Control, при cross-origin виденAccess-Control-Allow-OriginиVary: Origin. - Waterfall: нет ли двойной загрузки одного и того же файла.
Проверка CORS и кеша из консоли:
# Эмуляция cross-origin запроса (Origin заголовок)
curl -I -H "Origin: https://www.example.com" https://static.example.com/fonts/Inter-roman.var.woff2
# Проверка, что сервер не отдает лишние заголовки и тип корректный
curl -I https://site.example.com/fonts/Inter-roman.var.woff2
Вы должны увидеть для cross-origin запросов: HTTP/2 200, content-type: font/woff2, access-control-allow-origin: https://www.example.com, vary: Origin, cache-control: public, max-age=31536000, immutable.

И ещё немного практики: метрики, приоритеты и пре-коннекты
Preload повышает приоритет сетевого запроса. Если шрифтов несколько, а канал ограничен, это может задержать HTML, CSS или критический JS. Поэтому вместо «preload всего» используйте «preload ровно одного действительно критичного шрифта». Остальные отдавайте через @font-face с font-display: swap.
Для шрифтов с другого домена иногда полезно «раскочегарить» соединение заранее. В ряде случаев помогает preconnect к статическому домену — он установит TCP/TLS раньше. Но помните: preconnect сам по себе не заменяет preload и CORS; это лишь оптимизация рукопожатия. Применяйте точечно, только для доменов, которые действительно участвуют в критическом пути.
Чек-лист внедрения
- Определите, какой шрифт реально нужен «над фолдом» и есть ли смысл в сабсетах через
unicode-range. - Добавьте
preloadдля одного критичного WOFF2 сas="font",type="font/woff2"иcrossoriginпри необходимости. - Синхронизируйте URL и параметры между
preloadи@font-face. - Настройте CORS на сервере, если шрифт грузится cross-origin:
Access-Control-Allow-OriginиVary: Origin. - Включите
Cache-Control: public, max-age=31536000, immutableдля версионированных файлов. - Проверьте типы (
font/woff2) и отключите лишнюю компрессию для WOFF2. - Выберите
font-displayпо контенту: текст —swapилиoptional, иконки — подумайте о замене на SVG. - Отладьте в DevTools и через curl; устраните предупреждения «preloaded but not used» и CORS‑ошибки.
Продвинутые нюансы
Variable fonts. Переменные шрифты удобны тем, что заменяют набор начертаний одним файлом. Но они крупнее. Не всегда стоит делать preload всего variable‑шрифта — возможно, выгоднее сабсетить и грузить остальные оси лениво.
FOFT (Flash of Faux Text). Использование метрик‑совместимых системных шрифтов как фолбэка уменьшает скачки макета при подмене. В паре с font-display: swap это даёт ровный UX.
Early Hints и CDN. Если перед фронтом стоит CDN с поддержкой 103, вынесите Link: preload на край и дайте шрифту фору. При этом следите, чтобы правила CORS были на месте на том же краю.
Политики безопасности. Если включали строгие заголовки (например, для изоляции контента), оцените влияние Cross-Origin-Resource-Policy на шрифты. Для cross-origin загрузки может потребоваться значение cross-origin.
Итоги
Чтобы шрифты работали быстро и предсказуемо, достаточно собрать вместе четыре кирпича: аккуратный preload, корректный CORS, долгий Cache-Control и уместный font-display. Добавьте к этому дисциплину в путях и параметрах, исключите лишнюю компрессию WOFF2 — и получите стабильную загрузку без «миганий», повторных запросов и предупреждений браузера.


