Методы ускорения загрузки веб-приложения даже на обычном телефоне.

Как мы использовали разделение кода, встраивание кода и рендеринг на стороне сервера в PROXX.

На Google I/O 2019 Марико, Джейк и я представили PROXX , современный клон Minesweeper для веба. Что отличает PROXX, так это фокус на доступности (вы можете играть в него с помощью программы чтения с экрана!) и возможность работать как на обычном телефоне, так и на высокопроизводительном настольном устройстве. Обычно телефоны ограничены несколькими способами:

  • Слабые процессоры
  • Слабые или отсутствующие графические процессоры
  • Маленькие экраны без сенсорного ввода
  • Очень ограниченный объем памяти

Но они работают на современном браузере и очень доступны по цене. По этой причине телефоны с функциями возрождаются на развивающихся рынках. Их цена позволяет совершенно новой аудитории, которая раньше не могла себе этого позволить, выйти в сеть и воспользоваться современным вебом. Прогнозируется, что в 2019 году только в Индии будет продано около 400 миллионов телефонов с функциями , поэтому пользователи телефонов с функциями могут стать значительной частью вашей аудитории. Кроме того, скорости соединения, близкие к 2G, являются нормой на развивающихся рынках. Как нам удалось заставить PROXX хорошо работать в условиях телефонов с функциями?

Геймплей PROXX.

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

Это часть 1 из серии из двух частей. Часть 1 посвящена производительности загрузки , а часть 2 — производительности выполнения.

Сохранение статус-кво

Тестирование производительности загрузки на реальном устройстве имеет решающее значение. Если у вас нет реального устройства под рукой, я рекомендую WebPageTest , особенно «простую» настройку . WPT запускает ряд тестов загрузки на реальном устройстве с эмулированным 3G-соединением.

3G — хорошая скорость для измерения. Хотя вы, возможно, привыкли к 4G, LTE или даже к 5G, реальность мобильного интернета выглядит совсем иначе. Возможно, вы в поезде, на конференции, на концерте или в самолете. То, что вы там испытаете, скорее всего, ближе к 3G, а иногда даже хуже.

При этом мы сосредоточимся на 2G в этой статье, поскольку PROXX явно нацелен на обычные телефоны и развивающиеся рынки в своей целевой аудитории. После того, как WebPageTest запустит свой тест, вы получите водопад (похожий на тот, что вы видите в DevTools), а также киноленту вверху. Кинолента показывает, что видит ваш пользователь во время загрузки вашего приложения. На 2G загрузка неоптимизированной версии PROXX довольно плохая:

Видеоролик демонстрирует, что видит пользователь, когда PROXX загружается на реальном бюджетном устройстве через эмулированное 2G-соединение.

При загрузке через 3G пользователь видит 4 секунды белого ничего. Через 2G пользователь не видит абсолютно ничего более 8 секунд. Если вы прочитали , почему производительность имеет значение, вы знаете, что мы уже потеряли значительную часть наших потенциальных пользователей из-за нетерпения. Пользователю нужно загрузить все 62 КБ JavaScript, чтобы что-то появилось на экране. Луч надежды в этом сценарии в том, что как только что-то появляется на экране, оно также становится интерактивным. Или нет?

[Первая значимая краска][FMP] в неоптимизированной версии PROXX _технически_ [интерактивна][TTI], но бесполезна для пользователя.

После загрузки примерно 62 КБ сжатого в gzip JS и создания DOM пользователь получает возможность увидеть наше приложение. Технически приложение является интерактивным. Однако, если взглянуть на визуальную часть, то можно увидеть иную реальность. Веб-шрифты все еще загружаются в фоновом режиме, и пока они не будут готовы, пользователь не видит текста. Хотя это состояние квалифицируется как First Meaningful Paint (FMP) , оно, безусловно, не квалифицируется как по-настоящему интерактивное , поскольку пользователь не может сказать, о чем идет речь в вводе. Требуется еще секунда на 3G и 3 секунды на 2G, прежде чем приложение будет готово к работе. В целом, приложению требуется 6 секунд на 3G и 11 секунд на 2G, чтобы стать интерактивным.

Водопадный анализ

Теперь, когда мы знаем , что видит пользователь, нам нужно выяснить, почему . Для этого мы можем посмотреть на водопад и проанализировать, почему ресурсы загружаются слишком поздно. В нашей трассировке 2G для PROXX мы видим два основных красных флага:

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

Уменьшение количества подключений

Каждая тонкая линия ( dns , connect , ssl ) означает создание нового HTTP-подключения. Настройка нового подключения стоит дорого, так как занимает около 1 с на 3G и около 2,5 с на 2G. В нашем водопаде мы видим новое подключение для:

  • Запрос №1: Наш index.html
  • Запрос №5: Стили шрифтов с fonts.googleapis.com
  • Запрос №8: Google Analytics
  • Запрос №9: Файл шрифта с fonts.gstatic.com
  • Запрос №14: Манифест веб-приложения

Новое соединение для index.html неизбежно. Браузер должен создать соединение с нашим сервером, чтобы получить содержимое. Новое соединение для Google Analytics можно было бы обойти, встроив что-то вроде Minimal Analytics , но Google Analytics не блокирует рендеринг или переход нашего приложения в интерактивный режим, поэтому нас не особо волнует, как быстро оно загружается. В идеале Google Analytics должен загружаться в режиме ожидания, когда все остальное уже загружено. Таким образом, он не будет занимать полосу пропускания или вычислительную мощность во время первоначальной загрузки. Новое соединение для манифеста веб-приложения предписано спецификацией fetch , поскольку манифест должен быть загружен через неуполномоченное соединение. Опять же, манифест веб-приложения не блокирует рендеринг или переход нашего приложения в интерактивный режим, поэтому нам не нужно так сильно беспокоиться.

Однако два шрифта и их стили представляют собой проблему, поскольку они блокируют рендеринг и интерактивность. Если мы посмотрим на CSS, который поставляется fonts.googleapis.com , то увидим, что это всего лишь два правила @font-face , по одному для каждого шрифта. Стили шрифтов на самом деле настолько малы, что мы решили встроить их в наш HTML, удалив одно ненужное соединение. Чтобы избежать затрат на настройку соединения для файлов шрифтов, мы можем скопировать их на наш собственный сервер.

Распараллеливание нагрузок

Глядя на водопад, мы видим, что как только первый файл JavaScript загрузился, сразу же начинают загружаться новые файлы. Это типично для зависимостей модулей. Наш основной модуль, вероятно, имеет статические импорты, поэтому JavaScript не может работать, пока эти импорты не будут загружены. Важно понимать, что эти виды зависимостей известны во время сборки. Мы можем использовать теги <link rel="preload"> чтобы убедиться, что все зависимости начинают загружаться в ту секунду, когда мы получаем наш HTML.

Результаты

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

Мы используем диафильм WebPageTest, чтобы увидеть, чего добились наши изменения.

Эти изменения сократили наш TTI с 11 до 8,5 , что примерно соответствует 2,5 с времени установки соединения, которое мы стремились убрать. Молодцы, мы.

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

Хотя мы только что уменьшили наш TTI , мы на самом деле не повлияли на вечно длинный белый экран, который пользователь должен терпеть в течение 8,5 секунд. Вероятно, самые большие улучшения для FMP могут быть достигнуты путем отправки стилизованной разметки в ваш index.html . Распространенными методами достижения этого являются предварительный рендеринг и рендеринг на стороне сервера, которые тесно связаны и объясняются в Rendering on the Web . Оба метода запускают веб-приложение в Node и сериализуют полученный DOM в HTML. Рендеринг на стороне сервера делает это по запросу на, ну, стороне сервера, в то время как предварительный рендеринг делает это во время сборки и сохраняет вывод как ваш новый index.html . Поскольку PROXX является приложением JAMStack и не имеет серверной стороны, мы решили реализовать предварительный рендеринг.

Существует много способов реализовать предварительный рендерер. В PROXX мы решили использовать Puppeteer , который запускает Chrome без какого-либо пользовательского интерфейса и позволяет вам удаленно управлять этим экземпляром с помощью API Node. Мы используем это для внедрения нашей разметки и нашего JavaScript, а затем считываем DOM как строку HTML. Поскольку мы используем CSS Modules , мы получаем встраивание CSS нужных нам стилей бесплатно.

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setContent(rawIndexHTML);
  await page.evaluate(codeToRun);
  const renderedHTML = await page.content();
  browser.close();
  await writeFile("index.html", renderedHTML);

При таком подходе мы можем ожидать улучшения для нашего FMP. Нам по-прежнему нужно загружать и выполнять тот же объем JavaScript, что и раньше, поэтому не стоит ожидать, что TTI сильно изменится. Если что, наш index.html стал больше и может немного отодвинуть наш TTI. Есть только один способ узнать это: запустить WebPageTest.

Диафильм показывает явное улучшение нашей метрики FMP. TTI в основном не затронут.

Наша первая значимая отрисовка переместилась с 8,5 до 4,9 секунд, что является огромным улучшением. Наш TTI по-прежнему происходит примерно за 8,5 секунд, поэтому это изменение на него практически не повлияло. То, что мы сделали, — это изменение восприятия . Некоторые могут даже назвать это ловкостью рук. Отрисовывая промежуточный визуальный образ игры, мы меняем воспринимаемую производительность загрузки в лучшую сторону.

Встраивание

Другая метрика, которую предоставляют нам и DevTools, и WebPageTest, — это время до первого байта (TTFB) . Это время, которое проходит от отправки первого байта запроса до получения первого байта ответа. Это время также часто называют временем на круговую передачу (RTT), хотя технически между этими двумя числами есть разница: RTT не включает время обработки запроса на стороне сервера. DevTools и WebPageTest визуализируют TTFB светлым цветом в блоке запроса/ответа.

Светлая часть запроса означает, что запрос ожидает получения первого байта ответа.

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

Именно для этой проблемы изначально и был задуман HTTP/2 Push. Разработчик приложения знает , что нужны определенные ресурсы, и может передать их по сети. К тому времени, как клиент понимает, что ему нужно получить дополнительные ресурсы, они уже находятся в кэшах браузера. HTTP/2 Push оказался слишком сложным для правильной реализации и считается нерекомендуемым. Эта проблемная область будет пересмотрена во время стандартизации HTTP/3. На данный момент самым простым решением является встраивание всех критических ресурсов за счет эффективности кэширования.

Наш критический CSS уже встроен благодаря CSS-модулям и нашему предварительному рендереру на основе Puppeteer. Для JavaScript нам нужно встроить наши критические модули и их зависимости . Эта задача имеет различную сложность в зависимости от используемого вами сборщика.

Благодаря встраиванию JavaScript мы сократили TTI с 8,5 до 7,2 с.

Это сократило наш TTI на 1 секунду. Теперь мы достигли точки, где наш index.html содержит все необходимое для первоначального рендеринга и становится интерактивным. HTML может рендериться, пока он еще загружается, создавая наш FMP. В тот момент, когда HTML завершает парсинг и выполнение, приложение становится интерактивным.

Агрессивное разделение кода

Да, наш index.html содержит все, что нужно для интерактивности. Но при более внимательном рассмотрении оказывается, что он также содержит все остальное. Наш index.html составляет около 43 КБ. Давайте сравним это с тем, с чем пользователь может взаимодействовать в начале: у нас есть форма для настройки игры, содержащая несколько компонентов, кнопку запуска и, возможно, некоторый код для сохранения и загрузки пользовательских настроек. Вот, в общем-то, и все. 43 КБ кажутся большим объемом.

Целевая страница PROXX. Здесь используются только критические компоненты.

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

Анализ содержимого `index.html` PROXX показывает много ненужных ресурсов. Критические ресурсы выделены.

Что нам нужно сделать, так это разделить код . Разделение кода разбивает ваш монолитный пакет на более мелкие части, которые можно загружать лениво по требованию. Популярные упаковщики, такие как Webpack , Rollup и Parcel, поддерживают разделение кода с помощью динамического import() . Упаковщик проанализирует ваш код и встроит все модули, которые импортируются статически . Все, что вы импортируете динамически, будет помещено в свой собственный файл и будет извлечено из сети только после выполнения вызова import() . Конечно, обращение к сети имеет свою цену и должно выполняться только в том случае, если у вас есть свободное время. Мантра здесь заключается в том, чтобы статически импортировать модули, которые критически необходимы во время загрузки, и динамически загружать все остальное. Но вы не должны ждать до самого последнего момента, чтобы лениво загрузить модули, которые определенно будут использоваться. Idle Until Urgent Фила Уолтона — отличный шаблон для здоровой середины между ленивой загрузкой и нетерпеливой загрузкой.

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

export default function deferred(componentPromise) {
  return class Deferred extends Component {
    constructor(props) {
      super(props);
      this.state = {
        LoadedComponent: undefined
      };
      componentPromise.then(component => {
        this.setState({ LoadedComponent: component });
      });
    }

    render({ loaded, loading }, { LoadedComponent }) {
      if (LoadedComponent) {
        return loaded(LoadedComponent);
      }
      return loading();
    }
  };
}

При этом мы можем использовать Promise компонента в наших функциях render() . Например, компонент <Nebula> , который отображает анимированное фоновое изображение, будет заменен пустым <div> во время загрузки компонента. После загрузки компонента и готовности к использованию <div> будет заменен фактическим компонентом.

const NebulaDeferred = deferred(
  import("/components/nebula").then(m => m.default)
);

return (
  // ...
  <NebulaDeferred
    loading={() => <div />}
    loaded={Nebula => <Nebula />}
  />
);

При всем этом мы сократили наш index.html до всего лишь 20 КБ, менее половины от исходного размера. Какое влияние это оказывает на FMP и TTI? WebPageTest покажет!

Кинолента подтверждает: наш TTI теперь составляет 5,4 с. Радикальное улучшение по сравнению с нашими первоначальными 11 с.

Наши FMP и TTI находятся всего в 100 мс друг от друга, поскольку это всего лишь вопрос анализа и выполнения встроенного JavaScript. Всего через 5,4 с на 2G приложение становится полностью интерактивным. Все остальные, менее важные модули загружаются в фоновом режиме.

Еще больше ловкости рук

Если вы посмотрите на наш список критических модулей выше, вы увидите, что движок рендеринга не является частью критических модулей. Конечно, игра не может начаться, пока у нас нет нашего движка рендеринга для рендеринга игры. Мы могли бы отключить кнопку «Старт», пока наш движок рендеринга не будет готов к запуску игры, но по нашему опыту пользователь обычно тратит достаточно много времени на настройку своих игровых параметров, поэтому в этом нет необходимости. В большинстве случаев движок рендеринга и другие оставшиеся модули завершают загрузку к тому времени, как пользователь нажимает «Старт». В редких случаях, когда пользователь быстрее своего сетевого соединения, мы показываем простой экран загрузки, который ждет завершения работы оставшихся модулей.

Заключение

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

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

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

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

,

Как мы использовали разделение кода, встраивание кода и рендеринг на стороне сервера в PROXX.

На Google I/O 2019 Марико, Джейк и я представили PROXX , современный клон Minesweeper для веба. Что отличает PROXX, так это фокус на доступности (вы можете играть в него с помощью программы чтения с экрана!) и возможность работать как на обычном телефоне, так и на высокопроизводительном настольном устройстве. Обычно телефоны ограничены несколькими способами:

  • Слабые процессоры
  • Слабые или отсутствующие графические процессоры
  • Маленькие экраны без сенсорного ввода
  • Очень ограниченный объем памяти

Но они работают на современном браузере и очень доступны по цене. По этой причине телефоны с функциями возрождаются на развивающихся рынках. Их цена позволяет совершенно новой аудитории, которая раньше не могла себе этого позволить, выйти в сеть и воспользоваться современным вебом. Прогнозируется, что в 2019 году только в Индии будет продано около 400 миллионов телефонов с функциями , поэтому пользователи телефонов с функциями могут стать значительной частью вашей аудитории. Кроме того, скорости соединения, близкие к 2G, являются нормой на развивающихся рынках. Как нам удалось заставить PROXX хорошо работать в условиях телефонов с функциями?

Геймплей PROXX.

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

Это часть 1 из серии из двух частей. Часть 1 посвящена производительности загрузки , а часть 2 — производительности выполнения.

Сохранение статус-кво

Тестирование производительности загрузки на реальном устройстве имеет решающее значение. Если у вас нет реального устройства под рукой, я рекомендую WebPageTest , особенно «простую» настройку . WPT запускает ряд тестов загрузки на реальном устройстве с эмулированным 3G-соединением.

3G — хорошая скорость для измерения. Хотя вы, возможно, привыкли к 4G, LTE или даже к 5G, реальность мобильного интернета выглядит совсем иначе. Возможно, вы в поезде, на конференции, на концерте или в самолете. То, что вы там испытаете, скорее всего, ближе к 3G, а иногда даже хуже.

При этом мы сосредоточимся на 2G в этой статье, поскольку PROXX явно нацелен на обычные телефоны и развивающиеся рынки в своей целевой аудитории. После того, как WebPageTest запустит свой тест, вы получите водопад (похожий на тот, что вы видите в DevTools), а также киноленту вверху. Кинолента показывает, что видит ваш пользователь во время загрузки вашего приложения. На 2G загрузка неоптимизированной версии PROXX довольно плохая:

Видеоролик демонстрирует, что видит пользователь, когда PROXX загружается на реальном бюджетном устройстве через эмулированное 2G-соединение.

При загрузке через 3G пользователь видит 4 секунды белого ничего. Через 2G пользователь не видит абсолютно ничего более 8 секунд. Если вы прочитали , почему производительность имеет значение, вы знаете, что мы уже потеряли значительную часть наших потенциальных пользователей из-за нетерпения. Пользователю нужно загрузить все 62 КБ JavaScript, чтобы что-то появилось на экране. Луч надежды в этом сценарии в том, что как только что-то появляется на экране, оно также становится интерактивным. Или нет?

[Первая значимая краска][FMP] в неоптимизированной версии PROXX _технически_ [интерактивна][TTI], но бесполезна для пользователя.

После загрузки примерно 62 КБ сжатого в gzip JS и создания DOM пользователь получает возможность увидеть наше приложение. Технически приложение является интерактивным. Однако, если взглянуть на визуальную часть, то можно увидеть иную реальность. Веб-шрифты все еще загружаются в фоновом режиме, и пока они не будут готовы, пользователь не видит текста. Хотя это состояние квалифицируется как First Meaningful Paint (FMP) , оно, безусловно, не квалифицируется как по-настоящему интерактивное , поскольку пользователь не может сказать, о чем идет речь в вводе. Требуется еще секунда на 3G и 3 секунды на 2G, прежде чем приложение будет готово к работе. В целом, приложению требуется 6 секунд на 3G и 11 секунд на 2G, чтобы стать интерактивным.

Водопадный анализ

Теперь, когда мы знаем , что видит пользователь, нам нужно выяснить, почему . Для этого мы можем посмотреть на водопад и проанализировать, почему ресурсы загружаются слишком поздно. В нашей трассировке 2G для PROXX мы видим два основных красных флага:

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

Уменьшение количества подключений

Каждая тонкая линия ( dns , connect , ssl ) означает создание нового HTTP-подключения. Настройка нового подключения стоит дорого, так как занимает около 1 с на 3G и около 2,5 с на 2G. В нашем водопаде мы видим новое подключение для:

  • Запрос №1: Наш index.html
  • Запрос №5: Стили шрифтов с fonts.googleapis.com
  • Запрос №8: Google Analytics
  • Запрос №9: Файл шрифта с fonts.gstatic.com
  • Запрос №14: Манифест веб-приложения

Новое соединение для index.html неизбежно. Браузер должен создать соединение с нашим сервером, чтобы получить содержимое. Новое соединение для Google Analytics можно было бы обойти, встроив что-то вроде Minimal Analytics , но Google Analytics не блокирует рендеринг или переход нашего приложения в интерактивный режим, поэтому нас не особо волнует, как быстро оно загружается. В идеале Google Analytics должен загружаться в режиме ожидания, когда все остальное уже загружено. Таким образом, он не будет занимать полосу пропускания или вычислительную мощность во время первоначальной загрузки. Новое соединение для манифеста веб-приложения предписано спецификацией fetch , поскольку манифест должен быть загружен через неуполномоченное соединение. Опять же, манифест веб-приложения не блокирует рендеринг или переход нашего приложения в интерактивный режим, поэтому нам не нужно так сильно беспокоиться.

Однако два шрифта и их стили представляют собой проблему, поскольку они блокируют рендеринг и интерактивность. Если мы посмотрим на CSS, который поставляется fonts.googleapis.com , то увидим, что это всего лишь два правила @font-face , по одному для каждого шрифта. Стили шрифтов на самом деле настолько малы, что мы решили встроить их в наш HTML, удалив одно ненужное соединение. Чтобы избежать затрат на настройку соединения для файлов шрифтов, мы можем скопировать их на наш собственный сервер.

Распараллеливание нагрузок

Глядя на водопад, мы видим, что как только первый файл JavaScript загрузился, сразу же начинают загружаться новые файлы. Это типично для зависимостей модулей. Наш основной модуль, вероятно, имеет статические импорты, поэтому JavaScript не может работать, пока эти импорты не будут загружены. Важно понимать, что эти виды зависимостей известны во время сборки. Мы можем использовать теги <link rel="preload"> чтобы убедиться, что все зависимости начинают загружаться в ту секунду, когда мы получаем наш HTML.

Результаты

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

Мы используем диафильм WebPageTest, чтобы увидеть, чего добились наши изменения.

Эти изменения сократили наш TTI с 11 до 8,5 , что примерно соответствует 2,5 с времени установки соединения, которое мы стремились убрать. Молодцы, мы.

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

Хотя мы только что уменьшили наш TTI , мы на самом деле не повлияли на вечно длинный белый экран, который пользователь должен терпеть в течение 8,5 секунд. Вероятно, самые большие улучшения для FMP могут быть достигнуты путем отправки стилизованной разметки в ваш index.html . Распространенными методами достижения этого являются предварительный рендеринг и рендеринг на стороне сервера, которые тесно связаны и объясняются в Rendering on the Web . Оба метода запускают веб-приложение в Node и сериализуют полученный DOM в HTML. Рендеринг на стороне сервера делает это по запросу на, ну, стороне сервера, в то время как предварительный рендеринг делает это во время сборки и сохраняет вывод как ваш новый index.html . Поскольку PROXX является приложением JAMStack и не имеет серверной стороны, мы решили реализовать предварительный рендеринг.

Существует много способов реализовать предварительный рендерер. В PROXX мы решили использовать Puppeteer , который запускает Chrome без какого-либо пользовательского интерфейса и позволяет вам удаленно управлять этим экземпляром с помощью API Node. Мы используем это для внедрения нашей разметки и нашего JavaScript, а затем считываем DOM как строку HTML. Поскольку мы используем CSS Modules , мы получаем встраивание CSS нужных нам стилей бесплатно.

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setContent(rawIndexHTML);
  await page.evaluate(codeToRun);
  const renderedHTML = await page.content();
  browser.close();
  await writeFile("index.html", renderedHTML);

При таком подходе мы можем ожидать улучшения для нашего FMP. Нам по-прежнему нужно загружать и выполнять тот же объем JavaScript, что и раньше, поэтому не стоит ожидать, что TTI сильно изменится. Если что, наш index.html стал больше и может немного отодвинуть наш TTI. Есть только один способ узнать это: запустить WebPageTest.

Диафильм показывает явное улучшение нашей метрики FMP. TTI в основном не затронут.

Наша первая значимая отрисовка переместилась с 8,5 до 4,9 секунд, что является огромным улучшением. Наш TTI по-прежнему происходит примерно за 8,5 секунд, поэтому это изменение на него практически не повлияло. То, что мы сделали, — это изменение восприятия . Некоторые могут даже назвать это ловкостью рук. Отрисовывая промежуточный визуальный образ игры, мы меняем воспринимаемую производительность загрузки в лучшую сторону.

Встраивание

Другая метрика, которую предоставляют нам и DevTools, и WebPageTest, — это время до первого байта (TTFB) . Это время, которое проходит от отправки первого байта запроса до получения первого байта ответа. Это время также часто называют временем на круговую передачу (RTT), хотя технически между этими двумя числами есть разница: RTT не включает время обработки запроса на стороне сервера. DevTools и WebPageTest визуализируют TTFB светлым цветом в блоке запроса/ответа.

Светлая часть запроса означает, что запрос ожидает получения первого байта ответа.

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

Именно для этой проблемы изначально и был задуман HTTP/2 Push. Разработчик приложения знает , что нужны определенные ресурсы, и может передать их по сети. К тому времени, как клиент понимает, что ему нужно получить дополнительные ресурсы, они уже находятся в кэшах браузера. HTTP/2 Push оказался слишком сложным для правильной реализации и считается нерекомендуемым. Эта проблемная область будет пересмотрена во время стандартизации HTTP/3. На данный момент самым простым решением является встраивание всех критических ресурсов за счет эффективности кэширования.

Наш критический CSS уже встроен благодаря CSS-модулям и нашему предварительному рендереру на основе Puppeteer. Для JavaScript нам нужно встроить наши критические модули и их зависимости . Эта задача имеет различную сложность в зависимости от используемого вами сборщика.

Благодаря встраиванию JavaScript мы сократили TTI с 8,5 до 7,2 с.

Это сократило наш TTI на 1 секунду. Теперь мы достигли точки, где наш index.html содержит все необходимое для первоначального рендеринга и становится интерактивным. HTML может рендериться, пока он еще загружается, создавая наш FMP. В тот момент, когда HTML завершает парсинг и выполнение, приложение становится интерактивным.

Агрессивное разделение кода

Да, наш index.html содержит все, что нужно для интерактивности. Но при более внимательном рассмотрении оказывается, что он также содержит все остальное. Наш index.html составляет около 43 КБ. Давайте сравним это с тем, с чем пользователь может взаимодействовать в начале: у нас есть форма для настройки игры, содержащая несколько компонентов, кнопку запуска и, возможно, некоторый код для сохранения и загрузки пользовательских настроек. Вот, в общем-то, и все. 43 КБ кажутся большим объемом.

Целевая страница PROXX. Здесь используются только критические компоненты.

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

Анализ содержимого `index.html` PROXX показывает много ненужных ресурсов. Критические ресурсы выделены.

Что нам нужно сделать, так это разделить код . Разделение кода разбивает ваш монолитный пакет на более мелкие части, которые можно загружать лениво по требованию. Популярные упаковщики, такие как Webpack , Rollup и Parcel, поддерживают разделение кода с помощью динамического import() . Упаковщик проанализирует ваш код и встроит все модули, которые импортируются статически . Все, что вы импортируете динамически, будет помещено в свой собственный файл и будет извлечено из сети только после выполнения вызова import() . Конечно, обращение к сети имеет свою цену и должно выполняться только в том случае, если у вас есть свободное время. Мантра здесь заключается в том, чтобы статически импортировать модули, которые критически необходимы во время загрузки, и динамически загружать все остальное. Но вы не должны ждать до самого последнего момента, чтобы лениво загрузить модули, которые определенно будут использоваться. Idle Until Urgent Фила Уолтона — отличный шаблон для здоровой середины между ленивой загрузкой и нетерпеливой загрузкой.

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

export default function deferred(componentPromise) {
  return class Deferred extends Component {
    constructor(props) {
      super(props);
      this.state = {
        LoadedComponent: undefined
      };
      componentPromise.then(component => {
        this.setState({ LoadedComponent: component });
      });
    }

    render({ loaded, loading }, { LoadedComponent }) {
      if (LoadedComponent) {
        return loaded(LoadedComponent);
      }
      return loading();
    }
  };
}

При этом мы можем использовать обещание компонента в наших функциях render() . Например, компонент <Nebula> , который отображает анимированное фоновое изображение, будет заменен пустым <div> , пока компонент загружается. Как только компонент загружен и готов к использованию, <div> будет заменен фактическим компонентом.

const NebulaDeferred = deferred(
  import("/components/nebula").then(m => m.default)
);

return (
  // ...
  <NebulaDeferred
    loading={() => <div />}
    loaded={Nebula => <Nebula />}
  />
);

При всем этом мы сократили наш index.html . HTML всего на 20 КБ, менее половины исходного размера. Какое влияние это оказывает на FMP и TTI? WebPageTest расскажет!

Фильма подтверждает: наш TTI сейчас на 5,4 с. Радикальное улучшение по сравнению с нашими первоначальными 11 -м.

Наши FMP и TTI находятся всего на 100 мс друг от друга, так как это только вопрос анализа и выполнения вставленного JavaScript. После всего лишь 5,4 с на 2G приложение полностью интерактивно. Все остальные, менее важные модули загружаются в фоновом режиме.

Больше ловко

Если вы посмотрите на наш список критических модулей выше, вы увидите, что двигатель рендеринга не является частью критических модулей. Конечно, игра не может запустить, пока у нас не появится наш двигатель рендеринга для визуализации игры. Мы могли бы отключить кнопку «Пуск», пока наш механизм рендеринга не будет готов начать игру, но, по нашему опыту, пользователь обычно занимает достаточно много времени, чтобы настроить настройки игры, что это не нужно. Большую часть времени двигатель рендеринга и другие оставшиеся модули выполняются загрузки к тому времени, когда пользователь нажимает «запуск». В редком случае, когда пользователь быстрее, чем их сетевое соединение, мы показываем простой экран загрузки, который ожидает завершения оставшихся модулей.

Заключение

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

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

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

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