Главная » Статьи » Практический пример: анализ производительности сайтов Walmart

Практический пример: анализ производительности сайтов Walmart

Практический пример: анализ производительности сайтов Walmart

От автора: Walmart является одним из ведущих розничных e-commerce продавцов в США. В 2016 году они были вторыми после Amazon по продажам. В e-commerce скорость загрузки сайта напрямую влияет на конверсию. Для многих компаний e-commerce ускорение сайта на 1 секунду увеличило конверсию в 1.05, 1.1 или даже 1.2 раза . Это потому, что чем медленнее сайт, тем больше пользователей отказывается от него до его загрузки – следовательно, и конверсия меньше.

К сожалению, Walmart сайты довольно медленные. В моих тестах содержимое страницы продукта становится видимым только на третьей секунде:

Для сравнения, на Amazon контент становится видимым через 1,4 секунды. Клиент видит продукт, за которым пришел в два раза быстрее!

Давайте проанализируем сайт Walmart и посмотрим, как мы можем улучшить производительность и помочь Walmart заработать больше! В качестве примера я использую страницу продукта Lumia 635.

Фикс невидимого текста

Первая проблема со страницей заключается в том, что рендеринг начинается где-то на 2.3s, но текст не отображается до 3.0s:

Это происходит потому, что Walmart использует собственный шрифт, и по умолчанию Chrome и Firefox не будут отображать текст до загрузки шрифта. Вот как это выглядит вживую:

Посмотрите, как на странице не отображается текст в течение секунды? (Сеть урезается с предустановкой «Fast 3G» в Chrome DevTools)

Браузеры откладывают отрисовку текста, чтобы предотвратить вспышку нестилизованного текста (FOUT). Однако это делает контент невидимым дольше — и, вероятно, уменьшает конверсию!

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

Чтобы изменить это поведение, мы можем добавить font-display: optional для стилей @font-face. font-display управляет тем, как применяется пользовательский шрифт. В нашем случае он говорит браузеру просто использовать резервный шрифт, если пользовательский не кэшируется:

/* https://ll-us-i5.wal.co/.../BogleWeb.css */
@font-face { font-family: "BogleWeb"; /* ... */ font-display: optional;
}

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

Теперь текст сразу отображается. (Сеть урезается с предустановкой «Fast 3G» в Chrome DevTools. Файл CSS был заменен Fiddler)

Примечание: одностраничные приложения

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

Оптимизация JavaScript

Другая проблема заключается в том, что страница загружает около 2 МБ сжатого JavaScript. Это много:

JavaScript-код минимизирован, поэтому я могу анализировать его только на поверхности. Вот что я нашел.

Использовать defer для первого пакета

Большинство тегов script на странице имеют атрибут async или defer. Это хорошо, потому что браузер может отображать страницу, не ожидающую загрузки этих скриптов:

На странице больше скриптов в разных местах, так что это всего лишь пример

Однако один большой файл — bundle.3p.min-[hash].js , 112.3 kB в сжатом виде — не имеет ни одного из этих атрибутов. Если для загрузки требуется некоторое время (например, клиент находится в плохом соединении), страница останется пустой, пока скрипт не будет полностью загружен. Не круто!

Чтобы решить эту проблему, добавьте атрибут defer в этот тег скрипта. Как только весь JavaScript, который полагается на bundle.3p.min-[hash].js, также отложен (что, кажется, так), код будет работать нормально.

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

Боковое примечание: метки производительности

На скриншоте выше есть код, который, вероятно, измеряет время выполнения пакета:

<script>_wml.perf.mark("before-bundle")</script>
<script src="https://ll-us-i5.wal.co/dfw/[hash]/v1/standard_js.bundle.[hash].js" id="bundleJs" defer></script>
<script>_wml.perf.mark("after-bundle")</script>

Этот код работает не так, как ожидалось: из-за defer пакет выполняется после обоих этих встроенных скриптов. На всякий случай кто-нибудь из Walmart читает это.

Загрузка второстепенного кода только при необходимости

В Chrome DevTools есть вкладка «покрытие», в которой анализируется, сколько CSS и JS не используется. Если открыть вкладку, перезагрузить страницу и немного пощелкать по ней, чтобы запустить наиболее важный JavaScript, можно увидеть, что около 40-60% JS еще не выполнено:

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

Вот как динамически загружать компоненты с помощью React и webpack:

import React from 'react'; class FeedbackButton extends React.Component { handleButtonClick() { // ↓ Here, import() will make webpack split FeedbackModal // into a separate file // and download it only when import() is called import('../FeedbackModal/').then(module => { this.setState({ FeedbackModal: module.default }); }); } render() { const FeedbackModal = this.state.FeedbackModal; return <React.Fragment> <button onClick={this.handleButtonClick}> Provide feedback! </button> {FeedbackModal && <FeedbackModal />} </React.Fragment>; }
};

Не используйте babel-polyfill в современных браузерах

Если мы посмотрим на standard_js.bundle.[hash].js, мы заметим, что он включает babel-polyfill:

Довольно легко найти по «babel»

babel-polyfill весит 32.9 kB в сжатом виде и занимает 170 мс для загрузки на Fast 3G:

Не отправляя этот полифил в современных браузерах, мы могли бы сделать страницу полностью интерактивной на 170 мс раньше! И это довольно легко сделать:

либо используйте внешнюю службу, которая обслуживает полифилы на основе User-Agent, такие как polyfill.io,

или создайте второй пакет без полифилов и обслуживайте его с помощью <script type=»module»>, как в статье Филиппа Уолтона.

Не загружайте полифилы несколько раз

Другая проблема заключается в том, что Object.assign Object.assign обслуживается в трех файлах одновременно:

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

Удалите полифилы Node.js

По умолчанию пакет webpack связывает полифилы для функций Node.js, когда они видят их. Теоретически это полезно: если библиотека полагается на setImmediate или Buffer которые доступны только в Node.js, она все равно будет работать в браузере благодаря polyfill. На практике, однако, я видел следующее:

// node_modules/random-library/index.js
const func = () => { ... }; if (typeof setImmediate !== 'undefined') { // ↑ Webpack decides that `setImmediate` is used // and adds the polyfill setImmediate(func);
} else { setTimeout(func, 0);
}

Библиотека адаптирована для работы в браузере, но поскольку webpack видит, что он ссылается на setImmediate, он связывает полифил.

Полифилы Node небольшие (несколько килобайт), поэтому их удаление обычно не имеет смысла. Тем не менее, это хороший кандидат на оптимизацию, если мы сжимаем последние миллисекунды со страницы. Удалить их очень просто (но нужно протестировать — может, какой-то код им действительно нужен?):

// webpack.config.js
module.exports = { node: false,
};

Уменьшите блокирующий рендер CSS

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

Страница Walmart изначально зависит от двух файлов CSS. В моих тестах самый большой из них занимает больше времени для загрузки, чем пакет JS, поэтому он блокирует рендеринг даже после того, как скрипт был загружен и выполнен:

Обратите внимание, что страница остается пустой (посмотрите в «Фреймы» в нижней части изображения), пока CSS не будет полностью загружен.

Как это решить? Мы можем пойти так, как Гардиан в 2013 году: Найдите критический CSS и извлеките его в отдельный файл. «Критический» означает «страница выглядит смешно без него».

Здесь могут быть полезны такие инструменты, как Penthouse или Critical. Я также настроил бы результат вручную, чтобы исключить контент верхней видимой части страницы, но который не очень важный (например, заголовок навигации):

Мы можем показать это через пару секунд в обмен на ускорение общего рендеринга:

При обслуживании исходного HTML загружайте только критический CSS.

Когда страница загружается более или менее (например, во время события DOMContentLoaded), динамически добавляйте оставшийся CSS:

document.addEventListener('DOMContentLoaded', () => { const styles = ['https://i5.walmartimages.com/.../style.css', ...]; styles.forEach((path) => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = path; document.head.appendChild(link); });
});

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

Удалите дублирующие стили

В общей сложности страница Walmart загружает три файла CSS: один с определениями шрифтов (BogleWeb.css) и два со стилями приложения (standard_css.style.[hash].css и style.[hash].css). Последние два казались довольно похожими, поэтому я удалил весь контент, кроме селекторов, и попытался сравнить файлы.

Угадай, что? Среди этих файлов есть 3400 общих селекторов, и эти селекторы в основном имеют общие стили! Для перспективы первый файл содержит около 7900 селекторов, а второй — около 4400:

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

Добавьте service worker для кэширования файлов

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

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

С Walmart мы можем создать service worker, который кэширует ресурсы сайта в фоновом режиме еще до того, как пользователю они будут нужны. Существует несколько способов сделать это; конкретный зависит от инфраструктуры Walmart. Хороший пример одного подхода доступен в репозитории GoogleChrome.

Примечания: уведомления

С service worker мы также получаем возможность отправлять уведомления клиентам! Это следует использовать с осторожностью — или мы можем их раздражать, но это может также увеличить вовлеченность. Хорошие примеры уведомлений: «Продукт, который вы сохранили, позже получил скидку» или «Джон Форд ответил на ваш вопрос об iPhone 8».

Другие идеи

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

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

Улучшение времени до первого байта. Иногда сервер тратит слишком много времени на статические ресурсы. Видите длинные зеленые полосы? Это время ожидания сервера:

Эти задержки не детерминированы — я видел их довольно часто во время анализа, но каждый раз они происходят с разными ресурсами, поэтому это может быть проблема с сетью. Тем не менее, я тоже заметил их в результатах WebPageTest.

Включение сжатия Brotli. Когда вы загружаете текстовый ресурс с сервера, сервер обычно сжимает его с помощью GZip и обслуживает сжатую версию. Браузер распакует его позже, после получения. Это сжатие делает текст в несколько раз меньше.

Помимо GZip, есть и Brotli — новый алгоритм сжатия, который сжимает текст на 15-20% лучше. Прямо сейчас, все текстовые ресурсы на странице Walmart сжимаются с помощью GZip. Имеет смысл попробовать Brotli посмотреть, улучшает ли он среднее время загрузки.

Бонус. Увеличьте качество изображения продукта

Это тоже связано с производительностью.

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

 https://i5.walmartimages.com/[hash].jpeg?odnHeight=&odnWidth=&odnBg= 

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

Я бы оптимизировал эту часть для UX вместо производительности — и показывал изображения в лучшем качестве. Мы по-прежнему сохраняем разницу в размерах минимальной:

Попробуйте использовать другой алгоритм кодирования. WebP на 30% меньше JPEG с одинаковым уровнем сжатия. MozJPEG — оптимизированный кодировщик JPEG, который работает повсюду и имеет значительно меньше артефактов сжатия.

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

Используйте <picture> чтобы оставаться совместимым с разными браузерами. Например, мы могли бы обслуживать WebP для Chrome и JPEG для других браузеров:

<picture> <source srcset="https://i5.walmartimages.com/[hash].webp?..." type="image/webp"> <img src="https://i5.walmartimages.com/[hash].jpeg?...">
</picture>

Обслуживайте ретина изображения с помощью <source srcset>. Как это:

<picture> <source srcset="https://i5.walmartimages.com/[hash].webp?odnHeight=450&odnWidth=450, https://i5.walmartimages.com/[hash].webp?odnHeight=900&odnWidth=900 2x" type="image/webp" > <img src="https://i5.walmartimages.com/[hash].jpeg?odnHeight=450&odnWidth=450" srcset="https://i5.walmartimages.com/[hash].jpeg?odnHeight=900&odnWidth=900 2x" >
</picture>

Подведение итогов

Таким образом, чтобы оптимизировать страницу продукта на сайте Walmart, мы можем:

Исправить невидимый текст с помощью font-display: optional

Использовать defer для большого пакета JavaScript

Загрузить не важный код с импортом webpack

Удалить полифилы в современных браузерах

Уменьшить CSS-рендеринг

Удалить дублированные стили

Добавить service worker для кэширования файлов в фоновом режиме

С помощью этих трюков мы можем отображать страницу продукта раньше, по крайней мере, 400-600 мс. Если мы применим аналогичные улучшения ко всему сайту, мы сможем увеличить заказы как минимум на 3-6% — и помочь Walmart заработать больше.

Автор: Ivan Akulov

Источник: https://iamakulov.com/

Редакция: Команда webformyself.