Шрифты: подключение и оптимизация

Сборка шрифтов в Webpack относится к обработке статических ресурсов. Начиная с Webpack 5 для этого чаще всего используются Asset Modules, позволяющие отказаться от file-loader и url-loader.

Наиболее распространённые форматы шрифтов:

Формат Назначение
WOFF Современный формат с хорошим сжатием
WOFF2 Основной формат для современных браузеров
TTF TrueType-шрифты, более тяжёлые
OTF OpenType-шрифты
EOT Устаревший формат для старых Internet Explorer

В современных проектах основной акцент делается на woff2 и иногда woff.

Структура проекта:

src/
 ├── fonts/
 │    ├── Inter-Regular.woff2
 │    ├── Inter-Regular.woff
 │    └── Inter-Bold.woff2
 │
 ├── styles/
 │    └── main.css
 │
 └── index.js

Обработка шрифтов через Asset Modules

Базовая конфигурация Webpack:

// webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /\.(woff2?|ttf|otf|eot)$/i,
                type: 'asset/resource',
            },
        ],
    },
};

Что делает asset/resource

Тип asset/resource:

  • копирует файл в папку сборки;
  • генерирует уникальное имя;
  • возвращает URL к файлу;
  • автоматически подключается в CSS.

После сборки:

dist/
 ├── fonts/
 │    └── a1b2c3d4.woff2

Настройка директории для шрифтов

По умолчанию Webpack размещает файлы в корне output-директории. Обычно шрифты выносятся отдельно.

// webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /\.(woff2?|ttf|otf|eot)$/i,
                type: 'asset/resource',
                generator: {
                    filename: 'fonts/[name][ext]',
                },
            },
        ],
    },
};

Результат:

dist/
 └── fonts/
      ├── Inter-Regular.woff2
      └── Inter-Bold.woff2

Подключение шрифтов через CSS

После настройки обработки файлов используется @font-face.

@font-face {
    font-family: 'Inter';
    src:
        url('../fonts/Inter-Regular.woff2') format('woff2'),
        url('../fonts/Inter-Regular.woff') format('woff');

    font-weight: 400;
    font-style: normal;
}

Использование:

body {
    font-family: 'Inter', sans-serif;
}

Webpack автоматически обработает пути внутри url().


Подключение нескольких начертаний

Каждое начертание подключается отдельно.

@font-face {
    font-family: 'Inter';
    src: url('../fonts/Inter-Regular.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
}

@font-face {
    font-family: 'Inter';
    src: url('../fonts/Inter-Bold.woff2') format('woff2');
    font-weight: 700;
    font-style: normal;
}

Применение:

.title {
    font-weight: 700;
}

.text {
    font-weight: 400;
}

Использование font-display

Свойство font-display управляет отображением текста во время загрузки шрифта.

Пример:

@font-face {
    font-family: 'Inter';
    src: url('../fonts/Inter-Regular.woff2') format('woff2');
    font-display: swap;
}

Значения font-display

Значение Поведение
auto Поведение браузера по умолчанию
block Текст скрывается до загрузки
swap Сначала системный шрифт, затем замена
fallback Короткий период ожидания
optional Браузер может не загружать шрифт

Наиболее популярный вариант:

font-display: swap;

Он предотвращает появление пустого текста.


Оптимальный набор форматов

Современная практика:

@font-face {
    font-family: 'Inter';
    src:
        url('../fonts/Inter-Regular.woff2') format('woff2'),
        url('../fonts/Inter-Regular.woff') format('woff');

    font-display: swap;
}

Причины:

  • woff2 обеспечивает лучшее сжатие;
  • woff нужен для более старых браузеров;
  • TTF и OTF значительно тяжелее.

Хеширование шрифтов

Для долгосрочного кеширования используется content hash.

generator: {
    filename: 'fonts/[name].[contenthash][ext]',
}

Результат:

fonts/Inter-Regular.8f3d1a2b.woff2

Преимущества

  • браузер кеширует файл надолго;
  • обновление шрифта автоматически меняет имя;
  • отсутствуют проблемы со старым кешем.

Настройка output.publicPath

При размещении проекта в подкаталоге важно корректно настроить пути.

module.exports = {
    output: {
        publicPath: '/',
    },
};

Для CDN:

output: {
    publicPath: 'https://cdn.example.com/',
}

Тогда шрифты будут загружаться с CDN.


Inline-подключение шрифтов

Webpack позволяет встраивать маленькие шрифты в bundle через Base64.

{
    test: /\.(woff2?)$/i,
    type: 'asset',
    parser: {
        dataUrlCondition: {
            maxSize: 10 * 1024,
        },
    },
}

Как это работает

Маленькие файлы:

url(data:font/woff2;base64,...)

Большие файлы:

dist/fonts/font.woff2

Недостатки inline-шрифтов

  • увеличение размера JS/CSS;
  • невозможность отдельного кеширования;
  • рост времени парсинга.

Для шрифтов inline-режим используется редко.


Предзагрузка шрифтов

Для ускорения отображения текста используется preload.

<link
    rel="preload"
    href="/fonts/Inter-Regular.woff2"
    as="font"
    type="font/woff2"
    crossorigin
>

Эффект preload

Браузер:

  • начинает загрузку раньше;
  • уменьшает FOUT;
  • ускоряет первый рендер текста.

FOUT и FOIT

FOUT

Flash of Unstyled Text.

Сначала отображается системный шрифт, затем происходит замена.

Причина:

font-display: swap;

FOIT

Flash of Invisible Text.

Текст временно скрывается до загрузки шрифта.

Причина:

font-display: block;

Современные проекты обычно предпочитают FOUT вместо FOIT.


Локальные шрифты через local()

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

@font-face {
    font-family: 'Inter';

    src:
        local('Inter'),
        local('Inter Regular'),
        url('../fonts/Inter-Regular.woff2') format('woff2');
}

Преимущества:

  • отсутствие загрузки;
  • более быстрый рендер;
  • экономия трафика.

Недостаток — различия версий шрифтов у пользователей.


Подключение шрифтов через Sass

SCSS-файл:

@font-face {
    font-family: 'Inter';

    src:
        url('../fonts/Inter-Regular.woff2') format('woff2');

    font-weight: 400;
}

Webpack-конфигурация:

module.exports = {
    module: {
        rules: [
            {
                test: /\.scss$/i,
                use: [
                    'style-loader',
                    'css-loader',
                    'sass-loader',
                ],
            },
        ],
    },
};

Минификация CSS со шрифтами

Webpack часто используется вместе с CSS minimizer.

npm install css-minimizer-webpack-plugin --save-dev

Конфигурация:

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
    optimization: {
        minimizer: [
            `...`,
            new CssMinimizerPlugin(),
        ],
    },
};

После минификации:

@font-face{font-family:Inter;src:url(fonts/inter.woff2) format("woff2")}

Генерация source maps

Во время разработки полезны source maps.

module.exports = {
    devtool: 'source-map',
};

Это облегчает отладку CSS и путей к шрифтам.


Использование Google Fonts через npm

Вместо CDN шрифты можно хранить локально.

Установка:

npm install @fontsource/inter

Подключение:

import '@fontsource/inter';

Или конкретного веса:

import '@fontsource/inter/400.css';
import '@fontsource/inter/700.css';

Преимущества локального хранения

  • отсутствие внешних запросов;
  • контроль версий;
  • работа офлайн;
  • лучшая приватность.

Оптимизация количества шрифтов

Каждый дополнительный файл:

  • увеличивает количество запросов;
  • влияет на First Contentful Paint;
  • замедляет рендер.

Рекомендуется:

  • использовать 1–2 семейства;
  • ограничивать количество weight;
  • избегать ненужных italic-версий.

Subsetting шрифтов

Subset — уменьшенная версия шрифта с ограниченным набором символов.

Пример:

  • только латиница;
  • только кириллица;
  • только цифры.

Преимущества

  • значительное уменьшение размера;
  • ускорение загрузки;
  • уменьшение нагрузки на сеть.

Unicode Range

Можно разделять шрифты по диапазонам символов.

@font-face {
    font-family: 'Inter Latin';
    src: url('../fonts/inter-latin.woff2') format('woff2');

    unicode-range:
        U+0000-00FF;
}

Для кириллицы:

@font-face {
    font-family: 'Inter Cyrillic';
    src: url('../fonts/inter-cyrillic.woff2') format('woff2');

    unicode-range:
        U+0400-04FF;
}

Браузер загружает только нужный subset.


Кеширование шрифтов

Сервер обычно настраивается так:

Cache-Control: public, max-age=31536000, immutable

При использовании content hash это безопасно.


Пример полноценной конфигурации Webpack

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                ],
            },

            {
                test: /\.(woff2?|ttf|otf|eot)$/i,
                type: 'asset/resource',

                generator: {
                    filename: 'fonts/[name].[contenthash][ext]',
                },
            },
        ],
    },

    plugins: [
        new MiniCssExtractPlugin({
            filename: 'css/[name].[contenthash].css',
        }),
    ],
};

Типичные ошибки

Неверный путь к шрифту

Ошибка:

src: url('/fonts/font.woff2');

При использовании Webpack чаще используются относительные пути:

src: url('../fonts/font.woff2');

Отсутствие crossorigin

При preload возможны ошибки CORS.

Правильно:

<link
    rel="preload"
    href="/fonts/inter.woff2"
    as="font"
    type="font/woff2"
    crossorigin
>

Использование тяжёлых форматов

Плохая практика:

TTF + OTF без WOFF2

Современная практика:

WOFF2 + WOFF

Подключение слишком большого количества весов

Проблемный набор:

100
200
300
400
500
600
700
800
900

Оптимальный вариант:

400
700

Проверка загрузки шрифтов

Во вкладке Network DevTools можно проверить:

  • размер файлов;
  • время загрузки;
  • кеширование;
  • preload;
  • MIME type.

Корректный MIME:

font/woff2

MIME-типы шрифтов

Сервер должен отдавать правильные типы.

Формат MIME
WOFF font/woff
WOFF2 font/woff2
TTF font/ttf
OTF font/otf

Неверный MIME может приводить к отказу загрузки.


Lazy loading шрифтов

Иногда второстепенные шрифты загружаются позже.

Пример:

body {
    font-family: system-ui;
}

.fonts-loaded body {
    font-family: 'Inter';
}

После загрузки:

document.documentElement.classList.add('fonts-loaded');

Подход уменьшает блокировку рендера.


Переменные шрифты

Variable Fonts содержат несколько weight внутри одного файла.

Пример:

@font-face {
    font-family: 'Inter Variable';

    src:
        url('../fonts/Inter.var.woff2')
        format('woff2-variations');

    font-weight: 100 900;
}

Использование:

.title {
    font-weight: 750;
}

Преимущества variable fonts

  • меньше файлов;
  • меньше HTTP-запросов;
  • гибкая настройка веса;
  • лучшая производительность.

Производительность и Core Web Vitals

Шрифты напрямую влияют на:

Метрика Влияние
FCP Скорость первого отображения
LCP Скорость отображения крупного контента
CLS Скачки макета
INP Косвенное влияние через рендер

Для уменьшения CLS используются:

  • fallback-шрифты похожей ширины;
  • preload;
  • font-display: swap;
  • variable fonts.

Практическая схема оптимизации

Наиболее распространённая схема:

  1. Использование woff2.
  2. Добавление woff для совместимости.
  3. font-display: swap.
  4. Preload основных шрифтов.
  5. Content hash.
  6. Ограничение количества weight.
  7. Использование variable fonts.
  8. Subsetting для разных языков.
  9. Долгосрочное кеширование.
  10. Локальное хранение вместо внешнего CDN.