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

Вот переведенная китайская версия сообщения в блоге на InfoQ.

Но сначала давайте выясним, почему так важна производительность веб-сайта (в конце сообщения в блоге есть ссылки на примеры из практики):

  • Взаимодействие с пользователем: низкая производительность приводит к зависанию, что может расстраивать пользователей с точки зрения пользовательского интерфейса и пользовательского интерфейса.
  • Конверсия и доход: очень часто медленные веб-сайты могут привести к потере клиентов и отрицательно сказаться на коэффициентах конверсии и доходе.
  • SEO. Начиная с 1 июля 2019 года Google по умолчанию будет включать индексирование в первую очередь для мобильных устройств для всех новых веб-сайтов. Веб-сайты будут иметь более низкий рейтинг, если они работают медленно на мобильных устройствах и не имеют удобного для мобильных устройств контента.

В этом сообщении блога мы кратко рассмотрим эти основные области, которые помогли нам повысить производительность на наших страницах:

  • Измерение эффективности: лабораторные и полевые приборы.
  • Рендеринг: подходы к рендерингу, предварительному рендерингу и гибридному рендерингу на стороне клиента и на стороне сервера.
  • Сеть: CDN, кеширование, кэширование GraphQL, кодирование, HTTP / 2 и Server Push.
  • JavaScript в браузере: бюджет размера пакета, разделение кода, async и defer скрипты, оптимизация изображений (WebP, отложенная загрузка, прогрессивная) и подсказки по ресурсам (preload, prefetch, preconnect).

Для некоторого контекста наша домашняя страница построена с помощью React (TypeScript), Phoenix (Elixir), Puppeteer (безголовый Chrome) и GraphQL API (Ruby on Rails). Вот как это выглядит на мобильном телефоне:

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

Без данных вы просто еще один человек, у которого есть свое мнение. - У. Эдвардс Деминг

Лабораторные инструменты

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

Lighthouse - отличный инструмент для аудита веб-страниц в Chrome на локальном компьютере. Он также предоставляет несколько полезных советов о том, как улучшить производительность, доступность, SEO и т. Д. Вот несколько отчетов аудита производительности Lighthouse с имитацией быстрого 3G и 4-кратного замедления ЦП:

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

Полевые инструменты

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

  • WebPageTest - позволяет проводить тесты из разных браузеров на реальных устройствах из разных мест.
  • Test My Site - использует отчет о пользовательском опыте Chrome (CrUX), основанный на статистике использования Chrome; он общедоступен и обновляется ежемесячно.
  • PageSpeed ​​Insights - объединяет лабораторные (Lighthouse) и полевые (CrUX) данные.

Рендеринг

Существует несколько подходов к рендерингу контента, и у каждого из них есть свои плюсы и минусы:

  • Отрисовка на стороне сервера (SSR) - это процесс получения окончательных HTML-документов для браузеров на стороне сервера. Плюсы: поисковые системы могут сканировать веб-сайт без выполнения JavaScript (SEO), быстрая начальная загрузка страницы, код живет только на стороне сервера. Минусы: незначительное взаимодействие с веб-сайтом, полная перезагрузка страницы, ограниченный доступ к функциям браузера.
  • Отрисовка на стороне клиента - это процесс отрисовки контента в браузере с помощью JavaScript. Плюсы: широкие возможности взаимодействия с веб-сайтом, быстрая отрисовка при изменении маршрута после начальной загрузки, доступ к современным функциям браузера (например, офлайн-поддержка с помощью Service Workers). Минусы: неудобно для SEO, медленная начальная загрузка страницы, обычно требует реализации одностраничного приложения (SPA) и API на стороне сервера.
  • Предварительный рендеринг аналогичен рендерингу на стороне сервера, но выполняется заранее во время сборки, а не во время выполнения. Плюсы: обслуживание встроенных статических файлов обычно проще, чем запуск сервера, оптимизация для SEO, быстрая начальная загрузка страницы. Минусы: требуется предварительная отрисовка всех возможных страниц при любых изменениях кода, полной перезагрузке страницы, небогатых взаимодействиях с веб-сайтом, ограниченном доступе к функциям браузера.

Клиентский рендеринг

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

Мы решили перестроить некоторые части приложения с помощью React.

  • Наши разработчики уже знакомы с созданием приложений React (например, встроенных виджетов).
  • У нас уже есть несколько библиотек компонентов React, которые можно использовать в нескольких проектах.
  • Новые страницы имеют несколько интерактивных элементов пользовательского интерфейса.
  • Существует огромная экосистема React с множеством инструментов.
  • С помощью JavaScript в браузере можно создать прогрессивное веб-приложение с множеством приятных функций.

Предварительный рендеринг и рендеринг на стороне сервера

Проблема с клиентскими приложениями, созданными, например, с React Router DOM, остается такой же, как и с Ember.js. JavaScript - это дорогое удовольствие, и требуется время, чтобы увидеть первую полнофункциональную отрисовку в браузере.

Как только мы решили использовать React, мы начали экспериментировать с другими возможными вариантами рендеринга, чтобы браузеры могли быстрее рендерить контент.

  • Gatsby.js позволяет выполнять предварительный рендеринг страниц с помощью React и GraphQL. Gatsby.js - отличный инструмент, который сразу же поддерживает множество оптимизаций производительности. Однако использование предварительного рендеринга для нас не работает, поскольку у нас потенциально неограниченное количество страниц с пользовательским контентом.
  • Next.js - популярный фреймворк Node.js, который позволяет рендеринг на стороне сервера с помощью React. Однако Next.js очень самоуверен, требует использования своего маршрутизатора, решения CSS и так далее. А наши существующие библиотеки компонентов были созданы для браузеров и несовместимы с Node.js.

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

Предварительный рендеринг во время выполнения

Puppeteer - это библиотека Node.js, позволяющая работать с безголовым Chrome. Мы хотели попробовать Puppeteer для предварительного рендеринга во время выполнения. Это позволяет использовать интересный гибридный подход: рендеринг на стороне сервера с Puppeteer и рендеринг на стороне клиента с гидратацией. Вот несколько полезных советов от Google о том, как использовать безголовый браузер для рендеринга на стороне сервера.

У этого подхода есть свои плюсы:

  • Разрешает SSR, что хорошо для SEO. Сканерам не нужно выполнять JavaScript, чтобы видеть контент.
  • Позволяет один раз создать простое браузерное приложение React и использовать его как на стороне сервера, так и в браузерах. Ускорение приложения браузера автоматически делает SSR более быстрым и беспроигрышным.
  • Отображение страниц с помощью Puppeteer на сервере обычно происходит быстрее, чем на мобильных устройствах конечных пользователей (лучшее соединение, лучшее оборудование).
  • Hydration позволяет создавать многофункциональные SPA с доступом к функциям браузера JavaScript.
  • Нам не нужно знать обо всех возможных страницах заранее, чтобы выполнить их предварительную визуализацию.

Однако при таком подходе мы столкнулись с несколькими проблемами:

  • Пропускная способность - это основная проблема. Выполнение каждого запроса в отдельном процессе браузера без заголовка требует много ресурсов. Можно использовать один процесс браузера без заголовка и запускать несколько запросов на отдельных вкладках. Однако использование нескольких вкладок снижает производительность всего процесса.

  • Стабильность. Сложно увеличить или уменьшить масштаб многих безголовых браузеров, сохранить процессы в тепле и сбалансировать рабочую нагрузку. Мы пробовали разные подходы к хостингу: от самостоятельного размещения в кластере Kubernetes до бессерверного с AWS Lambda и Google Cloud Functions. Мы заметили, что у последнего были некоторые проблемы с производительностью с Puppeteer:

По мере того, как мы ближе познакомились с Puppeteer, мы повторили наш первоначальный подход (читайте ниже). У нас также есть несколько интересных текущих экспериментов с рендерингом PDF-файлов через безголовый браузер. Также можно использовать Puppeteer для автоматизированного сквозного тестирования, даже без написания кода. Теперь он поддерживает Firefox в дополнение к Chrome.

Гибридный подход к рендерингу

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

Мы решили попробовать язык программирования Эликсир. Elixir выглядит как Ruby, но работает поверх BEAM (Erlang VM), который был создан для создания отказоустойчивых и стабильных систем.

Elixir использует модель параллелизма акторов. Каждый Актер (процесс Elixir) имеет крошечный объем памяти около 1-2 КБ. Это позволяет одновременно запускать многие тысячи изолированных процессов. Phoenix - это веб-фреймворк Elixir, который обеспечивает высокую пропускную способность и позволяет обрабатывать каждый HTTP-запрос в отдельном процессе Elixir.

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

  • Puppeteer предварительно обрабатывает страницы React так, как мы хотим, во время сборки и сохраняет их в файлах HTML (оболочка приложения из шаблона PRPL).

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

  • Наше приложение Phoenix обслуживает эти предварительно обработанные страницы и динамически вставляет фактическое содержимое в HTML.

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

  • Клиенты получают и сразу начинают показывать HTML, а затем обновляют состояние React DOM, чтобы продолжить работу как обычный SPA.

Таким образом, мы можем создавать интерактивные приложения и иметь доступ к функциям браузера JavaScript.

Сеть

Сеть доставки контента (CDN)

Использование CDN обеспечивает кеширование контента и позволяет ускорить его доставку по всему миру. Мы используем Fastly.com, который обслуживает более 10% всех интернет-запросов и используется такими компаниями, как GitHub, Stripe, Airbnb, Twitter и многими другими.

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

Другой вариант повышения производительности - использование WebAssembly (WASM) на периферии с Fastly. Думайте об этом, как об использовании бессерверного, но на грани с такими языками программирования, как C, Rust, Go, TypeScript и т. Д. Cloudflare имеет аналогичный проект для поддержки WASM на Workers.

Кеширование

Для повышения производительности важно кэшировать как можно больше запросов. Кэширование на уровне CDN позволяет быстрее доставлять ответы новым пользователям. Кэширование путем отправки заголовка Cache-Control позволяет ускорить время ответа на повторяющиеся запросы в браузере.

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

Кеширование GraphQL

Один из наиболее распространенных способов отправки запросов GraphQL - использование метода POST HTTP. Один из подходов, который мы используем, - это кэширование некоторых запросов GraphQL на уровне Fastly:

  • Наше приложение React аннотирует запросы GraphQL, которые можно кэшировать.
  • Перед отправкой HTTP-запроса мы добавляем аргумент URL-адреса, создавая хэш из тела запроса, который включает запрос GraphQL и переменные (мы используем пользовательский fetch с Apollo Client).
  • Varnish (и Fastly) по умолчанию использует полный URL как часть ключа кеширования.
  • Это позволяет нам продолжать отправлять запросы POST с запросом GraphQL в теле запроса и кешировать на границе, не затрагивая наши серверы.

Вот некоторые другие потенциальные стратегии кеширования GraphQL, которые следует учитывать:

  • Кэш на стороне сервера: все запросы GraphQL на уровне преобразователя или декларативно путем аннотирования схемы.
  • Использование постоянных запросов GraphQL и отправка GET /graphql/:queryId, чтобы иметь возможность полагаться на кеширование HTTP.
  • Интегрируйтесь с CDN с помощью автоматизированных инструментов (например, Apollo Server 2.0) или используйте CDN, специфичные для GraphQL (например, FastQL).

Кодирование

Все основные браузеры поддерживают gzip с заголовком Content-Encoding для сжатия данных. Это позволяет отправлять в браузеры меньше байтов, что обычно означает более быструю доставку контента. Также можно использовать более эффективный алгоритм сжатия brotli в поддерживаемых браузерах.

Протокол HTTP / 2

HTTP / 2 - это новая версия сетевого протокола HTTP (h2 в DevConsole). Переход на HTTP / 2 может улучшить производительность благодаря этим отличиям по сравнению с HTTP / 1.x:

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

HTTP / 2 Server Push

Существует множество языков программирования и библиотек, которые не полностью поддерживают все функции HTTP / 2, поскольку они вносят критические изменения в существующие инструменты и экосистему (например, стойку). Но даже в этом случае можно хотя бы частично использовать HTTP / 2. Например:

  • Настройте прокси-сервер, такой как h2o или nginx, с HTTP / 2 перед обычным сервером HTTP / 1.x. Например. Puma и Ruby on Rails могут отправлять Early Hints, которые могут включать HTTP / 2 Server Push с некоторыми ограничениями.
  • Используйте CDN, поддерживающие HTTP / 2, для обслуживания статических ресурсов. Например, мы используем этот подход для отправки шрифтов и некоторых файлов JavaScript клиентам.

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

JavaScript в браузере

Бюджет размера пакета

Правило №1 производительности JavaScript - не использовать JavaScript. - меня

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

  • Используйте числа в зависимости от ваших потребностей или некоторых рекомендуемых значений. Например, ‹ 170KB минимизированный и сжатый JavaScript.
  • Используйте текущий размер пакета в качестве базового или попробуйте уменьшить его, например, на 10%.
  • Постарайтесь создать самый быстрый веб-сайт среди ваших конкурентов и соответствующим образом установите бюджет.

Вы можете использовать пакет bundlesize или подсказки и ограничения производительности Webpack, чтобы следить за бюджетом:

Убейте свои зависимости

Так называется популярная запись в блоге автора Sidekiq.

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

К сожалению, реальность с зависимостями JavaScript такова, что ваш проект, скорее всего, использует многие сотни зависимостей. Просто попробуйте ls node_modules | wc -l.

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

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

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

  • Маршруты загружаются отдельно отдельными блоками JavaScript.
  • Компоненты на странице, которые не видны сразу. Например. модальные окна, нижний колонтитул ниже сгиба.
  • Polyfills и ponyfills для поддержки новейших функций браузера во всех основных браузерах.
  • Предотвратите дублирование кода с помощью SplitChunksPlugin.
  • Файлы локалей по запросу, чтобы не отправлять сразу все поддерживаемые языки.

Вы можете использовать разделение кода с помощью Webpack динамический импорт и React.lazy с Suspense.

Мы создали функцию вместо React.lazy для поддержки именованного экспорта, а не экспорта по умолчанию.

Асинхронные и отложенные скрипты

Все основные браузеры поддерживают атрибуты async и defer в тегах script:

  • Встроенные скрипты полезны для загрузки небольшого критического кода JavaScript.
  • Использование сценария с async полезно для получения JavaScript без блокировки синтаксического анализа HTML, когда сценарий не требуется для ваших пользователей или каких-либо других сценариев (например, сценариев аналитики).
  • Использование сценариев с defer, вероятно, является лучшим способом с точки зрения производительности для получения и выполнения некритического JavaScript без блокировки синтаксического анализа HTML. Кроме того, он гарантирует порядок выполнения при вызове сценариев, что полезно, если один сценарий зависит от другого.

Вот визуализированная разница между скриптами в теге head:

Оптимизация изображения

Хотя 100 КБ JavaScript имеет совсем другую стоимость производительности по сравнению со 100 КБ изображений, в целом важно, чтобы изображения были как можно более легкими.

Один из способов уменьшить размер изображения - использовать более легкий формат изображения WebP в поддерживаемых браузерах. Для браузеров, не поддерживающих WebP, можно использовать одну из следующих стратегий:

  • Возврат к обычным форматам JPEG или PNG (некоторые сети CDN делают это автоматически на основе Accept заголовка запроса браузера).
  • Загрузка и использование WebP polyfill после обнаружения поддержки браузером.
  • Использование Service Workers для прослушивания fetch запросов и изменение фактических URL-адресов для использования WebP, если он поддерживается.

Ленивая загрузка изображений, только когда они находятся в области просмотра или рядом с ней, является одним из наиболее значительных улучшений производительности при начальной загрузке страницы с большим количеством изображений. Вы можете использовать функцию IntersectionObserver в поддерживаемых браузерах или использовать альтернативные инструменты для достижения того же результата, например, react-lazyload.

Некоторые другие оптимизации изображений могут включать:

  • Снижение качества изображений для уменьшения размера.
  • Изменение размера и загрузка изображений минимального размера.
  • Использование атрибута изображения srcset для автоматической загрузки высококачественных изображений для дисплеев Retina с высоким разрешением.
  • Использование прогрессивных изображений для немедленного отображения размытого изображения.

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

Подсказки по ресурсам

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

  • Preload загружает ресурсы в фоновом режиме для текущей загрузки страницы, прежде чем они будут фактически использованы на текущей странице (высокий приоритет).
  • Prefetch работает аналогично preload, извлекая ресурсы и кэшируя их, но для будущих действий пользователя (низкий приоритет).
  • Preconnect позволяет устанавливать ранние соединения до того, как HTTP-запрос будет фактически отправлен на сервер.

Есть также некоторые другие подсказки к ресурсам, такие как prerender или dns-prefetch. Некоторые из этих подсказок ресурсов можно указать в заголовках ответов. Только будьте осторожны при использовании подсказок ресурсов. Начать делать слишком много ненужных запросов и скачивать слишком много данных довольно просто, особенно если пользователи используют сотовую связь.

Заключение

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

Это видео напоминает мне о том, что вы хотите уменьшить размер пакета приложений. - Мой коллега

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

  • Использование Service Workers для кэширования, автономной поддержки и разгрузки основного потока.
  • Встраивание критического CSS или использование функционального CSS для уменьшения размера в долгосрочной перспективе.
  • Использование форматов шрифтов, таких как WOFF2 вместо WOFF (до 50% + сжатие).
  • Поддержание browserslist в актуальном состоянии.
  • Использование webpack-bundle-analyzer для визуального анализа блоков сборки.
  • Предпочитаете пакеты меньшего размера (например, date-fns) и плагины, позволяющие уменьшить размер (например, lodash-webpack-plugin).
  • Пробуем preact, lit-html или svelte.
  • Запуск Маяка в КИ.
  • Прогрессивная гидратация и стриминг с React.

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

Amazon подсчитал, что замедление загрузки страницы всего на 1 секунду может стоить 1,6 миллиарда долларов продаж в год.

Walmart увидел увеличение конверсии на 2% за каждую 1 секунду улучшения времени загрузки. Улучшение каждые 100 мс также приводило к увеличению дохода до 1%.

Google подсчитал, что, замедляя результаты поиска всего на 0,4 секунды, они могут терять 8 миллионов запросов в день.

Перестройка страниц Pinterest для повышения производительности привела к сокращению времени ожидания на 40%, увеличению трафика SEO на 15% и увеличению коэффициента конверсии для регистрации на 15%.

BBC увидела, что они теряют дополнительно 10% пользователей за каждую дополнительную секунду, необходимую для загрузки их сайта.

Тестирование нового более быстрого FT.com показало, что пользователи были более вовлечены на 30%, что означает больше посещений и потребляемого контента.

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

Universe.com увеличил количество просканированных страниц в 10 раз за счет повышения производительности браузера в 10 раз.