Авторы: Treebo: Лакшья Ранганатх, Chrome: Адди Османи

Treebo - это сеть бюджетных отелей с самым высоким рейтингом в Индии, работающая в туристическом сегменте стоимостью 20 миллиардов долларов. Они недавно выпустили новое Progressive Web App в качестве мобильного интерфейса по умолчанию, сначала используя React, а затем перейдя на Preact в производстве.

По сравнению со своим старым мобильным сайтом они увидели увеличение времени до первой отрисовки на 70% +, улучшение времени до интерактивности на 31%. и загружается менее чем за 4 секунды через 3G для многих обычных посетителей и на их целевом оборудовании. Он был интерактивным в возрасте до 5 секунд с использованием более медленной эмуляции 3G WebPageTest в Индии.

Переход с React на Preact позволил сократить время до интерактивности на 15%. Вы можете ознакомиться с их полным опытом на Treebo.com, но сегодня мы хотели бы погрузиться в некоторые технические аспекты, которые сделали возможным выпуск этого PWA.

Путешествие к представлению

Старый мобильный сайт

Старый мобильный сайт Treebo работал на монолитной установке Django. Пользователи должны были ждать запроса на стороне сервера для каждого перехода страницы на веб-сайте. Эта первоначальная установка имела время первого рисования 1,5 с, первое значимое время рисования 5,9 с и было впервые интерактивным через 6,5 с.

Базовое одностраничное приложение React

Для своей первой итерации переписанного Treebo начали с одностраничного приложения, созданного с использованием React и простой настройки webpack.

Вы можете взглянуть на фактический код, используемый ниже. Это генерирует несколько простых (монолитных) пакетов JavaScript и CSS.

Этот опыт имел первую отрисовку 4,8 с, был первым интерактивным примерно через 5,6 с, а их значимые изображения заголовков были нарисованы примерно за 7,2 с.

Рендеринг на стороне сервера

Затем они немного оптимизировали свою первую отрисовку и попробовали рендеринг на стороне сервера. Важно отметить, что рендеринг на стороне сервера платный. Он оптимизирует одно за счет другого.

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

Treebo использовал renderToString () React для рендеринга компонентов в строку HTML и внедрения состояния приложения при начальной загрузке.

В случае с Treebos, использование рендеринга на стороне сервера снизило время первой отрисовки до 1,1 с, а время первой значимой отрисовки до 2,4 с - это улучшило то, насколько быстро пользователи воспринимали страницу как готовую, они могли читать контент. ранее, и в тестах он показал себя немного лучше в SEO. Но обратная сторона заключалась в том, что это оказывало довольно негативное влияние на время до интерактивности.

Хотя пользователи могли просматривать контент, основной поток зависал при загрузке их JavaScript и просто зависал там.

При использовании SSR браузеру приходилось извлекать и обрабатывать гораздо большие полезные данные HTML, чем раньше, а затем по-прежнему получать, анализировать / компилировать и выполнять JavaScript. По сути, он делал больше работы.

Это означало, что первое взаимодействие произошло около 6,6 с, регресс.

SSR также может отодвинуть TTI, заблокировав основной поток на устройствах нижнего уровня.

Разделение кода и фрагменты на основе маршрута

Следующее, на что обратил внимание Treebo, - это разбиение на фрагменты на основе маршрутов, чтобы сократить время до интерактивности.

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

Что они здесь сделали, так это разделили зависимости своих поставщиков, их манифесты среды выполнения Webpack и их маршруты на отдельные части.

Это сократило время до первого взаимодействия до 4,8 с. Потрясающие!

Единственным недостатком было то, что загрузка JavaScript текущего маршрута началась только после того, как их начальные пакеты были выполнены, что также было не идеально.

Но это, по крайней мере, оказало некоторое положительное влияние на опыт. Для разделения кода на основе маршрутов и этого опыта они делают что-то более неявное. Они используют декларативную поддержку React Router для getComponent с вызовом webpack import () для асинхронной загрузки кусками.

Шаблон производительности PRPL

Разделение на части на основе маршрутов - отличный первый шаг в интеллектуальном объединении кода для более детального обслуживания и кэширования. Treebo хотел развить это и искать вдохновение в шаблоне PRPL.

PRPL - это шаблон для структурирования и обслуживания PWA с упором на производительность доставки и запуска приложений.

PRPL означает:

  • Push критически важных ресурсов для начального URL-маршрута.
  • Визуализировать начальный маршрут.
  • Предварительно кэшировать оставшиеся маршруты.
  • Ленивая загрузка и создание оставшихся маршрутов по запросу.

Часть Push поощряет обслуживание разнесенной сборки, разработанной для комбинаций сервер / браузер, которые поддерживают HTTP / 2, для доставки ресурсов, необходимых браузеру для быстрой первой отрисовки при оптимизации кэширования. Доставку этих ресурсов можно эффективно запустить с помощью <link rel="preload"> или HTTP / 2 Push.

Treebo решил использовать ‹link rel =” preload ”/› для предварительной загрузки фрагмента текущего маршрута заранее. Это привело к отбрасыванию их первого интерактивного времени, поскольку фрагмент текущего маршрута уже был в кеше, когда веб-пакет вызвал его для его получения после того, как их начальные пакеты завершили выполнение. Это немного сдвинуло время вниз, и поэтому первое взаимодействие произошло на отметке 4,6 с.

Единственный минус, который у них был с предварительной загрузкой, - это то, что она не поддерживает кроссбраузерность. Однако в Safari Tech Preview есть реализация предварительной загрузки ссылки. Я надеюсь, что он приземлится и останется в этом году. Также ведется работа, чтобы попробовать запустить его в Firefox.

HTML Streaming

Одна из трудностей с renderToString () заключается в том, что он синхронный и может стать узким местом производительности при рендеринге сайтов React на стороне сервера. Серверы не отправят ответ, пока не будет создан весь HTML-код. Когда веб-серверы вместо этого передают свой контент в потоковом режиме, браузеры могут отображать страницы для пользователей до того, как будет завершен весь ответ. Здесь могут помочь такие проекты, как react-dom-stream.

Чтобы улучшить воспринимаемую производительность и внедрить в свое приложение ощущение прогрессивного рендеринга, Treebo обратилась к потоковой передаче HTML. Они будут транслировать тег заголовка с тегами предварительной загрузки link rel, настроенными на раннюю предварительную загрузку в их CSS и своих сценариях JavaScripts. Затем они выполняют рендеринг на стороне сервера и отправляют остальную полезную нагрузку в браузер.

Преимущество этого было в том, что загрузка ресурсов началась раньше, при этом первая раскраска упала до 0,9 секунды, а первая интерактивная - до 4,4. Приложение было постоянно интерактивным на отметке 4,9 / 5 секунд.

Обратной стороной здесь было то, что соединение между клиентом и сервером оставалось открытым немного дольше, что могло вызвать проблемы, если вы столкнетесь с более длительным временем задержки. Для потоковой передачи HTML Treebo определила ранний фрагмент с содержимым ‹head›, затем у них есть основной контент и поздние фрагменты. Все это вводится на страницу. Вот как это выглядит:

Фактически, ранний блок имеет свои операторы rel = preload для всех различных тегов скрипта. Последний фрагмент содержит HTML-код, отрисованный сервером, и все, что будет включать состояние или фактически использовать загружаемый JavaScript.

Встраивание CSS с критическим путем

Таблицы стилей CSS могут блокировать рендеринг. Пока браузер не запросит, не получит, не загрузит и не проанализирует ваши таблицы стилей, страница может оставаться пустой. Уменьшая объем CSS, который должен пройти браузер, и встраивая его (стили критического пути) на страницу, удаляя таким образом HTTP-запрос, мы можем ускорить отображение страницы.

Treebo добавила поддержку встраивания CSS критического пути для текущего маршрута и асинхронной загрузки в остальной части CSS с помощью loadCSS в DOMContentLoaded.

Это привело к удалению тега ссылки блокировки рендеринга критического пути для таблиц стилей и встраиванию меньшего количества строк основного CSS, что позволило сократить время первой отрисовки примерно до 0,4 с.

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

Статические ресурсы с автономным кешированием

Service Worker - это программируемый сетевой прокси, позволяющий вам контролировать обработку сетевых запросов с вашей страницы.

Treebo добавила поддержку кэширования статических ресурсов Service Worker, а также настраиваемую автономную страницу. Ниже мы можем увидеть их регистрацию Service Worker и то, как они использовали sw-precache-webpack-plugin для кэширования ресурсов ».

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

Обслуживание JavaScript, кэшированного с помощью Service Worker с использованием Cache API (как мы рассмотрели в разделе Производительность при запуске JavaScript), также имеет приятное свойство раннего включения Treebo в кеш кода V8, что позволяет сэкономить немного времени при запуске во время повторных посещений. .

Затем Treebo захотела попытаться уменьшить размер пакета и время выполнения JS своего поставщика, поэтому они перешли с React на Preact в производственной среде.

Переход с React на Preact

Preact - крошечная альтернатива React размером 3 КБ с тем же API ES2015. Он нацелен на обеспечение высокопроизводительного рендеринга с дополнительной совместимостью позже (preact-compat), которая работает с остальной частью экосистемы React, такой как Redux.

Частично меньший размер Preact связан с удалением проверок Synthetic Events и PropType. Вдобавок это:

  • Отличает виртуальную модель DOM от модели DOM
  • Позволяет использовать такие реквизиты, как класс и для
  • Проходы (реквизиты, состояние) для рендеринга
  • Использует стандартные события браузера
  • Поддерживает полностью асинхронный рендеринг
  • Аннулирование поддерева по умолчанию

В ряде PWA переход на Preact привел к уменьшению размеров пакетов JS и меньшему времени начальной загрузки JavaScript для приложения. Недавние запуски PWA, такие как Lyft, Uber и Housing.com, используют Preact в производстве.

Примечание. Работаете с кодовой базой React и хотите использовать Preact? В идеале вы должны использовать preact и preact-compat для ваших dev, prod и тестовых сборок. Это позволит вам обнаруживать любые ошибки взаимодействия на ранней стадии. Если вы предпочитаете использовать псевдонимы preact и preact-compat в Webpack для производственных сборок (например, если вы предпочитаете использовать Enzyme), обязательно тщательно протестируйте все, как ожидается, перед развертыванием на ваших серверах.

В случае с Treebo этот переход привел к уменьшению размеров пакетов их поставщиков с 140 до 100 КБ. Между прочим, это все заархивировано. Время первого взаимодействия снизилось с 4,6 до 3,9 с на целевом мобильном оборудовании Treebo, что стало чистой прибылью.

Вы можете сделать это в своей конфигурации Webpack, задав псевдонимы response to preact-compat, а также response-dom на preact-compat.

Обратной стороной этого подхода было то, что им пришлось собрать несколько обходных путей, чтобы Preact точно работал со всеми различными частями экосистемы React, которые они хотели использовать.

Preact - хороший выбор в 95% случаев, когда вы будете использовать React; для остальных 5% вам, возможно, придется сообщать об ошибках, чтобы обойти крайние случаи, которые еще не учтены.

Примечания. Поскольку WebPageTest в настоящее время не предлагает способ тестирования реальных Moto G4 непосредственно из Индии, тесты производительности проводились с настройкой Мумбаи - EC2 - Chrome - Эмулированный Motorola G (поколение 4) - 3GSlow - Mobile. Если вы хотите посмотреть на эти следы, их можно найти здесь.

Скелетные экраны

«Каркас экрана - это, по сути, пустая версия страницы, на которую постепенно загружается информация».

~ Люк Вроблевски

Treebo любят реализовывать свои скелетные экраны с использованием компонентов с улучшенным предварительным просмотром (что-то вроде скелетных экранов для каждого компонента). Подход заключается в том, чтобы улучшить любой атомарный компонент (текст, изображение и т. Д.), Чтобы получить предварительную версию, так что если исходные данные, необходимые для компонента, отсутствуют, вместо этого отображается предварительная версия компонента.

Например, если вы посмотрите на название отеля, название города, цену и т. Д. В элементах списка выше, они будут реализованы с использованием таких компонентов типографии, как ‹Text /›, которые принимают два дополнительных свойства, preview и previewStyle, которые используются таким образом.

По сути, если hotel.name не существует, то компонент меняет фон на сероватый цвет с шириной и другими стилями, установленными в соответствии с переданным превьюStyle (ширина по умолчанию равна 100%, если превьюStyle не передается).

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

Итак, чтобы получить статический текст «Вкл. всех цен »в режиме предварительного просмотра вместе с остальным пользовательским интерфейсом, они просто используют саму цену как логику для режима предварительного просмотра.

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

Webpack-bundle-analyzer

На этом этапе Treebo хотел провести анализ пакетов, чтобы посмотреть, какие еще низко висящие плоды можно оптимизировать.

Примечание. Если вы используете такую ​​библиотеку, как React, на мобильных устройствах, важно внимательно относиться к библиотекам других поставщиков, которые вы используете. Невыполнение этого требования может отрицательно сказаться на производительности. Попробуйте разделить библиотеки поставщиков на части, чтобы маршруты загружали только те, которые необходимы.

Treebo использовал webpack-bundle-analyzer, чтобы отслеживать изменения размера их пакетов и контролировать, какие модули содержатся в каждом фрагменте маршрута. Они также используют его для поиска областей, в которых можно оптимизировать для уменьшения размеров пакетов, таких как удаление локалей moment.js и повторное использование глубоких зависимостей.

Оптимизация moment.js с помощью webpack

Treebo сильно полагается на moment.js в своих манипуляциях с датами. Когда вы импортируете moment.js и связываете его с Webpack, ваш пакет будет включать в себя все moment.js и его локали по умолчанию, которые составляют ~ 61,95 КБ в сжатом виде. Это серьезно увеличивает размер вашего окончательного пакета поставщика.

Для оптимизации размера moment.js доступны два плагина для веб-пакетов: IgnorePlugin, ContextReplacementPlugin

Treebo решила удалить все файлы языковых стандартов с помощью IgnorePlugin, поскольку они не нужны.

новый webpack.IgnorePlugin (/^\.\/ locale $ /, / moment $ /)

После удаления локалей размер пакета moment.js упал до ~ 16,48 КБ в сжатом виде.

Самым большим побочным эффектом удаления локалей moment.js стало то, что размер пакета поставщика упал с ~ 179 КБ до ~ 119 КБ. Это огромное снижение на 60 КБ для критически важного пакета, который необходимо обслуживать при первой загрузке. Все это приводит к значительному сокращению времени первого взаимодействия. Подробнее об оптимизации moment.js можно прочитать здесь.

Повторное использование существующих глубоких зависимостей

Первоначально Treebo использовала модуль «qs» для выполнения операций со строкой запроса. Используя выходные данные webpack-bundle-analyzer, они обнаружили, что «response-router» включает модуль «history», который, в свою очередь, включает модуль «query-string».

Поскольку существовало два разных модуля, выполняющих одни и те же операции, замена «qs» этой версией «строки запроса» (путем явной установки) в исходном коде уменьшила размер их пакета еще на 2,72 КБ в сжатом виде (размер файла Модуль «qs»).

Treebo были хорошими сторонниками открытого исходного кода. Они использовали много программного обеспечения с открытым исходным кодом. В свою очередь, они фактически открыли исходный код большей части своей конфигурации Webpack, а также шаблон, который содержит большую часть настроек, которые они используют в производственной среде. Вы можете найти это здесь: https://github.com/lakshyaranganath/pwa

Они также стараются поддерживать это в актуальном состоянии. По мере их развития вы можете использовать их в качестве еще одной эталонной реализации PWA.

Выводы и будущее

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

Ленивая загрузка изображений
Некоторые из вас могли догадаться из диаграмм водопада сети до того, что загрузки изображений веб-сайта конкурируют за пропускную способность с загрузками JS.

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

Lighthouse хорошо выделяет эти проблемы при аудите закадровых изображений:

Двойной импорт

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

Объединив подходы, за которыми последовали loadCSS и babel-plugin-dual-import, Treebo изменила свой подход к загрузке CSS, используя явный вызов настраиваемого импортированного importCss ('chunkname') для загрузки фрагмента CSS параллельно с их импортом. ('chunkpath') вызывают их соответствующий чанк JS.

С этим новым подходом переход маршрута приводит к двум параллельным асинхронным запросам, один для JS, а другой для CSS, в отличие от предыдущего подхода, когда весь CSS загружался в DOMContentLoaded. Это более жизнеспособно, так как пользователь всегда будет загружать необходимый CSS только для маршрутов, которые они посещают.

A / B-тестирование
В настоящее время Treebo внедряет подход к AB-тестированию с рендерингом на стороне сервера и разделением кода, чтобы отрисовывать только тот вариант, который нужен пользователю, во время рендеринга как на стороне сервера, так и на стороне клиента. (Treebo напишет в блоге, как они с этим справились).

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

Это область, над которой они экспериментируют. Один из примеров - это нетерпеливая загрузка фрагмента следующего маршрута во время анимации пульсации кнопки. onClick Treebo выполняет вызов webpack dynamic import () для записи фрагмента следующего маршрута и задерживает переход маршрута с помощью setTimeout. Они также хотят убедиться, что фрагмент следующего маршрута достаточно мал для загрузки в течение заданного тайм-аута 400 мс в медленной сети 3G.

Это конец.

Было весело сотрудничать над этой рецензией. Очевидно, что предстоит еще поработать, но мы надеемся, что вы нашли представление о производительности Treebo интересным чтением :) Вы можете найти нас в твиттере по адресам @addyosmani и @__lakshya (да, двойное подчеркивание xD), мы хотели бы услышать твои мысли.

Выражаем благодарность @_zouhir, @_developit и @samcccone за их обзоры и вклад.

Если вы новичок в React, React for Beginners от Веса Боса - это исчерпывающий обзор для начала.