От автора: в 2017 мы сделали свои 2 продукта веб-оптимизации — Mirage и Rocket Loader – еще быстрее! Вместе эти продукты ускоряют примерно 1.2 миллиарда веб-страниц в неделю. Обоим проектам примерно 5 лет, поэтому был большой шанс обновить их под современный мир браузеров, HTTP2 и современные JS инструменты. Мы измерили прирост производительности, и с очень грубыми подсчетами мы экономим посетителям сайтов в нашей сетки примерно 50-700ms. Исследование Google показало, что посетители, которые видят контент быстрее, намного сильнее увлечены им и реже уходят с сайта. Это действительно радует, особенно когда каждый год экономится 380 лет загрузки и ошеломляющая цифра в 1.03 петабайт данных. Поэтому вопрос о том, как ускорить загрузку страниц, всегда будет актуален.
Что делают Mirage и Rocket Loader
Mirage и Rocket Loader оптимизируют загрузки веб-страниц путем уменьшения и откладывания количества файлов, которые браузеру необходимо запросить для завершения парсинга HTML и рендера на экран.
Mirage
С Mirage пользователи на слабых мобильных соединениях будут быстро видеть полную страницу контента, используя плейсхолдер изображения маленького веса, которые загружаются намного быстрее. Без Mirage пользователям на слабом мобильном соединении придется долго ждать загрузки изображений высокого качества. Так как это займет много времени, для пользователей ваш сайт будет казаться медленным:
С Mirage посетители будут быстрее видеть контент, поэтому им будет казаться, что контент загружается быстро, что сократит число уходов с сайта.
Rocket Loader
Браузеры не будут показывать контент, пока не загрузится и не запустится весь JS, который на него влияет. Так пользователи могут довольно долго ждать, пока вообще что-то загрузится, даже если они пришли на сайт только ради этого контента! Rocket Loader прозрачно откладывает выполнение всего JS до загрузки остальной части страницы. Это позволяет браузеру максимально быстро показывать контент, который хотят пользователи.
Как они работают
Оба продукта работают по двухэтапному процессу: сначала наш оптимизирующий прокси-сервер переписывает способ доставки HTML клиента, после чего наш встроенный JS пытается оптимизировать загрузку страниц. Например, сервер Mirage переписывает теги изображений следующим образом:
1 2 3 4 5 |
<!-- before --> <img src="/some-image.png"> <!-- after --> <img data-cfsrc="/some-image.png" style="display:none;visibility:hidden;"> |
Браузеры не понимают data-cfsrc, поэтому JS Mirage может контролировать весь процесс загрузки этих изображений. Он использует эту возможность для умной загрузки плейсхолдер изображений на слабых соединениях.
Rocket Loader использует похожий подход для снижения приоритета JS во время загрузки страницы, позволяя браузеру максимально быстро показывать контент страницы.
Проблемы
JS для обоих продуктов был написан много лет назад, когда rollup принес плохой образ жизни, а не отличный билд-инструмент. С большими изменениями, которые мы наблюдали в браузерах, протоколах и JS, было много возможностей по оптимизации.
Динамически… замедляя
Разработанные для экосистемы того времени, оба продукта загружались с помощью загрузчика Cloudflare asynchronous-module-definition (AMD), который назывался CloudflareJS и включал еще несколько общих библиотек.
Поэтому процесс загрузки Mirage и Rocket Loader был таким:
Серверный рерайтер вставлял CFJS в блокирующий тег script
CFJS запускался и просматривал некоторые настройки на странице, решая во время выполнения, загружать ли Rocket/Mirage через AMD, вставляя новые теги script
Rocket/Mirage загружались и запускались
Борьба с браузерами
Динамическая загрузка означала, что продукты не смогут пользоваться оптимизациями, которые есть в современных браузерах. Теперь браузеры сканируют HTML, так как он доходит до них, и им не приходится ждать, идентифицируя и загружая как можно быстрее внешние ресурсы типа теги скриптов. Процесс называется preload scanning – одна из важнейших оптимизаций, выполняемых браузером. Так как мы использовали динамический код внутри CFJS для загрузки Mirage и Rocket Loader, мы тем самым заблокировали их для прелоад сканнера.
Хуже того, Rocket Loader динамически вставлялся с помощью злодея DOM API, document.write – техника, которая сильно понижает производительность. Поняв, почему все именно так, я создал схему. Посмотрите на нее и вернитесь после следующего параграфа:
Как я сказал, вставка скриптов через document.write сильно бьет по производительности загрузки страниц. document.write невидим для прелоад сканнера (даже если скрипт встроенный, а наш нет, прелоад сканнер даже не пытается просканировать JS), поэтому когда вставляется тег скрипта браузер уже будет занят поиском ресурсов, которые сканнер нашел в другом месте на странице (другие теги скриптов, изображения и т.д.). Это имеет значение, потому что браузер, столкнувшийся с неотсроченным или асинхронным JS, как Rocket Loader, должен блокировать все дальнейшее построение дерева DOM до тех пор, пока этот скрипт не будет загружен и не запущен, чтобы он мог изменить DOM. Таким образом, Rocket Loader вставлялся в тот момент, когда он был очень медленным для загрузки из-за отставания запросов от прелоад сканнера, что вызывало большие задержки, прежде чем парсер DOM мог продолжить работу!
Помимо этой катастрофы с производительностью, нас подгоняло к удалению document.write то, что Chrome начал вмешиваться в 55 версии, что вызвало очень интересные обсуждения. Это вмешательство иногда мешало Rocket Loader вставляться в медленные 2G соединения, останавливая загрузку любого другого JS!
Было ясно, что от document.write необходимо было избавляться!
Неиспользуемый и слишком общий код
CFJS был общей библиотекой для клиентского кода Cloudflare, в том числе для оригинального магазина приложений Cloudflare. Это означало, что у нее большой набор API. Mirage и Rocket Loader зависели от них, но перекрытие было небольшим. Когда мы запустили новый Cloudflare Apps, у CFJS больше не было других важных продуктов, зависящих от него.
План действий
До присоединения к Cloudflare в июле прошлого года я работал в TypeScript – язык на всем любимом новом синтаксисе современного JS. Принятие множества проектов на AMD, ES5 с помощью Gulp и Grunt шокировало меня. Я реально думал, что написал свою последнюю
1 |
define(['writing', 'very-bug'], function(twice, prone) {}) |
, но в 2017 все вернулось!
Поэтому было очень заманчиво все перелопатить и вернуться к новым игрушкам ECMAScript. Однако я так часто переписывал код, что знал, что это редко оправдывается. Поэтому было решено определить наивысший приоритет, необходимый для повышения производительности (стоит отметить, что я написал несколько веток git checkout -b typescript-version).
План был такой:
Определить и встроить части CFJS, используемые Mirage и Rocket Loader
Создать новую версию других зависимостей CFJS (наш виджет с логотипом был захардкоден на CloudflareJS)
Перейти с AMD на Rollup (а значит и на синтаксис импорта ECMAScript)
Решение не создавать новую общую библиотеку может быть довольно неожиданным, особенно в том случае, если переделка позволяет избежать больших накладных расходов неиспользуемого кода в зависимостях. Однако небольшое дублирование было меньшим злом по сравнению с зависимостями между проектами, учитывая что:
Перекрытие в коде было небольшим
CFJS в первую очередь стала слишком большой из-за более общих, библиотечных функций
У Rocket Loader в будущем будет кое-что интересное…
Выжимать килобайты из минифированных + сжатых JS файлов – пустая трата времени для большинства приложений. Однако в случае с кодом, который за время чтения этой статьи запустится буквально миллионы раз, это окупается. Мы продолжим это делать в 2018.
Уход от AMD
Уход от Gulp, Grunt и AMD был довольно механическим процессом замены синтаксиса:
1 2 3 4 5 6 |
define(['cloudflare/iterator', 'cloudflare/dom'], function(iterator, dom) { // ... return { Mirage: Mirage, }; }) |
На ECMAScript модули, готовык к Rollup:
1 2 3 4 5 6 |
import * as iterator from './iterator'; import { isHighLatency } from './connection'; // ... export { Mirage } |
Пострефакторное взвешивание
После того, как части CFJS, используемые проектами, были встроены в сами проекты, Rocket и Mirage стали немножко больше (все цифры минифированной версии + GZip):
Так мы добились значительной экономии размера файла (около половины jQuery) по сравнению с исходным размером, необходимым для полной загрузки любого продукта.
Новый поток вставки
Раньше поток вставки выглядел так:
1 2 3 4 5 |
// on page embed, injected into customers' pages <script> var cloudflare = { rocket: true, mirage: true }; </script> <script src="/cloudflare.min.js"></script> |
В cloudflare.min.js мы нашли динамический код, который после запуска запускает запросы для Mirage и Rocket Loader:
1 2 3 4 |
// cloudflare.min.js if(cloudflare.rocket) { require(“cloudflare/rocket”); } |
Теперь наш подход более browser friendly:
1 2 3 4 5 6 |
// on page embed <script> var cloudflare = { /* some config */ } </script> <script src="/mirage.min.js"></script> <script src="/rocket.min.js"></script> |
Если сравнить новую схему последовательности вставки, то сразу видно, почему она лучше:
Измерения
Теория подразумевала, что наша меньшая стратегия, ориентированная на браузер, должна быть быстрее, но убедиться в этом мы могли только после старых-добрых эмпирических измерений.
Чтобы измерить результаты, я создал тестовую страницу (в том числе Bootstrap, кастомные шрифты, пара изображений, текст) и вычислил изменение средних показателей производительности Lighthouse из 100 по нескольким прогонам. Я сосредоточился на данных:
Время, необходимое на первую значимую отрисовку (TTFMP) – FMP – это когда мы сначала видим полезный контент, например, изображения и текст
В целом – это совокупный балл Lighthouse для страницы – чем ближе к 100, тем лучше
Наблюдаемый средний балл Lighthouse (максимально 100) для FMP и общей производительности
Наблюдаемый средний TTFMP в ms
Оценка
Улучшение показателей по всем направлениям! Видно, что измерения привели к существенным улучшениям, например, к сокращению среднего времени до первой значимой отрисовки в 694ms для Rocket и 49ms для Mirage.
Заключение
Оптимизация Mirage и Rocket Loader привела к сокращению использования сети и улучшению производительности для пользователей оптимизированных сайтов Cloudflare.
Автор: Tim Ruffles
Источник: //blog.cloudflare.com/
Редакция: Команда webformyself.