🔠 Разгоняем Google Fonts

Google Fonts - самый удобный сервис для работы с веб-шрифтами. Рассказываем, как сделать его еще лучше и быстрее!

Главным источником контента в вебе по-прежнему остается текст, значит разработчики должны уделять большое внимание его отображению на сайте. Сейчас многие проблемы веб-шрифтов остались в прошлом, и все силы брошены на увеличение их производительности и скорости рендеринга. Браузеры стандартизировали стратегии загрузки FOUT/FOIT, а новая спецификация font-display позволяет управлять ими.

Self-hosted vs Google Fonts

Самым быстрым вариантом остается хранение файлов шрифтов вместе с файлами самого сайта (self-hosted fonts).

  • Загрузка с того же источника (same origin) происходит быстрее.
  • Адрес файлов точно известен, а значит можно воспользоваться механизмами предварительной загрузки (preload).
  • Можно установить собственные директивы управления кэшированием (cache-control).
  • Меньше рисков, связанных со взаимодействием со сторонними источниками (сбои в работе, безопасность и т.д.)

Тем не менее многие разработчики предпочитают использовать Google Fonts для работы с веб-шрифтами – почему?

Действительно, удобство этого сервиса переоценить невозможно. Он предоставляет минимально необходимые настройки (а значит маленькие файлы) с учетом конкретных браузеров и платформ пользователей. Поддерживаются стратегии загрузки шрифта с помощью свойства font-display (параметр &display=swap в URL). К тому же это огромная библиотека шрифтов со свободным доступом.

Поэтому мы не спешим отказываться от Google Fonts, но можно ли улучшить их – и без того высокую – производительность?

Как разогнать Google Fonts?

Если вы используете font-display для Google Fonts, то имеет смысл асинхронно загрузить всю цепочку запросов

font-display: swap – это уже огромный шаг вперед в деле производительности, но что, если мы можем сделать еще больше?

Harry Roberts, основатель csswizardry, решил поставить эксперимент и сравнить разные способы загрузки Google Fonts. В качестве плацдарма для испытаний выступили его сайты – harry.is и домашняя страница csswizardry.com.

Harry проверил 5 техник:

  1. Загрузка шрифтов с Google Fonts по старинке, без использования font-display.
  2. Дефолтная загрузка с font-display: swap.
  3. Асинхронная загрузка файла Google Fonts.
  4. Предварительная загрузка CSS-файла с помощью preload для повышения его приоритета.
  5. Установка соединения с доменом fonts.gstatic.com с помощью preconnect.

Каждая техника является аддитивной – включает в себя все предыдущие и вносит некоторые новые улучшения. Нет смысла пробовать, например, preload сам по себе, ведь комбинация в любом случае будет работать лучше.

Тестирование

Для каждого теста Harry измерял 5 показателей:

  • First Paint (FP). Влияние на критический путь рендеринга.
  • First Contentful Paint (FCP). Скорость появления первого значимого контента – не только шрифтов.
  • First Web Font (FWF). Загрузка первого веб-шрифта.
  • Visually Complete (VC). Визуальная стабилизация макета – показатель, неэквивалентный Last Web Font (LWF).
  • Оценка Lighthouse. Разве можно воспринимать серьезно тесты без показателей Lighthouse? :)

Все тесты были проведены с использованием приватного экземпляра WebPageTest на Samsung Galaxy S4 через 3G соеднинение. К сожалению, на момент тестирования сервис был недоступен, поэтому у нас нет публичных ссылок с результатами.

Чтобы код было удобнее читать, фрагмент ссылки на Google Fonts https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400;1,700 заменен на $CSS.

По старинке

Около года назад в Google Fonts появилась очень крутая возможность – настройка стратегии загрузки шрифта. Теперь любой новый сниппет содержит параметр &display=swap, который автоматически добавляет во все правила @font-face свойство font-display: swap. Кроме swap поддерживаются значения optional, fallback и block.

Для первого теста Harry отбросил этот параметр, чтобы получить подходящую базу для сравнения.

Сниппет подключения шрифтов:

<link rel="stylesheet" href="$CSS" />

Здесь есть два ключевых момента:

  1. CSS-файл со стороннего домена загружается синхронно, а следовательно блокирует рендеринг страницы.
  2. Инструкции @font-face в файле не содержат правил font-display.

Это одно синхронное действие поверх другого – очень плохое сочетание для производительности веб-страницы.

Результаты теста без font-display

Эти результаты можно принять за исходную точку.

На обоих сайтах файл Google Fonts был единственным блокирующим рендеринг ресурсом, поэтому показатель First Paint (FP) у них одинаковый.

Lighthouse выдал одну ошибку и одно предупреждение:

  • (Ошибка) Убедитесь, что текст остается видимым во время загрузки веб-шрифтов.
  • (Предупреждение) Устраните ресурсы, блокирующие рендеринг страницы.

Первая ошибка вызвана отсутствием стратегии загрузки шрифтов (например, правила font-display). Второе предупреждение связано с синхронной загрузкой CSS-файла Google Fonts.

Отсюда начинаем двигаться дальше и вносить прогрессивные изменения.

font-display: swap

Теперь Harry вернул обратно параметр &display=swap. По сути это делает загрузку шрифтов асинхронной – браузер отображает резервный шрифт, так что пользователи не сталкиваются с мельканием невидимого текста (FOIT).

Эта стратегия становится еще лучше, если вы подберете подходящий резервный шрифт – в идеале он должен быть похожим на окончательный вариант. Ведь резкая смена Times New Roman на Open Sans вряд ли намного лучше FOIT. К счастью, есть удобный инструмент для подбора fallback-шрифтов: Font Style Matcher.

Сниппет подключения шрифтов:

<link rel="stylesheet" href="$CSS&display=swap" />
Результаты теста с font-display: swap

Блокирующие рендеринг ресурсы никуда не делись, поэтому улучшений показателя First Paint (FP) ожидать не приходится. На самом деле он даже немного просел на CSS Wizardry. Однако сразу бросается в глаза резкое улучшение First Contentful Paint (FCP) – больше чем на секунду на harry.is. При этом время загрузки первого веб-шрифта (FWF) в одном случае немного улучшилось, а в другом – наоборот. Показатель Visually Complete (VS) увеличился на 200 мс.

Lighthouse теперь выдает только одно предупреждение:

  • (Предупреждение) Устраните ресурсы, блокирующие рендеринг страницы.

Это ожидаемо, так как веб-шрифт по-прежнему определяется внутри синхронного CSS-файла. После того, как мы добились определенных улучшений, глупо останавливаться – давайте сделаем всю цепочку запросов асинхронной!

Асинхронный CSS

Асинхронная загрузка CSS-файла – это ключевой момент в улучшении производительности. Существует несколько способов этого добиться, но самый простой – это, пожалуй, трюк с media="print" от Filament Group.

Атрибут media="print" указывает браузеру, что файл стилей предназначен только для печати, поэтому его загрузка не должна блокировать рендеринг. Однако сразу после загрузки значение атрибута меняется на all – и стили применяются к самой странице.

Сниппет подключения шрифтов:

<link rel="stylesheet"
      href="$CSS&display=swap"
      media="print" 
      onload="this.media='all'" />

Этот трюк ужасно прост, но у него есть свои минусы.

Дело в том, что обычная синхронная таблица стилей блокирует рендеринг страницы, поэтому браузер назначает ей наивысший приоритет (Highest) при загрузке. А вот стилям для печати – самый низкий (Idle).

Таким образом наш асинхронный файл получает сильно заниженный приоритет загрузки (хотя приоритет скорее следует назвать правильным, но он в любом случае гораздо ниже, чем мы ожидаем).

Для примера возьмем сайт Vitamix с асинхронной загрузкой CSS файла:

Хотя Chrome может выполнять асинхронные DNS/TCP/TLS-запросы, при более медленных соединениях все некритичные запросы будут останавливаться

Браузер делает именно то, что мы ему сказали: запрашивает CSS-файлы с приоритетом стилей для печати. При 3G-соединении загрузка занимает более 9 секунд! Практически все остальные ресурсы грузятся раньше. Значит правильный шрифт появится только через 12,8 секунд после начала загрузки страницы!

К счастью, в случае с веб-шрифтами это не конец света. Мы всегда должны быть в состоянии справиться и без них, используя резервные варианты. Пользовательский шрифты – это прогрессивное усиление. Если ожидается такая длительная загрузка, нужно использовать правило font-display: optional.

Однако для обычных стилей подобные задержки неприемлемы. В том числе для стилей ниже первого экрана, ведь за 10 секунд пользователь почти наверняка прокрутит страницу.

Итак, что же с нашим тестом?

Результаты теста с асинхронной загрузкой CSS-файла

Результаты потрясающие!

Улучшение показателей First Paint и First Contentful Paint просто ошеломляюще по сравнению с предыдущими тестами. Оценка Lighthouse достигла 100 баллов.

Если говорить о критическом пути рендеринга, то это большая победа оптимизации.

Однако – и это важно – из-за хака с атрибутом media на CSS Wizardry просело время загрузки первого веб-шрифта (FWF).

Итак, асинхронный CSS – это хорошая идея, но нужно как-то решить проблему снижения приоритета.

preload

Нам нужен асинхронный запрос с высоким приоритетом – обратимся к предварительной загрузке (preload), которая уже неплохо поддерживается практически во всех современных браузерах. Объединим ее с отлично поддерживаемым print-хаком и получим лучшее от обеих техник, одновременно обеспечив фоллбэк.

Сниппет подключения шрифтов:

<link rel="preload"
      as="style"
      href="$CSS&display=swap" />

<link rel="stylesheet"
      href="$CSS&display=swap"
      media="print" 
      onload="this.media='all'" />
Результаты теста с предварительной загрузкой CSS-файла (preload)

Показатели First Paint и First Contentful Paint почти не изменились, однако время загрузки первого веб-шрифта (FWF) на CSS Wizardry уменьшилось на 600 мс!

Показатели Harry.is остались прежними. Это можно объяснить тем, что на простой и маленькой странице нет большой конкуренции между сетевыми запросами и таблица стилей для печати и без увеличения приоритета грузилась достаточно быстро.

Что касается CSS Wizardry, то ухудшение времени First Paint на 200 мс больше похоже на аномалию, так как изменение приоритета асинхронного CSS файла не должно было оказать влияния на рендеринг. Остальные же показатели существенно улучшились.

preconnect

Последняя проблема, которую нужно решить на пути к идеальной производительности, заключается в том, что CSS-файл мы получаем с одного домена (fonts.googleapis.com), а файлы шрифтов лежат на другом (fonts.gstatic.com). В сочетании с плохой связью это может привести к большим задержкам.

Google Fonts использует HTTP-заголовок для установки предварительного соединения с доменом fonts.gstatic.com:

Однако выполнение этого заголовка связано с TTFB (Time to First Byte, время до первого байта) ответа, которое может быть очень большим. Среднее значение TTFB, включая очередь запросов, DNS, TCP, TLS и серверное время, для CSS-файла Google Fonts во всех тестах составило 1406 мс. При этом среднее время загрузки самого CSS-файла – около 9,5 мс – в 148 раз меньше!

Иначе говоря, несмотря на то, что Goggle пытается установить предварительное соединение с доменом fonts.gstatic.com, это дает лишь около 10 мс форы. Этот файл привязан к задержке, а не к пропускной способности.

Реализация предварительного подключения со стороны сайта может принести гораздо большие выгоды.

Сниппет подключения шрифтов:

<link rel="preconnect"
      href="https://fonts.gstatic.com"
      crossorigin />

<link rel="preload"
      as="style"
      href="$CSS&display=swap" />

<link rel="stylesheet"
      href="$CSS&display=swap"
      media="print" 
      onload="this.media='all'" />

Мы можем визуализировать эти изменения на WebpPageTest:

Результаты теста с предварительным подключением к домену fonts.gstatic.com

Показатели First Paint и First Contentful Paint не изменились – preconnect влияет только на ресурсы, загружаемые после критического пути.

Зато время загрузки первого веб-шрифта (FWF) и показатель визуальной завершенности (VC) существенно уменьшились!

Оценки Lighthouse тоже хороши – 99 и 100.

Бонус: font-display: optional

Использование асинхронной загрузки CSS-файла и свойства font-display не позволяют избежать FOUT (мелькания неоформленного текста) или, в лучшем случае, FOFT (мелькания синтезированного текста), если вы хорошо подобрали резервный шрифт. Чтобы смягчить этот эффект, Harry попробовал подключить шрифт с опцией font-display: optional.

Этот параметр ограничивает время, в течение которого резервный шрифт может быть заменен на основной. Таким образом, если ваш веб-шрифт грузится слишком долго, то он просто не будет использован. Это позволит избежать эффекта FOUT, что обеспечивает лучший пользовательский опыт при взаимодействии с сайтом и хороший показатель Cumulative Layout Shift (сдвиг макета).

Однако эта техника плохо сочетается с асинхронной загрузкой CSS.

Когда значение атрибута media изменяется с print на all, браузер обновляет CSSOM и применяет его к DOM. В этот момент страница узнает, что ей нужны некоторые веб-шрифты, и начинается чрезвычайно малый период блокировки с мельканием невидимого текста (FOIT) на половине загрузки страницы. Еще хуже, если браузер заменит невидимый текст снова резервным, так что пользователь даже не получит преимуществ нового шрифта. В общем, это очень похоже на баг.

Выглядит это примерно вот так:

А вот видео, демонстрирующее проблему в DevTools:

Ссылка на видео

Не следует использовать font-display: optional в сочетании с асинхронной загрузкой CSS-файлов. В целом лучше иметь неблокирующий CSS с FOUT, чем ненужный FOIT.

Сравнения и визуализации

На этих замедленный видео хорошо видна разница между разными техниками загрузки Google Fonts.

harry.is

Ссылка на видео

  1. В тестах с асинхронной загрузкой CSS, preload и preconnect рендеринг начинается через 1.8 сек. Также представлен показатель First Contentful Paint.(первое отображение контента).
  2. В первых двух тестах (без font-display и с font-display: swap) рендеринг страницы начинается через 3.4 сек. В первом тесте наблюдается мелькание невидимого текста (FOIT).
  3. В последнем тесте с preconnect веб-шрифт грузится через 3.8 сек, а визуальная завершенность макета наступает через 4.4. сек.
  4. В первом тесте время первого существенного отображения (First Contentful Paint) и время загрузки первого шрифта (First Web Font) одинаковы – 4.5 сек – так как все загружается синхронно.
  5. Визульная завершенность в базовом тесте наступает через 5 сек.
  6. В тесте с асинхронной загрузкой CSS – через 5.1 сек.
  7. В тесте с font-display: swap – через 5.2 сек.
  8. В тесте с preload – через 5.3s.

CSS Wizardry

Ссылка на видео

  1. В тесте с асинхронной загрузкой CSS рендеринг начинается через 1.7 сек.
  2. В тесте с preconnect – через 1.9 сек. Показатель First Contentful Paint также равен 1.9.
  3. В тесте с preload рендеринг начинается через 2 сек, и время First Contentful Paint тоже равно 2 сек.
  4. В базовом тесте рендеринг начинается через 3.4 сек.
  5. В тесте с font-display: swap показатели FP и FCP равны 3.6 сек.
  6. Также через 3.6 сек наступает визуальная завершенность в тесте с preconnect.
  7. В базовом тесте показатель FCP составляет 4.3 сек.
  8. Также через 4.3 сек достигается визуальная завершенность в тесте с preload.
  9. Через 4.4 сек – в базовом тесте.
  10. Через 4.6 сек – в тесте с font-display: swap.
  11. Через 5 сек – в тесте с асинхронной загрузкой CSS.

Таким образом, техника с preconnect оказалась самой быстрой.

Находки

Хотя self-hosted шрифты, кажется, остаются самым лучшим решением всех проблем производительности и доступности, Google Fonts имеет свои преимущества. К тому же мы можем со своей стороны улучшить работу этого сервиса.

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

Если вы подключаете на странице другие блокирующие рендеринг ресурсы или нарушаете принципы быстрого CSS, то ваши показатели могут существенно отличаться от тестовых.

Особенно полезны будут эти оптимизации в проектах, в которых Google Fonts является одним из самых узких мест в обеспечении производительности.

Сниппет асинхронной загрузки Google Fonts

В этом фрагменте код содержится сразу несколько разных техник, но он все еще достаточно компактный и поддерживаемый:

<!-- Прогрев домена статики -->
<link rel="preconnect"
      href="https://fonts.gstatic.com"
      crossorigin />

<!-- Асинхронная предзагрузка CSS файла с высоким приоритетом -->
<link rel="preload"
      as="style"
      href="$CSS&display=swap" />

<!-- Неблокирующая загрузка CSS-файла с низким приоритетом -->
<link rel="stylesheet"
      href="$CSS&display=swap"
      media="print" onload="this.media='all'" />

<!-- Фоллбэк при отключенном JavaScript -->
<noscript>
  <link rel="stylesheet"
        href="$CSS&display=swap" />
</noscript>

Источники

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ