Проект Motors Vertical (MoVe) от eBay - это мобильная площадка объявлений для продажи и покупки автомобилей на разных рынках. Команда из Берлина запустила пилотный веб-сайт в Канаде на kijijiautos.ca.

Вызов

Более 60% трафика на kijijiautos.ca поступает от пользователей мобильных устройств и планшетов, поэтому нам нужен производительный и отзывчивый веб-сайт, предлагающий те же функции, что и настольная версия. Но также функция, которая используется только на одном рынке, не должна быть в коде другого рынка, чтобы минимизировать количество JavaScript, отправляемого конечному пользователю.

Разрабатывая некоторые функции в виде прогрессивного веб-приложения (PWA), мы заметили улучшенные показатели потенциальных клиентов, аналогичные тому, что мы имеем в наших собственных решениях (Android и iOS), поэтому мы знали, что нам следует больше сосредоточиться на добавлении дополнительных функций PWA.

Решения

Веб-приложение MoVe разработано с использованием React с рендерингом на стороне сервера (SSR) для первых посещений и легко индексируется поисковыми системами. Мы выполняем рендеринг на стороне клиента после того, как пользователь вошел в наше приложение. Мы используем webpack для объединения ресурсов, которые используются для создания HTML-кода каждой страницы.

Серверный рендеринг

С помощью SSR мы можем доставлять контент пользователю раньше, чем при подходе на стороне клиента, примерно на 64% лучше «визуально завершенного» времени.

Мы визуализируем веб-страницу с помощью метода renderToNodeStream из React, но сначала нам нужно получить данные, необходимые для рендеринга страницы, а во-вторых, мы используем react-шлем, чтобы показать контент, специфичный для страницы, в элементе head для SEO-цели. Проблема с реактивным шлемом заключается в том, что нам нужно дождаться полной визуализации страницы, прежде чем отправлять контент в браузер. У нас были некоторые инициативы по отправке части элемента head ранее (например, тегов скриптов, стилей и шрифтов, которые не зависят от содержимого), чтобы браузер мог начать их загрузку раньше. Но проблема с этим подходом заключается в том, что код состояния запроса всегда будет 200, поэтому вам нужно вручную обрабатывать ошибки в запросе (404 или 500).

Отправка исходной информации HTML в браузер, чтобы он мог начать загрузку ресурсов до рендеринга приложения React на сервере

Разделение кода

Поскольку мы используем HTTP / 2, мы можем использовать разделение кода, чтобы разделить наш JavaScript на более мелкие части. Мы используем веб-пакет для разделения наших пакетов и разделения блоков по страницам (домашняя страница, страница поиска и т. Д.). Но мы также лениво загружаем некоторые компоненты React, которые не нужны при первой загрузке. Мы используем технику под названием Ожидание до срочности для загрузки ресурсов в режиме ожидания обратного вызова (или в течение тайм-аута, если браузер не поддерживает его), но срочно загружаем все, что нужно пользователю, перед обратным вызовом. Одним из примеров является боковая панель, которую можно использовать для добавления дополнительных фильтров на страницу поиска. Этот компонент обычно загружается в режиме ожидания обратного вызова, но если пользователь нажимает кнопку, чтобы открыть боковую панель, он сразу же загружается и отменяет обратный вызов.

Мы используем react-universal-component для отложенной загрузки компонентов как на клиенте, так и на сервере, чего у React.suspense еще нет.

Для сторонних библиотек, таких как React или lodash, мы решили создать группу пакетов для каждой библиотеки, которую мы используем. Например, мы собрали вместе react, react-dom и response-шлем в один файл, а библиотеку redux и промежуточное ПО - в другой. Это делает кеш активов более полезным для будущих посещений, поскольку, например, если мы обновим реакцию, файл redux останется прежним.

Дерево трясется

Поскольку мы создаем одно и то же приложение для разных рынков с одинаковой базой кода, встряхивание важно для удаления неиспользуемого кода с каждого рынка и уменьшения размера пакета. Мы используем webpack со свойством sideEffect, установленным в false для нашего файла JavaScript. Мы замечаем, что порядок CSS не гарантируется между средой разработки и производственной средой. Это должно быть решено с учетом специфики селекторов CSS. В webpack есть открытый вопрос для решения этой проблемы.

Стили

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

Шрифты

Загрузка шрифтов в веб-приложение иногда обходится дорого. У нас есть три разных шрифта, и мы начали их загружать с помощью метатега preload и использовали свойство swap, чтобы не блокировать рендеринг страницы. Этот метод лучше, чем полная блокировка контента, но мы заметили, что можем добиться большего. Мы решили использовать стратегию двухэтапной загрузки шрифтов. Мы использовали Wakamai Fondue, чтобы проанализировать, какой контент можно удалить на первом этапе, и отложить загрузку остальной части шрифта на втором этапе. (Подробнее см. В этом блоге Зака ​​Лезермана).

HTTP / 2

В настоящее время мы все должны использовать HTTP / 2 из соображений производительности. Мы также знаем, что мы не можем использовать те же методы, которые мы использовали ранее для протокола HTTP / 1.1. Одним из этих приемов было разделение ресурсов в разных доменах, чтобы браузер мог обрабатывать запросы параллельно. Для HTTP / 2 это не так, и лучше хранить все в одном домене и поддерживать одно и то же соединение открытым. В нашем приложении мы стараемся всегда использовать один и тот же домен и просто перенаправляем балансировщик нагрузки на любую службу, которую нам нужно загрузить. Например, у нас есть путь / static / для статических файлов, а наш шлюз API использует путь / consumer /. Таким образом, браузеру не нужно будет подключаться до выполнения фактического запроса (DNS, подключение, SSL).

Статические ресурсы хранятся в одном домене, поэтому DNS (зеленая / синяя полоса), соединение (оранжевая полоса) и SSL (розовая полоса) выполняются только в первом запросе

Динамически загружать следующую страницу

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

Например, чтобы загрузить ресурсы следующей страницы, мы используем метод preload react-universal-component, чтобы получить ресурсы для следующей страницы. Мы используем тот же метод, который описан ранее, для загрузки компонентов этой страницы: стратегия От бездействия до срочности.

Ресурсы страницы элемента просмотра (VIP) предварительно загружаются на страницу результатов поиска

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

Кеширование

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

Для статических файлов, таких как изображения или JavaScript и CSS, мы используем стратегию «сначала кэш» и возвращаемся в сеть, если кеш еще не существует.

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

Поскольку наши страницы динамически создаются на сервере и у нас нет файла index.html, нам нужно вручную внедрить ресурсы, которые будут предварительно кэшированы в сервис-воркере. Мы используем assets-webpack-plugin для генерации всей информации об ассетах, созданных webpack, и мы создали маршрут под названием /service-worker.js в нашем экспресс-приложении, которое генерирует файл сервис-воркера с использованием ассета. Информация. Это импортирует настоящего сервис-воркера с importScripts, поэтому мы можем изменить способ создания сервис-воркера и файлы, которые нужно предварительно кэшировать.

Помимо сервис-воркера, мы также используем HTTP-кеш и удаляем статические файлы через год. Мы используем хэш содержимого в имени файла для обновления JavaScript в браузере. С webpack это довольно просто, но мы замечаем, что каждый файл изменялся при каждом развертывании, даже если мы не меняли его содержимое. Это связано с тем, что webpack увеличивает идентификатор каждого модуля в зависимости от порядка разрешения, делая их каждый раз разными. Это можно решить, установив для свойства optimisation.moduleIds значение hashed, которое не будет меняться в зависимости от порядка. Для получения дополнительной информации см. Руководство по кешированию Webpack.

Измерение производительности

Когда мы говорим о производительности, нам всегда нужно сначала измерить и проанализировать, применить изменения и сравнить, чтобы узнать, есть ли у нас какие-либо улучшения. Без оценки вы не знаете, улучшит или ухудшит положение вас изменение.

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

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

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

Бот GitHub предупреждает нас, если что-то увеличило размер пакета в наших PR

Возможности для улучшений

У нас все еще есть некоторые улучшения, которые нам нужно сделать. Мы по-прежнему отправляем в браузер слишком много JavaScript. Показатель «времени до взаимодействия» (TTI) сайта kijijiautos.ca составляет более 12 с на устройствах более низкого уровня. Это вызвано не только сторонними библиотеками (реклама, A / B-тесты), но и нашим основным пакетом, который занимает почти 1 секунду для анализа и запуска с охватом 55% неиспользуемого кода. Эту проблему можно решить с помощью более легких библиотек, таких как Preact, ленивой загрузки некоторых редукторов redux, ленивого увлажнения некоторых компонентов React или просто удаления большого количества JavaScript.

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

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

Заключение

У нас еще есть путь, но я надеюсь, что некоторые из этих методов могут быть полезны и для других. Спасибо, что прочитали эту статью, и я хотел бы услышать, что вы делаете, чтобы сделать свой веб-сайт быстрее. Обращайтесь к нам в комментариях или в Твиттере.