Ниже приводится гостевой пост Деклана Река, который работает разработчиком в De Voorhoede, техническом агентстве переднего плана. Версия этой статьи изначально была опубликована в их блоге. Деклан спросил о переиздании здесь, и, поскольку он битком набит реальными примерами из практики, я был в этом заинтересован.

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

Кстати, недавно мы обновили наш сайт. Помимо полной переделки конструкции, это была идеальная возможность повысить производительность до максимума. Наша цель состояла в том, чтобы взять под контроль, сосредоточиться на производительности, быть гибкими в будущем и сделать написание контента для нашего сайта увлекательным. Вот как мы улучшили производительность интерфейса для нашего сайта. Наслаждаться!

#Дизайн для производительности

В наших проектах мы ежедневно обсуждаем с дизайнерами и владельцами продукта баланс между эстетикой и производительностью. Для нашего собственного сайта это было легко. Мы считаем, что хороший пользовательский опыт начинается с максимально быстрой доставки контента. Это означает производительность › эстетику.

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

#Сначала контент

Мы хотим предоставить нашим посетителям основной контент (текст с необходимым HTML и CSS) как можно быстрее. Каждая страница должна поддерживать основную цель контента: донести сообщение. Улучшения, то есть JavaScript, полный CSS, веб-шрифты, изображения и аналитика, уступают основному контенту.

"#"Взять под контроль

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

#Генератор статических сайтов

Мы написали собственный генератор статических сайтов на Node.js. Для создания полной структуры сайта со всеми его активами требуются файлы Markdown с короткими метаописаниями страниц JSON. Он также может сопровождаться файлом HTML для включения JavaScript для конкретной страницы.

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

Мета-описание JSON:

{
  "keywords": ["performance", "critical rendering path", "static site", "..."],
  "publishDate": "2016-08-12",
  "authors": ["Declan"]
}

И файл уценки:

# A case study on boosting front-end performance
At [De Voorhoede](https://www.voorhoede.nl/en/) we try to boost front-end performance...
## Design for performance
In our projects we have daily discussions...

#Доставка изображения

Средняя веб-страница — это колоссальные 2406 КБ, из которых 1535 КБ — изображения. С изображениями, занимающими такую ​​большую часть среднего веб-сайта, это также одна из лучших целей для выигрыша в производительности.

#ВебП

WebP — это современный формат изображений, который обеспечивает превосходное сжатие без потерь и с потерями для изображений в Интернете. Изображения WebP могут быть значительно меньше изображений других форматов: иногда они на 25% меньше, чем их аналог JPEG. WebP часто упускают из виду и редко используют. На момент написания, поддержка WebP ограничена Chrome, Opera и Android (все еще более 50% наших пользователей), но мы можем изящно перейти на JPG/PNG.

Элемент #‹картинка›

Используя элемент изображения, мы можем изящно перейти от WebP к более широко поддерживаемому формату, такому как JPEG:

<picture>
  <source type="image/webp" srcset="image-l.webp" media="(min-width: 640px)">
  <source type="image/webp" srcset="image-m.webp" media="(min-width: 320px)">
  <source type="image/webp" srcset="image-s.webp">
  <source srcset="image-l.jpg" media="(min-width: 640px)">
  <source srcset="image-m.jpg" media="(min-width: 320px)">
  <source srcset="image-s.jpg">
  <img alt="Description of the image" src="image-l.jpg">
</picture>

Мы используем picturefill by Scott Jehl для полифилла в браузерах, не поддерживающих элемент ‹picture›, и для обеспечения согласованного поведения во всех браузерах.

Мы используем ‹img› как запасной вариант для браузеров, не поддерживающих элемент ‹picture› и/или JavaScript. Использование самого большого экземпляра изображения гарантирует, что оно по-прежнему хорошо выглядит в резервном сценарии.

#Создать

Хотя подход к доставке изображений уже существовал, нам все еще нужно было придумать, как его безболезненно реализовать. Мне нравится элемент изображения за то, что он может делать, но я ненавижу писать фрагмент выше. Особенно, если мне нужно включить его при написании контента. Мы не хотим возиться с созданием 6 экземпляров каждого изображения, оптимизацией изображений и записью элементов изображения в нашей уценке. Итак, мы:

  • генерировать несколько экземпляров исходных изображений в процессе сборки как во входном формате (JPG, PNG), так и в WebP. Для этого мы используем gulp responsive.
  • уменьшить сгенерированные изображения
  • напишите ![Описание изображения](http://placehold.it/350x150.png) в наших файлах уценки.
  • используйте пользовательские написанные средства визуализации Markdown во время процесса сборки, чтобы компилироватьобычные объявления изображений Markdown в полноценные элементы ‹picture›.

#SVG-анимации

Для нашего сайта мы выбрали особый графический стиль, в котором SVG-иллюстрации играют главную роль. Мы сделали это по нескольким причинам.

  • Во-первых, SVG (векторные изображения), как правило, меньше растровых изображений;
  • Во-вторых, SVG по своей природе отзывчивы и отлично масштабируются, всегда оставаясь очень четкими. Таким образом, нет необходимости в генерации изображений и элементах «картинка»;
  • И последнее, но не менее важное: мы можем анимировать и изменять их с помощью CSS! Прекрасный пример дизайна для производительности. Все страницы нашего портфолио имеют сделанный на заказ анимированный SVG, который повторно используется на обзорной странице. Он служит повторяющимся стилем для всех элементов нашего портфолио, что делает дизайн согласованным, но очень мало влияет на производительность.

Посмотрите на эту анимацию и на то, как мы можем изменить ее с помощью CSS.

#Пользовательские веб-шрифты

Прежде чем углубиться, вот краткое руководство по поведению браузера в отношении пользовательских веб-шрифтов. Когда браузер встречает определение @font-face в CSS, указывающее на шрифт, недоступный на компьютере пользователя, он пытается загрузить этот файл шрифта. Пока происходит загрузка, большинство браузеров не отображают текст с использованием этого шрифта. Вообще. Это явление называется «Вспышка невидимого текста» или FOIT. Если вы знаете, что искать, вы найдете это почти везде в Интернете. И если вы спросите меня, это плохо для конечного пользователя. Это задерживает пользователя в достижении его основной цели: чтении контента.

Однако мы можем заставить браузер изменить свое поведение на «Flash of Unstyled Content» или FOUT. Сначала мы говорим браузеру использовать вездесущий шрифт, например Arial или Georgia. После загрузки пользовательского веб-шрифта он заменит стандартный шрифт и перерисует весь текст. Если пользовательский шрифт не загружается, содержимое по-прежнему прекрасно читается. Хотя некоторые могут посчитать это запасным вариантом, мы рассматриваем пользовательские шрифты как улучшение. Даже без него сайт выглядит нормально и работает на 100%.

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

#Поднастройка шрифта

Поднастройка — это самый быстрый способ улучшить производительность веб-шрифтов. Я бы порекомендовал его каждому веб-разработчику, использующему собственные шрифты. Вы можете сделать все возможное с подмножеством, если у вас есть полный контроль над содержимым и вы знаете, какие символы будут отображаться. Но даже просто подстановка вашего шрифта на западные языки окажет огромное влияние на размер файла. Например, наш шрифт Noto Regular WOFF, который по умолчанию имеет размер 246 КБ, уменьшается до 31 КБ при подмножении на западные языки. Мы использовали Генератор веб-шрифтов Font squirrel, который очень прост в использовании.

#Наблюдатель за шрифтом

Обозреватель шрифтов от Bram Stein — отличный вспомогательный скрипт для проверки загрузки шрифтов. Он не зависит от того, как вы загружаете свои шрифты, будь то через службу веб-шрифтов или размещая их самостоятельно. После того, как скрипт наблюдателя шрифтов уведомит нас о том, что все пользовательские веб-шрифты загружены, мы добавляем класс fonts-loaded к элементу ‹html›. Мы оформляем наши страницы соответствующим образом:

html {
  font-family: Georgia, serif;
}
html.fonts-loaded {
  font-family: Noto, Georgia, serif;
}

Примечание. Для краткости я не включил объявление @font-face для Noto в CSS выше.

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

В ближайшем будущем нам, вероятно, не понадобится JavaScript Брэма Штейна, чтобы получить такое поведение. Рабочая группа CSS предложила новый дескриптор @font-face (называемый font-display), где значение свойства определяет, как загружаемый шрифт отображается до его полной загрузки. Оператор CSS font-display: swap; даст нам то же поведение, что и описанный выше подход. Подробнее о свойстве font-display.

#Отложенная загрузка JS и CSS

Вообще говоря, у нас есть подход к загрузке активов как можно скорее. Мы устраняем запросы, блокирующие рендеринг, и оптимизируем для первого просмотра, используя кеш браузера для повторных просмотров.

#Ленивая загрузка JS

По дизайну на нашем сайте не так много JavaScript. Для того, что у нас есть и что мы собираемся использовать в будущем, мы разработали рабочий процесс JavaScript.

JavaScript в ‹head› блокирует рендеринг, а нам это не нужно. JavaScript должен только улучшать взаимодействие с пользователем; для наших посетителей это не критично. Простой способ исправить блокировку рендеринга JavaScript — поместить скрипт в хвост вашей веб-страницы. Недостатком является то, что он начнет загрузку скрипта только после загрузки всего HTML.

Альтернативой может быть добавление сценария в заголовок и отсрочка выполнения сценария путем добавления атрибута defer к тегу ‹script›. Это делает скрипт неблокирующим, так как браузер загружает его практически сразу, не выполняя код до загрузки страницы.

Остается только одно: мы не используем такие библиотеки, как jQuery, и поэтому наш JavaScript зависит от функций ванильного JavaScript. Мы хотим загружать JavaScript только в браузерах, поддерживающих эти функции (т.е. нарезку горчицы). Конечный результат выглядит так:

<script>
// Mustard Cutting
if ('querySelector' in document && 'addEventListener' in window) {
  document.write('<script src="index.js" defer><\/script>');
}
</script>

Мы помещаем этот небольшой встроенный скрипт в заголовок нашей страницы, определяя, поддерживаются ли функции ванильного JavaScript document.querySelector и window.addEventListener. Если это так, мы загружаем скрипт, записывая тег script непосредственно на страницу, и используем атрибут defer, чтобы сделать его неблокирующим.

#Отложенная загрузка CSS

На первый взгляд, самым большим ресурсом блокировки рендеринга для нашего сайта является CSS. Браузеры откладывают рендеринг страницы до тех пор, пока не будет загружен и проанализирован полный файл CSS, указанный в ‹head›. Такое поведение является преднамеренным, иначе браузеру пришлось бы пересчитывать макеты и перерисовывать все время во время рендеринга.

Чтобы предотвратить блокировку рендеринга CSS, нам нужно асинхронно загрузить файл CSS. Мы используем замечательную функцию loadCSS от Filament Group. Это даст вам обратный вызов при загрузке файла CSS, где мы устанавливаем файл cookie, указывающий, что CSS загружен. Мы используем этот файл cookie для повторяющихся просмотров, о чем я расскажу чуть позже.

Существует одна «проблема» с асинхронной загрузкой в ​​CSS: хотя HTML-код отображается очень быстро, он будет выглядеть как обычный HTML без применения CSS, пока не будет загружен и проанализирован полный CSS. Вот тут-то и появляется критический CSS.

#Критический CSS

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

Ручное определение этого критического CSS — процесс, требующий много времени, особенно во время будущих изменений стиля. Есть несколько отличных скриптов для создания критического CSS в процессе сборки. Мы использовали великолепный critical npm module by Addy Osmani.

См. ниже нашу домашнюю страницу, отрендеренную с помощью критически важного CSS и отрендеренную с использованием полного CSS. Обратите внимание на сгиб, где ниже сгиба страница все еще не стилизована.

"#"Сервер

Мы сами размещаем сайт de Voorhoede, потому что хотели контролировать серверную среду. Мы также хотели поэкспериментировать, как повысить производительность, изменив конфигурацию сервера. В настоящее время у нас есть веб-сервер Apache, и мы обслуживаем наш сайт через HTTPS.

# Конфигурация

Чтобы повысить производительность и безопасность, мы провели небольшое исследование того, как настроить сервер.

Мы используем стандартную конфигурацию Apache H5BP, которая является отличным началом для повышения производительности и безопасности вашего веб-сервера Apache. У них есть конфигурации и для других серверных сред.

Мы включили GZIP для большей части HTML, CSS и JavaScript. Мы аккуратно устанавливаем заголовки кэширования для всех наших ресурсов. Об этом читайте ниже в разделе кэширование на файловом уровне.

# HTTPS

Обслуживание вашего сайта через HTTPS может повлиять на производительность вашего сайта. Ухудшение производительности в основном связано с настройкой подтверждения SSL, что приводит к большой задержке. Но — как всегда — мы можем что-то с этим сделать!

HTTP Strict Transport Security — это HTTP-заголовок, который позволяет серверу сообщать браузеру, что с ним следует обмениваться данными только по протоколу HTTPS. Таким образом, он предотвращает перенаправление HTTP-запросов на HTTPS. Все попытки доступа к сайту по HTTP должны автоматически преобразовываться. Это спасает нас от поездки туда и обратно!

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

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

Я звучу как инженер DevOps, но я им не являюсь. Я просто кое-что читал и смотрел видео. Мне понравилась статья Эмили Старк Разрушение мифов о HTTPS: разрушение городских легенд безопасности с Google I/O 2016.

#Использование файлов cookie

У нас нет серверного языка, только статический веб-сервер Apache. Но веб-сервер Apache по-прежнему может выполнять включения на стороне сервера (SSI) и считывать файлы cookie. Разумно используя файлы cookie и предоставляя HTML, частично переписанный Apache, мы можем повысить производительность внешнего интерфейса. Возьмите этот пример ниже (наш реальный код немного сложнее, но сводится к тем же идеям):

<!-- #if expr="($HTTP_COOKIE!=/css-loaded/) || ($HTTP_COOKIE=/.*css-loaded=([^;]+);?.*/ && ${1} != '0d82f.css' )"-->
<noscript><link rel="stylesheet" href="0d82f.css"></noscript>
<script>
(function() {
  function loadCSS(url) {...}
  function onloadCSS(stylesheet, callback) {...}
  function setCookie(name, value, expInDays) {...}
  var stylesheet = loadCSS('0d82f.css');
  onloadCSS(stylesheet, function() {
    setCookie('css-loaded', '0d82f', 100);
  });
}());
</script>
<style>/* Critical CSS here */</style>
<!-- #else -->
<link rel="stylesheet" href="0d82f.css">
<!-- #endif -->

Логика на стороне сервера Apache — это строки комментариев, начинающиеся с ‹! — #. Давайте рассмотрим это шаг за шагом:

  • $HTTP_COOKIE!=/css-loaded/ проверяет, не существует ли файл cookie кэша CSS.
  • $HTTP_COOKIE=/.*css-loaded=([^;]+);?.*/ && ${1} != ‘0d82f.css’ проверяет, не является ли кэшированная версия CSS текущей версией.
  • Если
  • Для первого представления мы добавляем тег ‹noscript› с блокировкой рендеринга ‹link rel="stylesheet"›. Мы делаем это, потому что мы будем загружать полный CSS асинхронно с JavaScript. Если бы JavaScript был отключен, это было бы невозможно. Это означает, что в качестве запасного варианта мы загружаем CSS «по номерам», т.е. блокирующим образом.
  • Мы добавляем встроенный скрипт с функциями отложенной загрузки CSS, onloadCSScallback и устанавливаем куки.
  • В том же скрипте мы асинхронно загружаем полный CSS.
  • В обратном вызове onloadCSS мы устанавливаем файл cookie с хэшем версии в качестве значения файла cookie.
  • После скрипта мы добавляем встроенную таблицу стилей с критическим CSS. Это будет блокировка рендеринга, но она будет очень маленькой и не позволит странице отображаться как простой HTML без стилей.
  • ‹! — Оператор #else → (означающий, что файл cookie присутствует, загруженный css) представляет повторяющиеся просмотры посетителя. Поскольку мы можем до некоторой степени предположить, что файл CSS загружен ранее, мы можем использовать кеш браузера и обслуживать таблицу стилей блокирующим образом. Он будет обслуживаться из кеша и загружаться практически мгновенно.

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

#Кэширование на уровне файлов

Поскольку мы сильно зависим от кеширования браузера для повторяющихся просмотров, нам нужно убедиться, что мы кешируем правильно. В идеале мы хотим кэшировать активы (CSS, JS, шрифты, изображения) навсегда, аннулируя кеш только тогда, когда файл действительно изменяется. Кэш становится недействительным, если URL-адрес запроса уникален. Мы помечаем наш сайт git, когда выпускаем новую версию, поэтому самым простым способом было бы добавить параметр запроса для запроса URL-адресов с базовой версией кода, например `https://www.voorhoede.nl/assets/css/main. css?v=1.0.4`. Но.

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

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

"#"Результат

Если вы зашли так далеко (круто!), Вы, вероятно, хотите узнать результат. Протестировать производительность вашего сайта можно с помощью таких инструментов, как PageSpeed ​​Insights для очень практических советов и WebPagetest для расширенного сетевого анализа. Я думаю, что лучший способ проверить производительность рендеринга вашего сайта — это наблюдать за развитием вашей страницы, безумно ограничивая ваше соединение. Это означает: дросселировать, вероятно, нереальным образом. В Google Chrome вы можете ограничить свое соединение (через вкладку ИнспекторСеть) и посмотреть, как медленно загружаются запросы, пока ваша страница создается.

Итак, посмотрите, как загружается наша домашняя страница при ограниченном GPRS-соединении со скоростью 50 КБ/с.

Обратите внимание, как мы получаем первый рендеринг со скоростью 2,27 с в сети GPRS со скоростью 50 КБ/с, представленный первым изображением из диафильма и соответствующей желтой линией на виде водопада. Желтая линия рисуется сразу после загрузки HTML. HTML-код содержит важные CSS-коды, благодаря которым страница выглядит удобной для использования. Все остальные блокирующие ресурсы загружаются лениво, поэтому мы можем взаимодействовать со страницей, пока остальные загружаются. Это именно то, что мы хотели!

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

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

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

Тестирование нашего сайта с помощью инструментов, упомянутых ранее, также показало хорошие результаты. PageSpeed ​​Insights дает нам 100/100 баллов за мобильную производительность, как здорово?!

Когда мы смотрим на WebPagetest, мы получаем следующий результат:

Мы видим, что наш сервер работает хорошо, и что SpeedIndex для первого просмотра равен 693. Это означает, что нашу страницу можно использовать через 693 мс при кабельном соединении. Хорошо смотритесь!

"#"Дорожная карта

Мы еще не закончили и постоянно совершенствуем наш подход. В ближайшее время мы сосредоточимся на:

  • HTTP/2: он уже здесь, и в настоящее время мы экспериментируем с ним. Многие вещи, описанные в этой статье, являются рекомендациями, основанными на ограничениях HTTP/1.1. Вкратце: HTTP/1.1 восходит к 1999 году, когда макеты таблиц и встроенные стили были просто потрясающими. HTTP/1.1 никогда не предназначался для веб-страниц размером 2,6 МБ с 200 запросами. Чтобы облегчить проблемы нашего бедного старого протокола, мы объединяем JS и CSS, встраиваем важные CSS, используем URL-адреса данных для небольших изображений и так далее. Все для сохранения запросов. Поскольку HTTP/2 может выполнять несколько запросов параллельно по одному и тому же TCP-соединению, вся эта конкатенация и сокращение запросов могут даже оказаться антипаттерном. Мы перейдем на HTTP/2, когда закончим эксперименты.
  • Service Workers: это современный браузерный JavaScript API, работающий в фоновом режиме. Он включает множество функций, которые раньше были недоступны для веб-сайтов, таких как автономная поддержка, push-уведомления, фоновая синхронизация и многое другое. Мы играем с Service Workers, но нам все еще нужно реализовать это на нашем собственном сайте. Я гарантирую вам, мы будем!
  • CDN: Итак, мы хотели контролировать и сами разместили сайт. Да-да, и теперь мы хотим перейти на CDN, чтобы избавиться от сетевых задержек, вызванных физическим расстоянием между клиентом и сервером. Хотя наши клиенты в основном базируются в Нидерландах, мы хотим охватить всемирное сообщество пользователей таким образом, чтобы он отражал то, что мы делаем лучше всего: качество, производительность и продвижение Интернета.

Спасибо за чтение! Пожалуйста, посетите наш сайт, чтобы увидеть конечный результат. У вас есть комментарии или вопросы? Дайте нам знать через Твиттер. И если вам нравится создавать быстрые веб-сайты, почему бы не присоединиться к нам?