От переводчика: Это восьмая статья из цикла о Node.js от команды Mozilla Identity, которая занимается проектом Persona.
- "Охотимся за утечками памяти в Node.js"
- "Нагружаем Node под завязку"
- "Храним сессии на клиенте, чтобы упростить масштабирование приложения"
- "Производительность фронтэнда. Часть 1 — конкатенация, компрессия, кэширование"
- "Пишем сервер, который не падает под нагрузкой"
- "Производительность фронтэнда. Часть 2 — кешируем динамический контент с помощью etagify"
- "Приручаем конфигурации веб-приложений с помощью node-convict"
Мы смогли уменьшить объем шрифтов для Persona на 85%, с 300 до 45 килобайт, используя подмножества шрифтов. Эта статья рассказывает о том, как именно мы это сделали, и какие мы использовали инструменты.
Представляем connect-fonts
Connect-fonts — это middleware для Connect, которое улучшает производительность @font-face
, раздавая клиентам подобранные специально для их языка подмножества шрифтов, уменьшая тем самым их размер. Connect-fonts также генерирует специфические для локали и браузера стили @font-face
и CORS-заголовки для Firefox и IE9+. Для раздачи подмножеств шрифтов создаются так называемые font packs — поддиректории с подмножествами шрифтов плюс простой конфигурационный файл JSON. Некоторые наборы распространённых open source-шрифтов доступны в готовом виде в пакете npm, впрочем, создавать свои пакеты совсем нетрудно.
Если вы не слишком хорошо ориентируетесь в работе со шрифтами в интернете, мы собрали небольшую коллекцию ссылок по теме @font-face
. [От переводчика: а на Хабре очень кстати статья, посвящённая производительности веб-шрифтов]
Статическая и динамическая загрузка шрифтов
Когда вы просто отдаёте один большой шрифтовой файл всем пользователям, всё довольно просто:
- пишете блок
@font-face
в вашем файле CSS; - генерируете шрифтовые файл для веб из исходника в TTF или OTF и кладёте в директорию, к которой есть доступ у веб-сервера;
- настраиваете заголовки CORS, если ссылаетесь на шрифты с другого домена, так как политика одного источника Firefox и IE9+ распространяется и на шрифты.
Это несложные шаги. Первые два можно сделать ещё легче с помощью, например, FontSquirrel — онлайн генератора шрифтов, который автоматически создаст файлы шрифтов и код CSS. Для добавления заголовков CORS придётся почитать документацию Apache или Nginx, но и это не слишком трудно.
Но если вы хотите использовать все преимущества подмножеств шрифтов, всё усложняется. Вам нужны будут отдельные файлы шрифтов для каждой локали, и будет нужно динамически менять декларации @font-face
, чтобы они указывали на правильные URL. Так же придётся разбираться и с CORS. Именно эти проблемы решает connect-fonts.
Подмножества шрифтов: обзор
По умолчанию, каждый шрифт содержит множество символов — латиницу, диакритические знаки для таких языков как французский или немецкий, дополнительные алфавиты, такие как греческий или кириллица. Многие шрифты также содержат символы, особенно если поддерживают Юникод (например, символ снеговика — ☃). Есть и шрифты с поддержкой иероглифов. Всё это содержится в файле шрифта, чтобы он был полезен максимально широкой аудитории. Такая гибкость ведёт к большим размерам файлов. Microsoft Arial Unicode, который содержит все символы Uncode 2.1, весит невероятные 22 мегабайта!
В то же время типичная веб-страница использует щрифт для одной конкретной задачи — отображения контента, обычно на одном языке и без экзотических символов. Ограничивая шрифт этим небольшим подмножеством, мы можем очень сильно сэкономить.
Выигрыш в производительности при использовании подмножеств шрифтов
Давайте сравним размеры полных файлов распространённых шрифтов с подмножествами для нескольких локалей. Даже если ваш сайт работает только на английском языке, вы можете очень сильно сэкономить, выдавая локализованное подмножество.
Меньший размер файлов шрифтов означает более быструю загрузку и появление шрифтов на экране. Это особенно важно для мобильных устройств. Если пользователю доведётся зайти на сайт через 2G-соединение, уменьшение веса страницы на 50 килобайт может ускорить загрузку на 2-3 секунды. Ещё один момент — кеш в мобильных устройствах часто невелик, и у маленького шрифта больше шансов в нём задержаться.
Сравнение размеров полного шрифта Open Sans Regular с подмножествами для разных локалей:
То же самое в сжатом gzip виде:
Даже с учётом сжатия можно уменьшить размер шрифта на 80%, с 63 до 13 килобайт, в случае использования только английского подмножества Open Sans. И это только один шрифт — большинство сайтов используют несколько. Огромный потенциал для оптимизации!
Используя connect-fonts, мы в Mozilla Persona добились уменьшения на 85%, c 300 до 45 килобайт. Это эквивалентно паре секунд времени загрузки на типичном 3G-соединении и десятку секунд на 2G.
Дальнейшие оптимизации
Если вы хотите избавиться от каждого лишнего байта, можно настроить connect-fonts так, чтобы он возвращал CSS не в виде файла, а виде строки, которую можно будет включить в общий файл. Кроме того, connect-fonts по умолчанию генерирует минимальную декларацию @font-face
, не включая в неё файлы в форматах, которые не принимает конкретный браузер.
Пример использования connect-fonts в приложении
Предположим, что у нас есть простое приложение на Express, которое сообщает клиенту текущее время:
// app.js
const
ejs = require('ejs'),
express = require('express'),
fs = require('fs');
var app = express.createServer(),
tpl = fs.readFileSync(__dirname, '/tpl.ejs', 'utf8');
app.get('/time', function(req, res) {
var output = ejs.render(tpl, {
currentTime: new Date()
});
res.send(output);
});
app.listen(8765, '127.0.0.1');
используя самый примитивный шаблон:
// tpl.ejs
<!doctype html>
<p>the time is <%= currentTime %>
Подключим connect-fonts, чтобы отдавать локализованное подмножество Open Sans — одного из нескольких включённых в пакеты готовых наборов.
Изменения в приложении
Сначала установим нужные пакеты:
$ npm install connect-fonts
$ npm install connect-fonts-opensans
Подключим middleware:
// app.js с изменениями для использования connect-fonts
const
ejs = require('ejs'),
express = require('express'),
fs = require('fs'),
// добавлено:
connect_fonts = require('connect-fonts'),
opensans = require('connect-fonts-opensans');
var app = express.createServer(),
tpl = fs.readFileSync(__dirname, '/tpl.ejs', 'utf8');
Инициализируем модуль:
// продолжение app.js
// добавим этот вызов app.use:
app.use(connect_fonts.setup({
fonts: [opensans],
allow_origin: 'http://localhost:8765'
})
connect_fonts.setup()
принимает следующие аргументы:
fonts
— массив необходимых шрифтовallow_origin
— источник, из которого берём шрифты; connect-fonts использует эту информацию для установки заголовкаAccess-Control-Allow-Origin
для браузеров Firefox 3.5+ и IE9+.ua
(необязательный) — список user-agents, которым мы отдаём шрифты. По умолчанию connect-fonts определяет user-agent по запросу, самостоятельно решая, какие форматы файлов ключить в декларацию. Это поведение может быть изменено, если передатьua: 'all'
— тогда будут включены все имеющиеся форматы.
Внутри маршрута нужно передать локаль в шаблон:
// продолжение app.js
app.get('/time', function(req, res) {
var output = ejs.render(tpl, {
// передаём локаль в шаблон:
userLocale: detectLocale(req),
currentTime: new Date()
});
res.send(output);
});
Для определения локали Mozilla Persona использует i18n-abide. Есть ещё очень неплохой модуль locale, оба они доступны через npm. Но, чтобы не сложнять пример, мы просто возьмем первые два символа из поля запроса Accept-language
:
// примитивное определение локали
function detectLocale(req) {
return req.headers['accept-language'].slice(0,2);
}
app.listen(8765, '127.0.0.1');
// конец app.js
Изменения в шаблоне
Теперь нужно обновить шаблон. Connect-fonts предполагает, что маршруты прописаны в виде:
/:locale/:font-list/fonts.css
например:
/fr/opensans-regular,opensans-italics/fonts.css
В нашем случае надо добавить ссылку на файл стилей по тому пути, который ожидает connect-fonts:
// tpl.ejs - изменения для connect-fonts
<!doctype html>
<link href="/<%= userLocale %>/opensans-regular/fonts.css" rel="stylesheet">
и добавить в код страницы стиль для использования шрифта:
// продолжение tpl.ejs
<style>
body { font-family: "Open Sans", "sans-serif"; }
</style>
<p>the time is <%= currentTime %>
Вот и всё. CSS, созданный connect-fonts, зависит от локали пользователя и его браузера. Вот пример для английской локали:
* это пример вывода при свойстве ua установленном в 'all' */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: url('/fonts/en/opensans-regular.eot');
src: local('Open Sans'),
local('OpenSans'),
url('/fonts/en/opensans-regular.eot#') format('embedded-opentype'),
url('/fonts/en/opensans-regular.woff') format('woff'),
url('/fonts/en/opensans-regular.ttf') format('truetype'),
url('/fonts/en/opensans-regular.svg#Open Sans') format('svg');
}
Если вы хотите сэкономить один HTTP запрос, вы можете использовать метод connect_fonts.generate_css()
, чтобы получать код CSS в виде строки, и включить её в общий файл CSS.
Теперь наше маленькое приложение красиво показывает время. Его полный исходный код доступен на Гитхабе. Мы использовали готовый font pack, но создать собственный совсем не трудно. Инструкция есть в readme.
Зключение
Подмножества могут дать очень большой выигрыш в производительности для сайтов, которые используют веб-шрифты. connect-fonts делает немало работы со шрифтами в многоязычном приложении. Даже если ваш сайт подерживает только один язык, вы всё равно можете использовать этот модуль чтобы урезать шрифты до единственной необходимой вам локали и автоматически генрировать CSS и заголовки CORS, плюс это облегчит последующую интернационализацию.
Дальнейшие оптимизации могут состоять в исключении хинтинга для платформ, которые его не поддерживают (всех кроме Windows), автоматическом сжатии шрифтов и установке заголовков для кеширования.
Продолжение следует...
Автор: ilya42