
Августовский канал. Шлюз Домбровка. Фото Артур Басак
Рукописная гарнитура или скрипт — это, как правило, семейство шрифтов, выполненных от руки или имитирующих рукописный текст. Почему оптимизируем именно скрипт? За счет своей специфики, такие гарнитуры очень сильно отличаются друг от друга.
Скажем, если вы используете шрифт без засечек, с засечками или моноширинный, то подмена шрифта по умолчанию на загружаемый не будет столь заметна, как в случае со скриптами. Именно поэтому такие проблемы, как FOUC, FOIT и т.д. тут стоят очень остро. Как же можно исправить эту ситуацию?
На платформе Tispr есть возможность заключать контракты с электронной подписью. Для подписи используется гарнитура Brownie.

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

Электронная подпись контракта на платформе Tispr
Шрифт по умолчанию
Древнейшая практика в веб-типографике это указание семейства шрифтов по умолчанию. Раньше это не давало каких-то ощутимых бенефитов, пока шрифт загружался, контент вообще не рендерился (Flash of Invisible Text / FOIT). Теперь, вместе со свойством font-display, мы можем не блокировать отображение и показать текст пользователю сразу. Необходимый шрифт загрузится и произойдет подмена.

Пока загружается Brownie, мы не видим вообще текста (Flash of Invisible Text / FOIT), font-display: block
В случае, если загрузка оборвется или файл шрифта будет отсутствовать на сервере, то текст так и останется видимым на странице со шрифтом по умолчанию.
Изначально в проекте у подписи контракта не было семейства шрифтов по умолчанию, следовательно оно наследовалось от элементов по иерархии выше в CSS, а это, в свою очередь, общая для платформы гарнитура TT Norms. Т.е. визуально понять, что это электронная подпись контракта было крайне сложно.
.signature {
font-family: 'Brownie';
}
Пока загружается Brownie, мы видим TT Norms, font-display: swap
Наравне с serif, sans-serif и monospace в CSS есть возможность указать семейство шрифтов OC по умолчанию для курсивного текста — cursive (на платформе MacOS это будет гарнитура Apple Chancery, на Windows и Ubuntu — Comic Sans MS).
.signature {
font-family: 'Brownie', cursive;
}
Пока загружается Brownie, мы видим cursive (Apple Chancery), font-display: swap
Это, как минимум, уже визуально отличит текст подписи от общего текста платформы, хотя, как я уже упомянул выше, разница между Brownie и cursive кардинальная, поэтому нам надо максимально быстро подгрузить шрифт, иначе подмена будет очень заметна (Flash of Unstyled Content / FOUC).
Формат шрифта
Первый шаг к быстрой загрузке шрифта — это его формат. В свойстве src секции font-face крайне важно указать форматы в верном порядке, иначе браузер загрузит первый попавшийся формат, который сможет распознать.
Изначально, в проекте порядок для Brownie был такой:
@font-face {
font-family: Brownie;
font-display: swap;
src:
url(/static/media/brownie.eot) format('embedded-opentype'),
url(/static/media/brownie.ttf) format('truetype'),
url(/static/media/brownie.woff) format('woff'),
url(/static/media/brownie.woff2) format('woff2');
}Т.е. для большинства браузеров загружался формат TrueType. Для справки: WOFF2 не просто новее TTF и позволяет конфигурировать такие штуки, как лигатуры и т.д., он еще и значительно меньше весит:
Если мы поднимем WOFF2 выше, то первым делом браузер попытается загрузить его (EOT должен остаться первым, так как только его поддерживают старые версии IE, остальные браузеры его проигнорируют). В случае отсутствия поддержки этого формата, начнется попытка загрузки следующего в списке.
@font-face {
font-family: Brownie;
font-display: swap;
src:
url(/static/media/brownie.eot) format('embedded-opentype'),
url(/static/media/brownie.woff2) format('woff2'),
url(/static/media/brownie.woff) format('woff'),
url(/static/media/brownie.ttf) format('truetype');
}WOFF2 намного быстрее скачать, чем TTF, но все же нам надо что-то скачать. В условиях плохого интернета, переключение стилей текста все еще будет заметно. Идеально было бы вообще избежать загрузки при открытии модального окна с подписью. Идеально было бы, если бы к этому моменту шрифт уже был загружен в кэш.
Предварительная загрузка ссылок
Предварительная загрузка ссылок (Link prefetching) или предварительная загрузка контента (Preloading content) это относительно новый механизм встроенный в браузеры, который позволяет указать перечень необходимых для загрузки ресурсов веб-приложения. Браузер попытается загрузить ресурсы, когда начнется время его простоя (idle). Механизм призван улучшить производительность веб-сайтов и ускорить рендеринг веб-страниц.
Файл шрифта, по сути, тоже является ресурсом, и мы могли бы пометить его, как необходимый для предзагрузки.
<link rel="preload" href="/static/media/brownie.woff2" as="font" type="font/woff2" />Почему preload, а не prefetch? Директива preload новее и она поддерживается в Safari и Chrome — это целевые браузеры платформы Tispr. К сожалению, директива не поддерживается Firefox. С prefetch все наоборот — она не поддерживается в Safari. Еще один ключевой момент это то, что директива prefetch носит рекомендательный характер для браузера, preload — добровольно принудительный. Если проще, то preload подходит для ресурсов, которые нужны здесь и сейчас, prefetch — для ресурсов страниц, которые мы откроем позже.
Почему нельзя использовать обе директивы сразу? Это было бы ок, в случае, если бы наши целевые браузеры были только Firefox и Safari, но в случае с Chrome, который поддерживает обе директивы, предзагрузка шрифта произойдет дважды.
Если вы используете React.js (как раз то, на чем построен Tispr UI), то компонент react-helmet позволяет очень удобно конфигурировать секцию head документа HTML из любого места в JSX коде. Важно также предзагружать именно тот формат, который в нашем списке первый, т.е. WOFF2 и там и там, в противном случае можно получить двойную загрузку с разными форматами.
import React, { PureComponent, Fragment } from 'react';
import Helmet from 'react-helmet';
// ...other imports
import brownieScriptTypeface from './fonts/brownieScript.woff2';
import styles from './style.scss';
class EditContract extends PureComponent {
// ...other methods
render () {
const { contract } = this.props;
return (
<div className={ styles.edit }>
<Helmet>
<link rel="preload" href={ brownieScriptTypeface } as="font" type="font/woff2" />
</Helmet>
// ...other codeТакже очень удобно покрыть функционал react-helmet модульными тестами:
const helmet = Helmet.peek();
expect(helmet.linkTags).toEqual([
{ rel: 'preload', href: brownieScriptTypeface, as: 'font', type: 'font/woff2' },
]);CSS Font Loading API
CSS Font Loading API — это вариант для браузеров, которые не поддерживают preload. В целом, в рамках этого API нам доступно два интерфейса — FontFace и FontFaceSet.
Первый, является эквивалентом CSS правилу font-face и позволяет загрузить определенный шрифт с указанными параметрами. Второй, позволяет работать с уже описанным набором шрифтов. Благодаря этим интерфейсам мы можем добиться лучшего покрытия браузеров. CSS Font Loading API гораздо лучше поддерживается, чем директивы предварительной загрузки ресурсов.
Ключевой момент, все трюки связанные с предзагрузкой надо выполнить в компоненте страницы, после ее открытия и до загрузки модального окна с электронной подписью.
FontFaceSet.load() в нашем случае подойдет лучше, так как наша гарнитура уже имеет соответствующее font-face правило в CSS коде.
// Font Utils
const isFontFaceSupported = window.FontFace;
const isFontFaceSetSupported = document.fonts;
const rem = '16px';
export function preloadTypeface (typeface, hasCSSRule, options) {
if (hasCSSRule) {
if (isFontFaceSetSupported) {
const fontStyle = `${rem} ${typeface}`;
if (!document.fonts.check(fontStyle)) {
document.fonts.load(fontStyle).then(f => f);
}
}
} else {
if (isFontFaceSupported && isFontFaceSetSupported) {
const { path, ...other } = options || {};
const font = new window.FontFace(typeface, `url(${path})`, other);
font.load().then(fontFace => document.fonts.add(fontFace));
}
}
}
// React Component
import React, { PureComponent, Fragment } from 'react';
import { preloadTypeface } from 'web-client/src/lib/utils/font';
// ...other imports
import brownieScriptTypeface from './fonts/brownieScript.woff2';
import styles from './style.scss';
class EditContract extends PureComponent {
componentDidMount () {
preloadTypeface('Brownie', true);
}
// ...other codeЕсть еще один трюк, который помогает оптимизировать загрузку шрифта. Скажем, если бы гарнитура использовалась только для пары слов или даже букв (например для буквицы), то мы могли бы нарезать шрифт на отдельные глифы и загрузить только необходимые нам.
Подобную практику я как-то применял на своем собственном веб-сайте zubry.by, когда для заголовков использовал рукописную гарнитуру MarckScript. У Google Fonts API есть возможность нарезки шрифта по средствам параметра text в URL:
<link
href="https://fonts.googleapis.com/css?family=Marck+Script&display=swap&text=КонвертыМаркиОткрыткиПосткарты"
rel="stylesheet"
/>
zubry.by — заголовок страницы «Марки», гарнитура MarckScript
В подходах описанных выше, нет ничего сверхъестественного. Они давно известны и просты в своем использовании. При этом, эти трюки кардинально улучшают пользовательский опыт взаимодействия.
Артур Басак, 2020