Русская страница живёт примерно на сотне знаков: тридцать три буквы алфавита в двух регистрах, десять цифр, кавычки, тире и знак номера в адресе компании. При этом подключённая к сайту гарнитура тащит за собой тысячи глифов для десятков письменностей, и каждое начертание весит около трёхсот килобайт. Получается перекос: один процент рабочих символов занимает девяносто девять процентов файла. Сабсет наводит порядок, из полной гарнитуры вырезают только нужные кодовые точки, и триста килобайт превращаются в тридцать с небольшим. Инструмент для вырезки бесплатный, команда умещается в одну строку, вся настройка занимает один вечер. Ниже разбор с командами pyftsubset, диапазонами юникода, правилом @font-face и цифрами экономии.
Полный шрифт несёт тысячи глифов, а странице нужна лишь сотня
Современная интерфейсная гарнитура собирается сразу для всего мира. В одном файле живут латиница, кириллица, греческий алфавит, турецкие диакритики, вьетнамская латиница с надстрочными знаками, а местами арабская вязь и деванагари. К буквам прилагаются стрелки, математические операторы, символы валют, лигатуры, альтернативные формы, малые прописные и ординалы. Гарнитура среднего размера хранит полторы-три тысячи глифов, крупные многоязычные коллекции переваливают за пять тысяч. Каждый глиф расходует байты на контуры, хинтинг для мелких кеглей и записи в таблицах позиционирования, а качественная гарнитура держит ещё тысячи кернинговых пар, чтобы сочетания букв не разъезжались.
Потом работает умножение. Семейство из regular, medium, bold и курсива занимает четыре-шесть файлов, и каждый несёт полную копию алфавитного хозяйства. Вариативная версия с осями веса и ширины без вырезки тяжелее одиночного статического начертания. Хинтинг добавляет десятки килобайт на файл, хотя на экранах с плотной пиксельной сеткой эти инструкции почти не работают, а платят за них все посетители сайта.
Арифметика страницы скромнее. Русский текст обходится тридцатью тремя буквами в двух регистрах, десятью цифрами, с десятком знаков препинания, кавычками, тире и валютой, в сумме около сотни кодовых точек. Из файла на три тысячи глифов это три процента содержимого, остальные девяносто семь остаются балластом, который загружается с телефона при первом заходе. Типичное TTF одного начертания весит 200-400 КБ, конвертация в woff2 без вырезки оставляет 80-150 КБ, а семейство из четырёх файлов набирает больше мегабайта в исходном формате и под полмегабайта в сжатом. Нередко шрифты оказываются самым тяжёлым ресурсом страницы после картинок.
Диапазоны юникода для русского текста с латиницей и пунктуацией
Сабсет строится от списка кодовых точек, поэтому первым делом фиксируют, что именно должен уметь файл. Базовая рамка для русскоязычного проекта с латинскими вставками выглядит так:
- основная латиница U+0020-007E с пробелом, цифрами и знаками препинания;
- блок U+00A0-00FF с неразрывным пробелом, угловыми кавычками U+00AB и U+00BB, знаком копирайта и градусом;
- ядро кириллицы U+0400-045F со всеми буквами русского алфавита, включая Ё U+0401 и ё U+0451, а заодно с украинскими і, ї, є и белорусской ў;
- хвост кириллицы U+0460-04FF с историческими литерами, который нужен редким проектам, и украинская ґ из U+0490-0491 по потребности;
- общая пунктуация U+2010-2027 с неразрывным дефисом, коротким и длинным тире, типографскими кавычками и многоточием;
- одиночные знаки U+2116, U+2122, U+20AC и U+20BD, то есть номер, товарный знак, евро и рубль.
Рамка служит стартом, а не догмой. Точный состав подсказывает сам сайт: из базы выгружают тексты меню, кнопок, форм, подсказок, слоганов, отзывов и юридических страниц, собирают уникальные символы и сверяют со списком. Отдельно проходят футер и мелкий шрифт, там встречаются параграф U+00A7 и зарегистрированный знак U+00AE, оба попадают в блок U+00A0-00FF автоматически, отдельной строки не требуют. Запас оставляют сознательно: акционные надписи, новые категории и неожиданные символы в комментариях стоят дешевле, чем починка выпавшего знака на работающем проекте. На практике расширенный список добавляет к файлу пять-десять килобайт, зато экономит часы разбора писем о кривых кавычках.
Установка fontTools и первая команда генерации сабсета
Всю работу делает открытая библиотека fontTools и её консольная утилита pyftsubset. Ставится одной строкой:
pip install fonttools brotli
Модуль brotli нужен для упаковки результата в woff2, без него генерация доходит до финального шага и падает с ошибкой сжатия. Дальше сама команда для гарнитуры с кириллицей, латиницей и базовой пунктуацией:
pyftsubset Inter-Regular.ttf --unicodes="U+0020-007E,U+00A0-00FF,U+0400-045F,U+0460-04FF,U+2010-2027,U+2116,U+2122,U+20AC,U+20BD" --flavor=woff2 --layout-features="*" --output-file="inter-cyr-lat.woff2"
Параметр --unicodes принимает диапазоны и одиночные точки через запятую, именно здесь решается судьба каждого символа. Ключ --flavor=woff2 включает упаковку в современный формат. Параметр --layout-features="*" сохраняет все типографские функции гарнитуры: кернинг, лигатуры, локальные формы, малые прописные, если они есть. Ключ --output-file задаёт имя результата. Утилита заодно вычищает ставшие ненужными таблицы, меньше глифов, короче таблицы подстановок и позиционирования, в готовом файле остаются сотни глифов вместо тысяч, и на этой разнице строится вся экономия.
Субсет собирают из исходного TTF или OTF, из которых собирали сайт, а не из уже упакованного woff2, потому что повторная распаковка и упаковка отнимает часть качества сжатия. Вместо ручного списка диапазонов утилита умеет читать контент: параметр --text-file="site-text.txt" принимает файл с реальными текстами и сам собирает из него уникальные символы. Надёжнее комбинировать оба способа: символы складываются, и ни один знак с готовых страниц не теряется.
Формат woff2 и сохранение кернинга с лигатурами в параметрах pyftsubset
Выбор формата закрывает вторую половину задачи. Отправлять в браузер TTF или OTF незачем: woff2 жмёт контуры современным алгоритмом brotli и выигрывает у старого woff примерно треть объёма, а поддержка формата давно есть во всех актуальных браузерах. Основную экономию даёт сама вырезка, формат добивает остаток.
Дальше тонкая настройка. Звёздочка в --layout-features="" страхует типографику: по умолчанию утилита сохраняет лишь минимальный набор функций, и без явного указания легко потерять лигатуры или редкие подстановки. Ключ --no-hinting вырезает хинтинг и снимает ещё несколько килобайт, на плотных экранах разницы не видно, а вот на старых офисных мониторах мелкий текст без хинтинга слегка расплывается, поэтому решение зависит от аудитории. Для гарнитур на движке CFF есть ключ --desubroutinize, он раскрывает подпрограммы контуров, чтобы сжатие работало эффективнее. Параметр --name-IDs="" сохраняет записи таблицы имён, включая лицензионную пометку, и юристы спокойны почти без затрат веса.
Правило @font-face с unicode-range и деление шрифта на подмножества
Сабсет из предыдущих разделов даёт один файл на всё. Более тонкая схема делит гарнитуру на подмножества: латиницу и кириллицу пакуют отдельно, а браузеру перечисляют диапазоны через unicode-range прямо в правиле @font-face.
@font-face {
font-family: "Inter";
src: url("/fonts/inter-latin-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0020-007E, U+00A0-00FF;
}
@font-face {
font-family: "Inter";
src: url("/fonts/inter-cyr-400.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0400-045F, U+0460-04FF, U+2116, U+20BD;
}
Логика браузера прямая: перед загрузкой он смотрит на текст, который должен показать, собирает использованные кодовые точки и качает только файлы с пересекающимися диапазонами. Страница с латинскими цифрами и аббревиатурами возьмёт один лёгкий файл на 10-15 КБ, русская страница подтянет и кириллический, вместе они дадут 35-40 КБ на начертание против трёхсот у полного файла. Оба блока носят одно имя семейства и один вес, переключение происходит посимвольно, швов на экране не видно.
Резервные гарнитуры перечисляют списком в свойстве font-family после фирменного имени, unicode-range не отменяет порядок, он лишь решает, качать ли файл вообще. Скачок при подмене шрифта лечат метриками: значение swap у font-display сразу показывает текст резервной гарнитурой, а когда фирменный файл приехал, меняет начертание. Чтобы строки не прыгали, резерву подгоняют геометрию через size-adjust, ascent-override и descent-override, тогда текст стоит на месте до и после подмены.
Подводные камни сабсета и поиск выпавших символов
Главный подвох вырезки молчалив. Браузер не сообщает об отсутствующем глифе, он тихо подставляет символ из системной гарнитуры, и в строке вырастает инородная буква с чужим рисунком и чужим кернингом. Такие дефекты редко замечает команда, зато отлично замечают читатели.
Классика жанра связана с угловыми кавычками. В юникоде они живут не в кириллице, а в блоке U+00A0-00FF, и узкий сабсет из одного кириллического ядра теряет их первым делом. Вторая беда прячется в букве ё: точки U+0401 и U+0451 лежат внутри ядра U+0400-045F, но ручной диапазон U+0410-044F их отрезает, и слово с ё в заголовке отрисовывается чужим шрифтом. Знак номера U+2116 и рубль U+20BD не входят ни в один базовый блок, их добавляют в список точечно. Длинное тире U+2014, многоточие U+2026 и неразрывный пробел U+00A0 раскиданы по разным блокам, одна забытая позиция выливается в выпавший знак на рабочем сайте. Отдельная засада кроется в интерфейсных строках: маска телефона, подсказки в формах и кнопки содержат скобки, плюс и проценты, которых нет в текстах статей, поэтому их тоже добавляют в файл для --text-file.
Проверяют результат не глазами, а списком. Собирают тестовую страницу со всеми знаками из рамки и смотрят её после каждой пересборки. Техническая проверка идёт через ttx из того же пакета fontTools:
ttx -t cmap inter-cyr-lat.woff2
Утилита выпишет рядом XML файл со списком кодовых точек результата, и поиск по нему сразу покажет, доехал ли знак номера до финального файла. Гигиена пересборки простая: каждое начертание прогоняют своей командой, курсив забывают чаще всего, при обновлении гарнитуры до новой версии семейство пересобирают целиком, а имена файлов меняют с каждым релизом, чтобы кэш браузеров не раздавал старые копии.
Сборка всего семейства и реальная экономия в килобайтах и секундах
Ручной запуск для каждого начертания утомителен, поэтому семейство собирают циклом:
for style in Regular Italic Medium Bold; do
pyftsubset Inter-$style.ttf --unicodes="U+0020-007E,U+00A0-00FF,U+0400-045F,U+0460-04FF,U+2010-2027,U+2116,U+2122,U+20AC,U+20BD" --flavor=woff2 --layout-features="*" --output-file="inter-$style.woff2"
done
Для деления на подмножества команду запускают дважды с разными диапазонами и именами результата, в PowerShell цикл оформляют через foreach, суть не меняется.
Теперь цифры. Семейство из четырёх начертаний в исходном TTF весит около 1200 КБ, полные woff2 занимают примерно 440 КБ. Сабсеты кириллицы с латиницей дают 35-40 КБ на начертание, вся четвёрка укладывается примерно в 150 КБ. Самое интересное происходит на отдельной странице: русскому тексту нужны regular и bold, и вместо 600 КБ полных исходников браузер грузит 70-80 КБ сабсетов. При мобильном канале 10 Мбит/с это разница между половиной секунды ожидания и шестью сотыми. Страница без кириллицы при делении на подмножества качает только лёгкий латинский файл и экономит ещё больше.
Остаётся обвязка. Критичный кириллический файл заказывают заранее:
<link rel="preload" href="/fonts/inter-cyr-400.woff2" as="font" type="font/woff2" crossorigin>
Латиницу предзагружать не стоит, страница с русским текстом возьмёт оба файла и так. Шрифты меняются редко, поэтому раздачу держат с длинным кэшем и пометкой неизменности, повторные визиты обходятся без запросов к серверу.
Выигрыш получается не только в секундах. Первый экран перестаёт ждать тяжёлый файл, текст отрисовывается сразу, метрики скорости растут, а поисковые системы учитывают их при ранжировании. До и после сверяют в панели браузера: суммарный вес шрифтов, число запросов и момент, когда текст получает фирменное начертание. Сабсет работает как та же гигиена, что и сжатие картинок: разовая настройка на вечер и эффект на каждом визите каждого посетителя. Шрифт после вырезки делает ровно то же, что и раньше: показывает текст. Разница одна: за показ платят килобайты, а не сотни килобайт.