Как оптимизировать загрузку шрифтов и избежать FOIT/FOUT в 2025
В 2025 году веб-шрифты остаются одной из главных причин проблем с производительностью и визуальной стабильностью. FOIT (Flash of Invisible Text) и FOUT (Flash of Unstyled Text) продолжают влиять на пользовательский опыт, но современные подходы и новые API позволяют эффективно решать эти проблемы.
| Показатель | Значение | Описание |
|---|---|---|
| 📊 Использование кастомных шрифтов | 72% | сайтов используют кастомные шрифты |
| ⏱️ Время загрузки | 1.8s | среднее время загрузки шрифтов |
| ⚠️ Проблемы с отображением | 47% | пользователей сталкиваются с FOIT/FOUT |
| 🎯 Целевой показатель | 0.3s | целевой показатель LCP с шрифтами |
1. Понимание проблем: FOIT vs FOUT
⚫ FOIT (Flash of Invisible Text)
Проблема: Текст не отображается до загрузки шрифта
- Пользователь видит пустые области
- Затрудняет чтение и навигацию
- Увеличивает CLS (Cumulative Layout Shift)
- Особенно критично для медленных сетей
/* Типичное поведение по умолчанию */
@font-face {
font-family: 'Custom Font';
src: url('font.woff2') format('woff2');
/* font-display не указан = FOIT */
}
🟠 FOUT (Flash of Unstyled Text)
Проблема: Текст отображается в системном шрифте, затем резко меняется
- Резкая смена типографики
- Может вызывать мерцание
- Лучше FOIT, но всё равно не идеально
- Может нарушить визуальную целостность
/* Поведение при font-display: swap */
@font-face {
font-family: 'Custom Font';
src: url('font.woff2') format('woff2');
font-display: swap;
}
2. Font Display стратегии 2025
Свойство font-display — основной инструмент управления загрузкой шрифтов. В 2025 году появились новые лучшие практики.
| Стратегия | Поведение | Период блокировки | Период подмены | Рекомендация |
|---|---|---|---|---|
| auto | Зависит от браузера | Короткий | Короткий | ❌ Не рекомендуется |
| block | FOIT → системный шрифт | 3-5s | Бесконечно | 🟡 Для критичных шрифтов |
| swap | Сразу системный → кастомный | ~100ms | Бесконечно | ✅ Основная рекомендация |
| fallback | FOIT → системный → кастомный | ~100ms | 3s | ✅ Для баланса UX |
| optional | FOIT → системный (постоянно) | ~100ms | Нет | ✅ Для второстепенных шрифтов |
🎯 Оптимальная настройка 2025
/* Критический шрифт для заголовков */
@font-face {
font-family: 'Heading Font';
src: url('heading.woff2') format('woff2');
font-display: block; /* Краткий FOIT для LCP */
font-weight: 700;
}
/* Основной текстовый шрифт */
@font-face {
font-family: 'Body Font';
src: url('body.woff2') format('woff2');
font-display: swap; /* Быстрая подмена */
font-weight: 400;
}
/* Декоративный шрифт */
@font-face {
font-family: 'Accent Font';
src: url('accent.woff2') format('woff2');
font-display: optional; /* Только если быстро загрузится */
font-weight: 400;
}
⚡ Современный подход с size-adjust
@font-face {
font-family: 'Custom Font';
src: url('font.woff2') format('woff2');
font-display: swap;
size-adjust: 98%; /* Компенсация метрик */
ascent-override: 95%;
descent-override: 25%;
line-gap-override: 0%;
}
/* Системный шрифт-заглушка с такими же метриками */
@font-face {
font-family: 'Custom Font Fallback';
src: local('Arial');
size-adjust: 98%;
ascent-override: 95%;
descent-override: 25%;
line-gap-override: 0%;
}
3. Предзагрузка и оптимизация доставки
❌ Плохая практика
<!-- Загрузка в CSS -->
@font-face {
font-family: 'Font';
src: url('/fonts/font.woff2');
}
<!-- Слишком поздно -->
Результат: Шрифт обнаруживается поздно, FOIT до 3s
✅ Оптимальная практика 2025
<!-- Предзагрузка в HEAD -->
<link rel="preload"
href="/fonts/font.woff2"
as="font"
type="font/woff2"
crossorigin>
<!-- Критический CSS -->
<style>
@font-face {
font-family: 'Font';
src: url('/fonts/font.woff2');
font-display: swap;
}
</style>
Результат: Шрифт начинает загружаться сразу, минимум FOIT
Продвинутая стратегия предзагрузки:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Предзагрузка критических шрифтов -->
<link rel="preload"
href="/fonts/heading-bold.woff2"
as="font"
type="font/woff2"
crossorigin>
<link rel="preload"
href="/fonts/body-regular.woff2"
as="font"
type="font/woff2"
crossorigin>
<!-- Inline критический CSS -->
<style>
/* Системный шрифт как fallback */
.font-fallback {
font-family: system-ui, -apple-system, sans-serif;
}
/* Определение кастомных шрифтов */
@font-face {
font-family: 'Heading Font';
src: url('/fonts/heading-bold.woff2') format('woff2');
font-display: block;
font-weight: 700;
}
@font-face {
font-family: 'Body Font';
src: url('/fonts/body-regular.woff2') format('woff2');
font-display: swap;
font-weight: 400;
}
</style>
</head>
<body>
<!-- Контент -->
</body>
</html>
4. Кастомные решения и современные API
🔧 Font Loading API 2025
class FontLoader {
constructor() {
this.loadedFonts = new Set();
}
async loadFont(fontFamily, fontUrl) {
if (this.loadedFonts.has(fontFamily)) return;
try {
const font = new FontFace(fontFamily, `url(${fontUrl})`);
await font.load();
document.fonts.add(font);
this.loadedFonts.add(fontFamily);
// Добавляем класс для CSS transitions
document.documentElement.classList.add(`${fontFamily}-loaded`);
} catch (error) {
console.warn(`Failed to load font: ${fontFamily}`, error);
}
}
// Предзагрузка без блокировки
preloadCriticalFonts() {
this.loadFont('Heading Font', '/fonts/heading.woff2');
this.loadFont('Body Font', '/fonts/body.woff2');
}
}
// Использование
const fontLoader = new FontLoader();
fontLoader.preloadCriticalFonts();
🎨 Плавные переходы с CSS
/* Базовые стили с системным шрифтом */
body {
font-family: system-ui, -apple-system, sans-serif;
opacity: 1;
transition: opacity 0.3s ease;
}
/* Когда кастомные шрифты загружены */
.fonts-loaded body {
font-family: 'Body Font', system-ui, sans-serif;
}
.fonts-loaded h1, .fonts-loaded h2, .fonts-loaded h3 {
font-family: 'Heading Font', system-ui, sans-serif;
}
/* Плавное появление */
.fonts-loaded .content {
animation: smoothFontLoad 0.5s ease;
}
@keyframes smoothFontLoad {
from { opacity: 0.8; }
to { opacity: 1; }
}
Интеграция с Performance Observer:
// Мониторинг загрузки шрифтов
const fontObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(`Font loaded: ${entry.name}`, {
duration: entry.duration.toFixed(2),
size: entry.transferSize
});
// Оптимизация на основе метрик
if (entry.duration > 1000) {
this.enableAggressiveCaching(entry.name);
}
});
});
fontObserver.observe({ entryTypes: ['font'] });
// Отслеживание LCP с шрифтами
const lcpObserver = new PerformanceObserver((list) => {
const lcpEntry = list.getEntries().at(-1);
if (lcpEntry?.element?.computedStyle?.fontFamily?.includes('Custom Font')) {
console.log('LCP uses custom font');
}
});
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
5. Оптимизация форматов и кэширования
📦 Форматы шрифтов 2025
- WOFF2 — основной формат (на 30% меньше WOFF)
- WOFF — fallback для старых браузеров
- Variable Fonts — один файл вместо нескольких начертаний
- COLRv1 — для цветных шрифтов (поддержка растёт)
Современная конфигурация @font-face:
/* Variable Font - современный подход */
@font-face {
font-family: 'Modern Variable Font';
src: url('font.woff2') format('woff2-variations');
src: url('font.woff2') format('woff2') tech('variations');
font-weight: 100 900;
font-stretch: 75% 125%;
font-display: swap;
font-style: normal;
}
/* Статические fallbacks для совместимости */
@font-face {
font-family: 'Modern Static';
src: url('font-regular.woff2') format('woff2');
font-display: swap;
font-weight: 400;
}
@font-face {
font-family: 'Modern Static';
src: url('font-bold.woff2') format('woff2');
font-display: swap;
font-weight: 700;
}
Оптимизация кэширования:
# nginx конфигурация
location ~* \.(woff2|woff)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Vary "Accept-Encoding";
# Brotli сжатие для шрифтов
brotli_static on;
gzip_static on;
# CORS для CDN
add_header Access-Control-Allow-Origin "*";
}
# Cloudflare Rules 2025
# Browser Cache TTL: 1 year
# Edge Cache TTL: 1 month
# Cache Level: Cache Everything
6. Инструменты мониторинга и анализа
📊 Web Vitals + RUM
// Real User Monitoring для шрифтов
function monitorFontPerformance() {
const observer = new PerformanceObserver((list) => {
const fonts = list.getEntriesByType('font');
fonts.forEach(font => {
// Отправка метрик в аналитику
sendToAnalytics('font_load', {
name: font.name,
duration: font.duration,
size: font.transferSize,
cls: getCLS() // Cumulative Layout Shift
});
});
});
observer.observe({ entryTypes: ['font'] });
}
// CLS мониторинг для FOIT
let clsValue = 0;
const clsObserver = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (!entry.hadRecentInput) {
clsValue += entry.value;
if (clsValue > 0.1) {
triggerFontOptimization();
}
}
});
});
clsObserver.observe({ entryTypes: ['layout-shift'] });
🔍 Lighthouse CI конфигурация
// .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v10
with:
configPath: './lighthouse-config.js'
uploadArtifacts: true
temporaryPublicStorage: true
- name: Font Performance Check
run: |
lhci autorun --config=./lighthouse-config.js
// lighthouse-config.js
module.exports = {
ci: {
assert: {
assertions: {
'font-display': ['warn', {minScore: 0.8}],
'preload-fonts': ['error', {minScore: 0.9}],
'unused-css-rules': ['warn', {maxLength: 10}]
}
}
}
};
Заключение
- Используйте
font-display: swapдля основного текста иblockдля заголовков - Всегда предзагружайте критические шрифты с
<link rel="preload"> - Применяйте метрические override'ы для минимизации CLS
- Используйте Variable Fonts для уменьшения количества файлов
- Внедряйте Font Loading API для продвинутого контроля
- Мониторьте производительность шрифтов через RUM



