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

Оптимизация шрифтов продолжается с целью ускорения работы сайтов: согласно HTTP Archive, ~37% топовых сайтов используют веб-шрифты на начало 2014 года, что означает двукратный рост по сравнению с предыдущим промежутком в 12 месяцев. Конечно, для большинства это уже не сюрприз.

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

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

Но как обстоят дела со скоростью отрисовки? Не сказывается ли использование веб-шрифтов негативно на производительности?

Шрифты являются критически важным ресурсом страницы, поэтому да, их использование может повлиять на скорость показа страницы в браузере. Однако сам по себе факт использования веб-шрифтов не означает, что страница будет загружаться медленно.

Приведем небольшой список факторов, определяющих влияние веб-шрифтов на скорость загрузки страницы:

  • Количество подключенных шрифтов и степень их толщины;
  • Размер файлов используемых шрифтов;
  • Скорость передачи удаленных файлов шрифтов;
  • Время начала загрузки веб-шрифтов.

Контроль над первыми двумя пунктами находится в руках дизайнера сайта. Чем больше использовано различных шрифтов, тем больше будет запросов и переданных байтов.

Стоит уменьшить число подключенных шрифтов до минимума, в целях ускорения загрузки. Итак, первый шаг к оптимизации: используйте веб-шрифты, но старайтесь снизить их число до минимума.

Измерение задержки загрузки шрифтов

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

Техники наподобие частичного включения (font subsetting), UA-specific оптимизация и более эффективная компрессия (например, сервис Google Fonts недавно перешел на Zopfli для WOFF-ресурсов), также очень важны для оптимизации размера передаваемых файлов.

Плюс к этому, поскольку мы говорим о задержках, имеет значение расположение самого файла шрифтов – то есть CDN как нельзя лучше подходит для пользовательского кэша!

Вместо того чтобы говорить об абстрактных величинах, разберемся, сколько же на самом деле занимает скачивание файла веб-шрифта при загрузке посетителем вашего сайта?

Лучшим способом ответить на этот вопрос, является инструмент под названием Resource Timing API, который позволяет вам получать временные замеры DNS, TCP и передачи файлов для каждого шрифта. Google Fonts как раз недавно внедрили поддержку Resource Timing!

Ниже приведен сниппет кода, замеряющего задержки шрифтов в Google Analytics:

// проверка: поддерживает ли Resource Timing бразуер посетителя
 if (typeof window.performance == 'object') {
   if (typeof window.performance.getEntriesByName == 'function') {
    function logData(name, r) {
     var dns = Math.round(r.domainLookupEnd - r.domainLookupStart),
           tcp = Math.round(r.connectEnd - r.connectStart),
         total = Math.round(r.responseEnd - r.startTime);
     _gaq.push( 
       ['_trackTiming', name, 'dns', dns],
       ['_trackTiming', name, 'tcp', tcp],
       ['_trackTiming', name, 'total', total]
     );
   }
    var _gaq = _gaq || [];
   var resources = window.performance.getEntriesByType("resource");
   for (var i in resources) {
     if (resources[i].name.indexOf("themes.googleusercontent.com") != -1) {
       logData("webfont-font", resources[i])
     }
     if (resources[i].name.indexOf("fonts.googleapis.com") != -1) {
       logData("webfont-css", resources[i])
     }
    }
   }
 }

Вышеприведенный код замеряет основные параметры для UA-оптимизированных CSS-файлов и веб-шрифтов, определенных в нем: CSS располагается по адресу fonts.googleapis.com и кэшируется около 24 часов, а файлы шрифтов — на themes.googleusercontent.com и имеют долговременный промежуток хранения.

Теперь, давайте взглянем на все данные в целом (промежуток responseEnd — startTime) в Google Analytics для моего сайта:

промежуток responseEnd - startTime

По соображениям безопасности, Resource Timing API намеренно не предоставляет информацию об извлеченных из кэша шрифтах, но мы, тем не менее, можем, усреднив, использовать в своей практике величину в 20 мс. Почему столько?

Загрузка сохраненной в кэше информации не «бесплатна» с точки зрения использования ресурсов. Реальная задержка вытаскивания из кэша будет зависеть от производительности железа, но для наших целей будем использовать относительно агрессивную величину в 20 мс.

Учитывая упомянутый выше факт и основываясь на приведенной статистике посещений, получим среднее время загрузки CSS-файла, которое составит приблизительно 100 мс. При этом около 26% посетителей загружают его из локального кэша.

После этого, нам нужно загрузить требуемые файл(ы) шрифтов, что в среднем занимает <20мс – большая часть посетителей уже имеет их у себя в кэше браузера! Это отличная новость — концепция долго хранящихся и общих для всех сайтов ресурсов шрифтов работает.

Полученные результаты будут разниться в зависимости от используемых шрифтов, объема и типа трафика и от других переменных. Не нужно говорить о задержках и производительности веб-шрифтов абстрактными терминами, ведь у нас есть конкретные инструменты и API для точного измерения всех этих значений. А то, что мы можем измерить, мы можем и оптимизировать.

Ограничение по времени загрузки медленных ресурсов шрифтов

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

В этом случае, критические ресурсы – включая файлы шрифтов – могут блокировать отображение страницы, что еще больше усугубляет положение.

Чтобы успешно этому противостоять, разные браузеры используют различные механизмы:

  1. IE сразу отрисовывает текст с использованием запасного шрифта и перерисовывает его, как только будет завершена загрузка основного;
  2. Firefox ожидает загрузки оригинального файла шрифтов 3 секунды, после чего, использует запасной шрифт, а как только загрузка основного будет завершена – перерисовывает текст, используя его;
  3. Google Chrome и Safari не отрисовывают страницу до тех пор, пока загрузка шрифтов не будет завершена.

У каждого из вышеприведенных походов есть свои преимущества и недостатки и вряд ли стоит дискутировать на эту тему.

Тем не менее, большинство согласится, что отсутствие какого-либо тайм-аута в Google Chrome и Safari это не слишком разумный подход, и именно этим уже некоторое время занимается команда разработчиков Google Chrome.

Каким должно быть значение тайм-аута?

Чтобы ответить на этот вопрос, мы использовали Google Chrome для замера времени выборки шрифтов разного размера, что дало следующие результаты:

Диапазон размера шрифтов Процент 50 70 90 95 99
0KB — 10KB 5.47% 136 мс 264 мс 785 мс 1.44 с 5.05 с
10KB — 50KB 77.55% 111 мс 259 мс 892 мс 1.69 с 6.43 с
50KB — 100KB 14.00% 167 мс 882 мс 1.31 с 2.54 с 9.74 с
100KB — 1MB 2.96% 198 мс 534 мс 2.06 с 4.19 с 10+ с
1MB+ 0.02% 370 мс 969 мс 4.22 с 9.21 с 10+ с

Первая хорошая новость состоит в том, что большая часть веб-шрифтов относительно небольшого размера (<50Кб).

Вторая – загрузка большинства шрифтов завершается за промежуток времени в несколько сотен миллисекунд: установка тайм-аута в 10 секунд повлияет лишь на ~0.3% запросов загрузки шрифтов, а промежуток в 3 секунды только на ~1.1%.

Основываясь на этих данных, можно сделать заключение, что оптимальным решением для Google Chrome было бы скопировать поведение Firefox: тайм-аут после 3 секунд, затем использование запасного шрифта, а после загрузки основного – перерисовка с помощью него.

Такое поведение будет реализовано в Google Chrome M35 и, надеюсь, разработчики Safari сделают также.

Практика: инициализация запроса загрузки ресурса

Итак, мы разобрались с тем, как измерять задержку загрузки каждого ресурса, но есть еще одна переменная, о которой мы не упомянули: нужно также оптимизировать инициализацию загрузки.

С первого взгляда это может показаться очевидным, но применительно к веб-шрифтам это может оказаться достаточно нетривиальным.

Давайте разберем следующий пример:

@font-face {
   font-family: 'FontB';
   src: local('FontB'), url('http://mysite.com/fonts/fontB.woff') format('woff');
 }
 p { font-family: FontA } 
<!DOCTYPE html>
 <html>
 <head>
   <link href='stylesheet.css' rel='stylesheet'>
 <!—смотрите стили выше -->
   <style>
     @font-face {
      font-family: 'FontA';
      src: local('FontA'), url('http://mysite.com/fonts/fontA.woff') format('woff');
    }
   </style>
   <script src='application.js' />
 </head>
 <body>
 <p>Hello world!</p>
 </body>
 </html>

В этом небольшом коде много чего происходит: у нас есть внешние CSS- и JavaScript- файлы, а также inline-блок с несколькими строками CSS, включающими два объявления шрифтов.

Вопрос вот в чем: когда какой запрос шрифта будет инициирован браузером?

Давайте разберемся пошагово:

  1. Парсер документа ищет внешний файл stylesheet.css и запрос отправляется;
  2. Парсер документа обрабатывает inline-блок с CSS-кодом, в котором объявлен шрифт FontA. Мы хотим, чтобы запрос шрифта инициировался как можно раньше, насколько это возможно. Но этого не происходит. Далее мы еще вернемся к этому вопросу. А пока продолжается выполнение следующих пунктов;
  3. Парсер документа блокируется внешним скриптом: мы не можем продолжить до тех пор, пока его загрузка не будет завершена;
  4. После окончания загрузки скрипт запускается и формируется окончательное DOM-дерево, рассчитываются стили, и выполняется позиционирование блоков, после чего мы, наконец, отправляем запрос на загрузку шрифта fontA. В этот момент, мы уже можем начать отрисовку, но текст с использованием выбранного нами шрифта пока не может быть выведен, так как сам файл со шрифтом все еще загружается…

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

С одной стороны это хорошо, так как минимизирует число загрузок. С другой же стороны, это также означает, что браузер не может инициировать запрос загрузки шрифта, пока не будут построены DOM- и CSSOM— деревья, проанализировав которые выяснится, какие шрифты нужны для данной страницы.

В примере, приведенном выше, наш внешний JavaScript— файл блокирует построение DOM-дерева до тех пор, пока его (скрипта) загрузка не будет завершена и он не будет выполнен, что вносит задержку в скачивание шрифта.

Чтобы это исправить, у нас есть несколько вариантов: (a) убрать JavaScript-код, (b) добавить атрибут async для асинхронной загрузки скрипта (если это возможно) или (c) переместить подключение скриптов вниз страницы.

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

Совет: в дополнение к измерению относительных задержек запросов для каждого ресурса, мы также можем измерить и проанализировать начальное время запроса с помощью Resource Timing! Отслеживание этого значения позволит нам определить, когда был произведен запрос на загрузку шрифта.

Оптимизация загрузки шрифтов в Google Chrome M33

В Google Chrome M33 был сделан важный оптимизационный шаг, который значительно улучшил производительность рендеринга шрифтов.

Чтобы понять, что конкретно было сделано, взглянем на временную шкалу обработки страницы в Google Chrome версии pre-M33, которая иллюстрирует это:

Google Chrome M33
  • Расчет стилей для страницы был завершен за ~840 мс;
  • Позиционирование блоков было запущено по прошествии ~1040 мс с момента начала обработки страницы, после чего, почти сразу же инициировался запрос ресурса шрифта.

Однако почему мы должны ожидать выполнения позиционирования, если расчет стилей уже произведен двумя сотнями миллисекунд ранее?

Так как мы знаем значения стилей, то можем выяснить, загрузку каких шрифтов нам необходимо инициировать прямо сейчас – это новая модель поведения в Google Chrome M33!

С первого взгляда, такой оптимизационный ход может показаться не слишком значимым, но основываясь на замерах с помощью Google Chrome промежуток между расчетом стилей и позиционированием блоков действительно больше, чем можно подумать:

Проценты 50 60 70 80 90
Время от начала расчета стилей до начала позиционирования блоков 132 мс 182 мс 259 мс 410 мс 820 мс

Запуская запрос на скачивание шрифтов немедленно после расчета первого стиля, мы экономим в среднем около 130мс, а при проценте равном 90 — экономия составляет около 800 мс!

Манипулируя этими параметрами, мы можем управлять задержками загрузки шрифтов. Как мы видели ранее, это помогает начать загрузку шрифта до того, как будет произведено позиционирование блоков, а это значит, что блокировки вывода текста происходить не будет.

Это огромное улучшение производительности!

Конечно, можно задать очевидный вопрос… Почему задержка между расчетом стилей и позиционированием блоков так велика? Начнем с Google Chrome DevTools: посмотрите на временную линейку и выявите наиболее медленные операции (к примеру, долго выполняющиеся скрипты и так далее). Затем, если чувствуете в себе запал, то наберите в адресной строке chrome://tracing чтобы увидеть, что происходит на самом деле – может быть браузер просто занят обработкой и позиционированием блоков на странице.

Оптимизация веб-шрифтов с помощью Font Load Events API

И наконец, мы подошли к самой эпичной части данной статьи: Font Load Events API. В двух словах, это API, который позволит нам управлять и определять, как и когда будут загружены шрифты – мы можем планировать загрузку шрифтов, а также определять, как и когда шрифт будет отображен и многое другое.

Если вы знакомы с JS-библиотекой Web Font Loader, то рассматривайте этот API как аналогичный, но более интегрированный в браузер:

var font = new FontFace("FontA", "url(http://mysite.com/fonts/fontA.woff)", {});
 font.ready().then(function() {
   // шрифт загружен.. можно определить собственное поведение. 
});

  font.load(); // немедленный запуск загрузки / don't block on render tree!

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

В приведенном выше примере, мы создали объект FontFace напрямую в коде JavaScript и вызвали тем самым немедленную загрузку шрифта – мы можем вставить этот сниппет в верхнюю часть нашей страницы и полностью избежать блокировки CSSOM- и DOM деревьев!

Советуем вам поэкспериментировать с этим API в Google Chrome Canary – специальной версии для разработчиков, и если все пойдет хорошо, реализовать все в стабильной версии M35.

Подведем итоги

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

Все это дает прекрасные результаты. Вопрос не в том, использовать ли веб-шрифты в ущерб производительности, а в том, чтобы их грамотно оптимизировать.

Давайте составим итоговый список этапов оптимизации:

  1. Проведите проверку использования веб-шрифтов и исключите те, от которых можно отказаться;
  2. Убедитесь, что ресурсы шрифтов оптимизированы;
  3. Сделайте замеры ресурсов шрифтов с помощью Resource Timing: с помощью пункта «measure ; optimize»;
  4. Оптимизируйте задержку передачи и время инициализации запроса на загрузку каждого шрифта;
  5. Оптимизируйте критический путь рендеринга, исключите ненужные JS-скрипты и так далее;
  6. Потратьте некоторое время на то, чтобы поэкспериментировать с Font Load Events API.

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

Перевод статьи «Optimizing Web Font Rendering Performance» был подготовлен дружной командой проекта Сайтостроение от А до Я.