Оптимизация шрифтов в WordPress: Self-Hosting, Preload и Font-Display

Google Fonts кажется удобным решением: одна строка кода и на сайте красивая типографика. Но за этим удобством скрываются три серьёзные проблемы: каждый шрифт добавляет 200-500 мс к времени загрузки (DNS-поиск + TLS-рукопожатие к googleapis.com), шрифты блокируют отрисовку первого контента (FCP), а с 2022 года использование Google Fonts без согласия пользователя нарушает GDPR (решение суда Мюнхена). Решение — self-hosting шрифтов с правильной стратегией загрузки через preload и font-display.

Почему Google Fonts — это проблема

При подключении Google Fonts происходит цепочка сетевых запросов, каждый из которых добавляет задержку:

  1. DNS-lookup для fonts.googleapis.com (~50 мс)
  2. TLS-рукопожатие (~100 мс)
  3. Загрузка CSS с описанием шрифтов (~50 мс)
  4. DNS + TLS для fonts.gstatic.com (~150 мс)
  5. Загрузка woff2-файлов (~100-300 мс)

Итого: 450-650 мс до того, как текст на странице станет читаемым. Для сайтов с Core Web Vitals целью LCP < 2.5с это критичная задержка. К тому же IP-адрес посетителя передаётся в Google, что в EU трактуется как передача персональных данных без согласия.

Шаг 1: Выбор и скачивание шрифтов

Для технического блога в стиле VS Code используются два шрифта: Inter (для основного текста) и JetBrains Mono (для кода и заголовков). Скачиваем их в формате woff2 — он даёт максимальное сжатие и поддерживается всеми современными браузерами.

Через Google Webfonts Helper:

Сервис gwfh.mranftl.com позволяет скачать Google Fonts в виде готовых woff2-файлов с CSS.

  1. Выбираем шрифт (например, Inter)
  2. Отмечаем нужные начертания: 400, 500, 600, 700
  3. Скачиваем ZIP-архив
  4. Распаковываем и переименовываем файлы для ясности

Структура файлов в теме:

wp-content/themes/customcode/
└── assets/
    └── fonts/
        ├── inter-v13-latin_cyrillic-400.woff2
        ├── inter-v13-latin_cyrillic-500.woff2
        ├── inter-v13-latin_cyrillic-600.woff2
        ├── inter-v13-latin_cyrillic-700.woff2
        ├── jetbrains-mono-v18-latin_cyrillic-400.woff2
        └── jetbrains-mono-v18-latin_cyrillic-500.woff2

Важно скачивать версии с поддержкой кириллицы (latin_cyrillic), иначе русские буквы будут рендериться fallback-шрифтом.

Шаг 2: Подключение через @font-face в style.css

Добавляем правила @font-face в начало style.css. Ключевой параметр — font-display: swap, который предотвращает FOIT (Flash of Invisible Text) и снижает CLS.

/* Inter — основной шрифт */
@font-face {
    font-family: 'Inter';
    src: url('assets/fonts/inter-v13-latin_cyrillic-400.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+0400-045F, U+0490-0491;
}

@font-face {
    font-family: 'Inter';
    src: url('assets/fonts/inter-v13-latin_cyrillic-500.woff2') format('woff2');
    font-weight: 500;
    font-style: normal;
    font-display: swap;
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+0400-045F, U+0490-0491;
}

@font-face {
    font-family: 'Inter';
    src: url('assets/fonts/inter-v13-latin_cyrillic-600.woff2') format('woff2');
    font-weight: 600;
    font-style: normal;
    font-display: swap;
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+0400-045F, U+0490-0491;
}

@font-face {
    font-family: 'Inter';
    src: url('assets/fonts/inter-v13-latin_cyrillic-700.woff2') format('woff2');
    font-weight: 700;
    font-style: normal;
    font-display: swap;
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+0400-045F, U+0490-0491;
}

/* JetBrains Mono — для кода */
@font-face {
    font-family: 'JetBrains Mono';
    src: url('assets/fonts/jetbrains-mono-v18-latin_cyrillic-400.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+0400-045F, U+0490-0491;
}

@font-face {
    font-family: 'JetBrains Mono';
    src: url('assets/fonts/jetbrains-mono-v18-latin_cyrillic-500.woff2') format('woff2');
    font-weight: 500;
    font-style: normal;
    font-display: swap;
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+0400-045F, U+0490-0491;
}

Разбор ключевых параметров:

  • format(‘woff2’) — браузер сразу понимает формат, не тратит время на autodetect
  • font-display: swap — текст отображается fallback-шрифтом до загрузки кастомного, нет FOIT
  • unicode-range — браузер загружает шрифт только если на странице есть символы из этого диапазона. Экономит трафик для страниц без кириллицы

Шаг 3: Preload критических шрифтов

Даже с self-hosting шрифты начинают загружаться только после парсинга CSS. preload запускает загрузку параллельно с HTML, экономя 100-200 мс.

Добавляем в functions.php:

/**
 * Preload критических шрифтов
 */
function customcode_preload_fonts() {
    $fonts = [
        get_template_directory_uri() . '/assets/fonts/inter-v13-latin_cyrillic-400.woff2',
        get_template_directory_uri() . '/assets/fonts/inter-v13-latin_cyrillic-600.woff2',
        get_template_directory_uri() . '/assets/fonts/jetbrains-mono-v18-latin_cyrillic-400.woff2',
    ];
    
    foreach ( $fonts as $font ) {
        printf(
            '' . "\n",
            esc_url( $font )
        );
    }
}
add_action( 'wp_head', 'customcode_preload_fonts', 1 );

Прелоадятся только самые используемые начертания (Regular и Semibold для Inter, Regular для Mono). Bold и Medium подгружаются лениво — это экономит 30-40 КБ на первом экране.

Почему обязателен атрибут crossorigin:

Без crossorigin браузер загружает шрифт дважды — один раз для preload, второй раз при использовании в CSS. Это распространённая ошибка, которая удваивает трафик вместо экономии.

Шаг 4: font-size-adjust для стабильного CLS

Даже с font-display: swap происходит небольшая «дрожь» макета, когда fallback-шрифт (system-ui) заменяется на Inter — у них разный x-height. Параметр font-size-adjust выравнивает высоту строчных букв.

body {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    font-size-adjust: 0.52; /* x-height Inter */
}

code, pre, .font-mono {
    font-family: 'JetBrains Mono', 'Courier New', monospace;
    font-size-adjust: 0.51; /* x-height JetBrains Mono */
}

Как подобрать значение font-size-adjust:

Формула: x-height / font-size. Для Inter при 100px font-size x-height ≈ 52px, значит значение 0.52. Современные браузеры (Chrome 120+) также поддерживают новое свойство font-metrics-override, но font-size-adjust работает везде.

Шаг 5: Правильный fallback-стек

Fallback-шрифты должны быть максимально похожи на кастомные по метрикам. Для Inter лучшим fallback является system-ui (San Francisco на macOS, Segoe UI на Windows, Roboto на Android).

:root {
    --font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    --font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', Consolas, monospace;
}

Порядок важен: сначала кастомный шрифт, затем системные шрифты ОС (они загружены мгновенно), затем универсальные семейства.

Шаг 6: Кэширование шрифтов на 1 год

Шрифты меняются редко, поэтому их можно кэшировать агрессивно. На shared-хостинге Бегет это делается через .htaccess:

# Долгое кэширование шрифтов
<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType font/woff2 "access plus 1 year"
    ExpiresByType font/woff "access plus 1 year"
    ExpiresByType application/font-woff2 "access plus 1 year"
    ExpiresByType application/x-font-woff "access plus 1 year"
</IfModule>

<IfModule mod_headers.c>
    <FilesMatch "\.(woff2|woff)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>
</IfModule>

На Nginx-серверах (VPS) соответствующий блок в конфиге сайта:

location ~* \.(woff2|woff)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
    access_log off;
}

Директива immutable говорит браузеру не делать conditional-запросы (If-Modified-Since) — файл не изменится по этому URL.

Шаг 7: Отключение Google Fonts от плагинов и тем

Многие плагины (Elementor, Contact Form 7, Revolution Slider) и темы подключают Google Fonts самостоятельно. Эти подключения нужно отключить, иначе дублирование шрифтов сведёт на нет всю оптимизацию.

Универсальный код для отключения:

/**
 * Отключение Google Fonts во всех плагинах и темах
 */
add_filter( 'style_loader_tag', function( $html, $handle ) {
    if ( strpos( $html, 'fonts.googleapis.com' ) !== false || 
         strpos( $html, 'fonts.gstatic.com' ) !== false ) {
        return ''; // Полностью удаляем тег
    }
    return $html;
}, 10, 2 );

// Отключение Google Fonts в Elementor
add_action( 'elementor/frontend/after_enqueue_styles', function() {
    wp_dequeue_style( 'elementor-google-fonts' );
    wp_deregister_style( 'elementor-google-fonts' );
} );

// Отключение Google Fonts в Gutenberg
add_action( 'enqueue_block_assets', function() {
    global $wp_styles;
    if ( isset( $wp_styles->queue ) ) {
        foreach ( $wp_styles->queue as $handle ) {
            if ( strpos( $handle, 'font' ) !== false ) {
                wp_dequeue_style( $handle );
            }
        }
    }
} );

Проверка через DevTools:

Откройте сайт, нажмите F12 → Network → фильтр font. Если в списке есть запросы к fonts.googleapis.com или fonts.gstatic.com — значит где-то осталось подключение Google Fonts. Ищите источник по имени handle в колонке Initiator.

Шаг 8: Subset-оптимизация для многоязычных сайтов

Если сайт на нескольких языках, можно разделить шрифт на subsets и подгружать только нужные. Например, для английских страниц не нужна кириллица:

/* Latin-only subset (для английских страниц) */
@font-face {
    font-family: 'Inter';
    src: url('assets/fonts/inter-v13-latin-400.woff2') format('woff2');
    font-weight: 400;
    font-display: swap;
    unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}

/* Cyrillic subset (для русских страниц) */
@font-face {
    font-family: 'Inter';
    src: url('assets/fonts/inter-v13-cyrillic-400.woff2') format('woff2');
    font-weight: 400;
    font-display: swap;
    unicode-range: U+0400-045F, U+0490-0491;
}

Браузер сам выберет нужный файл на основе символов на странице. Это экономит 20-30 КБ на английских страницах.

Шаг 9: Variable Fonts (продвинутая оптимизация)

Variable Fonts — это один файл, содержащий все начертания (от 100 до 900 по весу). Inter Variable и JetBrains Mono доступны в таком формате, что сокращает количество HTTP-запросов с 4-6 до 1.

@font-face {
    font-family: 'Inter Variable';
    src: url('assets/fonts/Inter-Variable.woff2') format('woff2-variations');
    font-weight: 100 900;
    font-style: normal;
    font-display: swap;
}

/* Использование любых весов из одного файла */
.text-light { font-weight: 300; }
.text-regular { font-weight: 400; }
.text-medium { font-weight: 500; }
.text-semibold { font-weight: 600; }
.text-bold { font-weight: 700; }

Variable Font Inter (все веса) весит ~150 КБ, против ~120 КБ у четырёх отдельных woff2. Для сайтов с 3+ начертаниями variable fonts почти всегда выигрывают.

Шаг 10: Измерение результата

После всех оптимизаций проверяем метрики через несколько инструментов:

WebPageTest (детальный waterfall):

  • Запускаем тест на webpagetest.org с локацией, близкой к основной аудитории
  • Смотрим вкладку Waterfall: шрифты должны загружаться в первые 500 мс параллельно с HTML
  • Проверяем, что нет запросов к fonts.googleapis.com

Lighthouse / PageSpeed Insights:

  • Секция «Avoid enormous network payloads» — шрифты должны быть < 100 КБ
  • Секция «Preload Largest Contentful Paint element» — если LCP-элемент использует шрифт, он должен быть прелоажен
  • CLS должен упасть ниже 0.05

DevTools Network:

# Ожидаемые заголовки ответа для шрифтов:
Content-Type: font/woff2
Cache-Control: public, max-age=31536000, immutable
Content-Encoding: br (или gzip)

Чек-лист оптимизации шрифтов

  • ✅ Google Fonts удалены из всех подключений
  • ✅ Шрифты скачаны в woff2 формате с поддержкой кириллицы
  • ✅ Файлы лежат в assets/fonts/ темы
  • @font-face с font-display: swap и unicode-range
  • ✅ Preload критических начертаний в <head> с crossorigin
  • font-size-adjust для предотвращения CLS
  • ✅ Правильный fallback-стек (system-ui для sans, SF Mono для mono)
  • ✅ Кэширование на 1 год с immutable
  • ✅ Рассмотрен переход на Variable Fonts
  • ✅ Метрики LCP и CLS улучшены

Решение частых проблем

Шрифт не загружается (в DevTools 404)

Проверьте путь в src: url(). Относительные пути в CSS считаются от местоположения самого CSS-файла, а не от корня сайта. Если style.css в корне темы, то путь assets/fonts/font.woff2 корректен. Если CSS минифицируется и перемещается — используйте абсолютные пути через get_template_directory_uri().

FOIT (Flash of Invisible Text) — текст невидим первые секунды

Значит, font-display установлен в block или auto. Замените на swap. В редких случаях (логотипы, брендинг) можно использовать optional — если шрифт не загрузился за 100 мс, используется fallback навсегда.

FOUT (Flash of Unstyled Text) — текст «прыгает» при загрузке

Это нормально для font-display: swap, но минимизируется через font-size-adjust и правильный fallback-стек. Если прыжки критичны для UX (например, в hero-секции), используйте inline-шрифт в Critical CSS для первого экрана.

CORS-ошибка при загрузке шрифтов с CDN

Шрифты требуют CORS-заголовков. Если шрифты отдаются с другого домена (CDN), добавьте на стороне CDN заголовок:

add_header 'Access-Control-Allow-Origin' '*';

Для шрифтов * безопасен — они не содержат чувствительных данных.

Шрифт не отображается в Safari

Safari требователен к синтаксису @font-face. Убедитесь, что:

  • Используется кавычки вокруг имени шрифта: font-family: 'Inter';
  • Указан format('woff2'), а не format("woff2") (двойные кавычки иногда не работают)
  • Нет опечаток в unicode-range

Итог

Self-hosting шрифтов с preload и правильной стратегией font-display убирает 400-600 мс задержки от Google Fonts, решает проблему GDPR и значительно улучшает Core Web Vitals (особенно LCP и CLS). Вложения минимальны: 10 минут на скачивание woff2 и настройку CSS, а выгода — стабильно быстрая загрузка сайта на любом хостинге и независимость от внешних сервисов.