Оптимизация загрузки: как избежать FOIT/FOUT и ускорить рендеринг

Оптимизация загрузки: как избежать FOIT/FOUT и ускорить рендеринг

Как оптимизировать загрузку шрифтов и избежать 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

Полезные ресурсы

Другие статьи

Самое интересное из мира технологий, полезные статьи и новости нашего агенства.
Организация CSS в 2026: BEM, Utility-first, CSS Modules, CSS-in-JS и современный CSS

Организация CSS в 2026: BEM, Utility-first, CSS Modules, CSS-in-JS и современный CSS

Полное руководство по выбору методологии организации CSS в 2026 году: сравнение подходов, примеры кода и рекомендации по выбору

Адаптивные таблицы 2026: 5 проверенных способов их создания

Адаптивные таблицы 2026: 5 проверенных способов их создания

Полное руководство по созданию адаптивных таблиц: от overflow-x до трансформации в карточки и CSS Grid.

GPT-5 Pro за 30 минут выполнила расчет скрытой симметрии Керра

GPT-5 Pro за 30 минут выполнила расчет скрытой симметрии Керра

GPT-5 Pro помогла с самой муторной частью — быстро «угадала форму» этих правил и подобрала коэффициенты так, чтобы все сходилось.

Все статьи

Остались вопросы?

Заполните форму ниже. Мы обязательно ответим вам в ближайшее время.

Оставляя заявку, вы даете согласие на обработку персональных данных и соглашаетесь с политикой конфиденциальности.