Загружаем веб-шрифты асинхронно

Vlad Poe
High Technologies Center
6 min readJan 18, 2017

Материал является переводом статьи Брэма Стейна.

Типографика — важная часть дизайна, и нет ничего удивительного в том, что использование веб-шрифтов стало общераспространенной практикой как только браузеры начали их поддерживать. Согласно данным HTTP Archive, в ноябре 2016 около 65% сайтов использовали веб-шрифты. И это впечатляет.

Но у данной тенденции есть оборотная сторона: с точки зрения представления и доступности, веб-шрифты превратились в “бутылочное горлышко” веба. Причина — способ их обработки большинством браузеров. Текст на странице скрывается до тех пор, пока шрифт не будет полностью загружен, что лишает пользователя доступа к желаемому контенту. Проблема становится критичной при слабом сигнале сети, когда текст продолжает быть недоступным вплоть до нескольких секунд. Этот эффект получил название Flash Of Invisible Text (FOIT, “вспышка невидимости текста”), и он даже стал причиной определенного негатива по отношению к веб-шрифтам среди некоторых разработчиков.

В этом есть своя правда. Скрытый текст — проблема, и использование только системных шрифтов — абсолютно допустимое решение. Не каждому сайту требуются кастомные шрифты, они не всегда соответствуют бюджету на разработку, и они никогда они не будут загружаться так же быстро, как системные. Это нормально.

Однако проблема FOIT, это не проблема веб-шрифтов. Это проблема Firefox, Chrome, Opera, Safari и других браузеров, которые выбрали такой алгоритм их загрузки. Microsoft Internet Explorer и Edge пошли другим путем. Они показывают текст, оформленный фолбек-шрифтом, немедленно при загрузке и до тех пор, пока не будут загружены кастомные шрифты вашей страницы. Эффект подмены шрифта при такой модели был назван Flash Of Unstyled Text (FOUT, “вспышка нестилизованного текста”). В сравнении с FOIT, FOUT обеспечивает гораздо лучшее пользовательское восприятие при медленном интернете: пользователи получают доступ к текстовому контенту максимально быстро, а к его оформлению— по мере загрузки нужных ресурсов. Это соответствует принципам прогрессивного улучшения. И это хорошо.

Как разработчики, мы без больших усилий можем управлять способом отображения шрифтов на наших веб-страницах. К счастью, W3C готовит внедрение нового css-свойства font-display, которое обеспечит управление загрузкой напрямую в стилях. Свойство принимает четыре значения: block, swap, fallback и optional (значение по умолчанию — auto). Block скрывает текст на время загрузки, swap покажет сначала системный шрифт, и кастомный — по окончанию загрузки. Fallback ведет себя так же, как swap, но имеет таймаут-значение: если за время таймаута кастомный шрифт не будет полностью обработан, его загрузка прекратится, на странице будет использован системный. Font-display: optional покажет стилизованный тексте только в случае, если на его загрузку потребуется не более 100 миллисекунд (например, если шрифт сохранен в кэше браузера, или соединение с сетью действительно быстрое).

Свойствоfont-display добавляется к правилу @font-face и распространяется только на указанные в нем шрифты.

@font-face {
font-family: Source Serif;
src: url(/path/to/sourceserif-regular.woff2) format("woff2"),
url(/path/to/sourceserif-regular.woff) format("woff");
font-display: swap;
}

В данный момент свойство реализовано за флагом в Chrome и Opera. Как только оно получит широкую поддержку, управление асинхронной загрузкой веб-шрифтов станет вопросом добавления одного css правила. К счастью, необходимости ждать этого момента нет. Немного JavaScript позволит вам добиться такого же эффекта в любом браузере даже сейчас. Чтобы начать, добавьте правило @font-face в css-файл. В нашем примере мы используем два правила для двух шрифтов одного семейства.

@font-face {
font-family: Source Serif;
src: url(/path/to/sourceserif-regular.woff2) format("woff2"),
url(/path/to/sourceserif-regular.woff) format("woff");
}

@font-face {
font-family: Source Serif;
src: url(/path/to/sourceserif-bold.woff2) format("woff2"),
url(/path/to/sourceserif-bold.woff) format("woff");
font-weight: bold;
}

Возможно, вы обратили внимание, что я использую только два формата: WOFF и WOFF2. Этого вполне достаточно для современных браузеров. Подробнее, почему не приветствуется “пуленепробиваемый @font-face”, — в статье Зака Летермана.

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

Нам нужно обмануть браузер и заставить его показать текст без оформления. Мы можем сделать это, указав системный шрифт по-умолчанию. Затем, по окончанию загрузки, добавить кастомный, который в этом случае будет показан моментально. Создадим два селектора: первый определит правило для шрифта по-умолчанию, второй обеспечит стилизацию текста. Для него мы используем класс fonts-loaded.

html {
/* задаем фолбек на системный шрифт*/
font-family: Georgia, serif;
}

.fonts-loaded {
/* добавляем кастомный шрифт по загрузке */
font-family: Source Serif, Georgia, serif;
}

Если вы попробуете повторить это в своем браузере, то увидите текст, оформленный системным шрифтом. Это ваша основа. Теперь асинхронно загрузим кастомный шрифт, используя для этого инструмент Font Face Observer. Font Face Observer — это небольшая библиотека с отличной кроссбраузерной поддержкой (подсказка: написал её я).

Подключим библиотеку асинхронно внутри тега <head>. Как только скрипт загрузится, мы получим две сущности FontFaceObserver: одну для обычного начертания (regular), вторую для полужирного (bold). Затем вызовем метод load для каждой сущности. Метод load возвращает промисы, которые будут исполнены после загрузки шрифтов или отменены, если она не удалась. Мы используем Promise.all, чтобы дождаться загрузки и первого и второго начертания. Как только это произошло, добавим классfonts-loaded тегу <html>.

var html = document.documentElement;
var script = document.createElement("script");
script.src = "/path/to/fontfaceobserver.js";
script.async = true;

script.onload = function () {
var regular = new FontFaceObserver("Source Serif");
var bold = new FontFaceObserver("Source Serif", {
weight: "bold"
});

Promise.all([
regular.load(),
bold.load()
]).then(function () {
html.classList.add("fonts-loaded");
});
};
document.head.appendChild(script);

Если вы повторите указанную последовательность действий для вашей страницы и симулируете медленное интернет-подключение (например, 3G), то сначала увидите системный шрифт, и после — как он подменяется кастомным. Вот оно! Мы асинхронно подключаем веб-шрифт во всех браузерах!

Решена ли проблема? — Не совсем.

Возможно вы заметили короткую вспышку видимости системного шрифта, возникающую каждый раз при перезагрузке страницы. Текст или того хуже — весь лейаут как бы прыгают: это происходит в момент замены шрифтов, так как они, скорее всего, имеют разные размеры. И это портит пользовательское восприятие! Если сравнивать отдельные символы, разница, возможно, небольшая; но она становится значительной, если мы имеем дело с целыми абзацами текста. Это основной аргумент дизайнеров и разработчиков, выступающих против асинхронной загрузки веб-шрифтов.

Чтобы визуально нивелировать этот эффект, вы можете указать для своего шрифта соответствующие размеры. Font style matcher Моники Динозауреску вам в этом поможет. Приблизительно подобрав нужное значение, примените его к селектору fonts-loaded.

.fonts-loaded {
font-family: Source Serif, Georgia, serif;
font-size: 1.21em;
}

Редко когда оба шрифта будут иметь одинаковые размеры для каждого из символов, поэтому полностью исключить скачки в верстке невозможно. Но вышеуказанная техника позволяет сделать это для большинства случаев. Скрытие текста на неопределенное время при медленном подключении — недопустимо. Однако, если шрифт сохранен в кэше браузера, если он может быть показан в течение очень короткого промежутка времени, то желательно использовать сценарий при котором текст изначально скрыт. Это в наших силах — контролировать сценарий рендеринга шрифта в зависимости от его наличия/отсутствия в кэше.

Хорошую симуляцию работы кэша обеспечивает Session Storage, и мы можем использовать его для тестирования. Session Storage работает также как и Local Storage за одним исключением: он удаляет данные по завершению сессии (например, когда мы закрываем браузер). Мы будем выставлять флаг в Session Storage после того, как наши веб-шрифты будут полностью загружены при первом посещении. Затем мы отправим запрос, чтобы проверить, установлен ли флаг. Если да, мы прибавляем класс fonts-loaded тегу <html>, имитируя стандартное поведение браузера. Если нет, загружаем шрифты асинхронно и устанавливаем флаг в Session Storage.

var html = document.documentElement;

if (sessionStorage.fontsLoaded) {
html.classList.add("fonts-loaded");
} else {
var script = document.createElement("script");
script.src = "/path/to/fontfaceobserver.js";
script.async = true;

script.onload = function () {
var regular = new FontFaceObserver("Source Serif");
var bold = new FontFaceObserver("Source Serif", {
weight: "bold"
});

Promise.all([
regular.load(),
bold.load()
]).then(function () {
html.classList.add("fonts-loaded");
sessionStorage.fontsLoaded = true;
});
};
document.head.appendChild(script);
}

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

Вы можете продолжить оптимизацию процесса подключения шрифтов, используя возможности перезагрузки (preload). Preload specification стандартизирует процедуру, как разработчику сообщить браузеру о предзагружаемых ресурсах. Это можно сделать через тег <link> или через HTTP заголовок.

<link rel="preload" href="/path/to/sourceserif-regular.woff2" as="font" type="font/woff2" crossorigin>

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

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

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

Спасибо Roel Nieskens и Zach Leatherman за редакцию статьи и полезные советы.

--

--