Анализ производительности критического пути рендеринга

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

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

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

До сих пор мы фокусировались исключительно на том, что происходит в браузере после того, как ресурс (файл CSS, JS или HTML) становится доступным для обработки. Мы проигнорировали время, необходимое для получения ресурса из кэша или из сети. Мы предположим следующее:

  • Сеть туда и обратно (задержка распространения) до сервера стоит 100 мс.
  • Время ответа сервера составляет 100 мс для HTML-документа и 10 мс для всех остальных файлов.

Привет, мир, опыт

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Critical Path: No Style</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

Попробуй

Мы начнем с базовой HTML-разметки и одного изображения; нет CSS или JavaScript. Давайте откроем временную шкалу нашей сети в Chrome DevTools и проверим полученный водопад ресурсов:

CRP

Как и ожидалось, загрузка HTML-файла заняла около 200 мс. Обратите внимание, что прозрачная часть синей линии представляет продолжительность времени, в течение которого браузер ожидает в сети без получения каких-либо байтов ответа, тогда как сплошная часть показывает время завершения загрузки после получения первых байтов ответа. Загрузка HTML небольшая (<4 КБ), поэтому все, что нам нужно, — это один проход туда и обратно, чтобы загрузить весь файл. В результате для загрузки HTML-документа требуется около 200 мс, причем половина времени тратится на ожидание в сети, а другая половина — на ожидание ответа сервера.

Когда содержимое HTML становится доступным, браузер анализирует байты, преобразует их в токены и строит дерево DOM. Обратите внимание, что DevTools удобно сообщает время события DOMContentLoaded внизу (216 мс), что также соответствует синей вертикальной линии. Разрыв между окончанием загрузки HTML и синей вертикальной линией (DOMContentLoaded) — это время, необходимое браузеру для построения дерева DOM — в данном случае всего несколько миллисекунд.

Обратите внимание, что наша «потрясающая фотография» не заблокировала событие domContentLoaded . Оказывается, мы можем построить дерево рендеринга и даже отрисовать страницу, не дожидаясь каждого ресурса на странице: не все ресурсы критически важны для быстрой первой отрисовки . Фактически, когда мы говорим о критическом пути рендеринга, мы обычно говорим о разметке HTML, CSS и JavaScript. Изображения не блокируют первоначальный рендеринг страницы, хотя нам также следует постараться отрисовать изображения как можно скорее.

Тем не менее, событие load (также известное как onload ) блокируется в образе: DevTools сообщает о событии onload через 335 мс. Напомним, что событие onload отмечает момент, когда все ресурсы , необходимые странице, были загружены и обработаны; в этот момент счетчик загрузки может перестать вращаться в браузере (красная вертикальная линия в водопаде).

Добавление JavaScript и CSS в микс

Наша страница «Hello World experience» кажется простой, но под ее капотом скрыто много всего. На практике нам понадобится нечто большее, чем просто HTML: скорее всего, у нас будет таблица стилей CSS и один или несколько скриптов, чтобы добавить интерактивности на нашу страницу. Давайте добавим оба в смесь и посмотрим, что произойдет:

<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure Script</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body onload="measureCRP()">
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="timing.js"></script>
  </body>
</html>

Попробуй

Прежде чем добавлять JavaScript и CSS:

ДОМ ЦРП

С помощью JavaScript и CSS:

ДОМ, CSSOM, JS

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

Что случилось?

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

Что, если мы заменим наш внешний скрипт встроенным? Даже если сценарий встроен непосредственно в страницу, браузер не сможет его выполнить, пока не будет создан CSSOM. Короче говоря, встроенный JavaScript также блокирует парсер.

Тем не менее, несмотря на блокировку CSS, ускоряет ли встраивание скрипта страницу? Давайте попробуем и посмотрим, что произойдет.

Внешний JavaScript:

ДОМ, CSSOM, JS

Встроенный JavaScript:

DOM, CSSOM и встроенный JS

Мы делаем на один запрос меньше, но время onload и domContentLoaded фактически одинаково. Почему? Что ж, мы знаем, что не имеет значения, является ли JavaScript встроенным или внешним, потому что, как только браузер попадает в тег сценария, он блокируется и ждет, пока будет создан CSSOM. Кроме того, в нашем первом примере браузер загружает CSS и JavaScript параллельно, и они заканчивают загрузку примерно в одно и то же время. В этом случае встраивание кода JavaScript нам не сильно поможет. Но есть несколько стратегий, которые могут ускорить отображение нашей страницы.

Во-первых, напомним, что все встроенные скрипты блокируют парсер, но для внешних скриптов мы можем добавить ключевое слово «async», чтобы разблокировать парсер. Давайте отменим встраивание и попробуем:

<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure Async</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body onload="measureCRP()">
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script async src="timing.js"></script>
  </body>
</html>

Попробуй

Блокирующий парсер (внешний) JavaScript:

ДОМ, CSSOM, JS

Асинхронный (внешний) JavaScript:

DOM, CSSOM, асинхронный JS

Намного лучше! Событие domContentLoaded возникает вскоре после анализа HTML; браузер знает, что не следует блокировать JavaScript, и поскольку других сценариев блокировки парсера нет, построение CSSOM также может выполняться параллельно.

В качестве альтернативы мы могли бы встроить как CSS, так и JavaScript:

<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure Inlined</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <style>
      p {
        font-weight: bold;
      }
      span {
        color: red;
      }
      p span {
        display: none;
      }
      img {
        float: right;
      }
    </style>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script>
      var span = document.getElementsByTagName('span')[0];
      span.textContent = 'interactive'; // change DOM text content
      span.style.display = 'inline'; // change CSSOM property
      // create a new element, style it, and append it to the DOM
      var loadTime = document.createElement('div');
      loadTime.textContent = 'You loaded this page on: ' + new Date();
      loadTime.style.color = 'blue';
      document.body.appendChild(loadTime);
    </script>
  </body>
</html>

Попробуй

DOM, встроенный CSS, встроенный JS

Обратите внимание, что время domContentLoaded фактически такое же, как и в предыдущем примере; вместо того, чтобы помечать наш JavaScript как асинхронный, мы встроили CSS и JS в саму страницу. Это делает нашу HTML-страницу намного больше, но преимуществом является то, что браузеру не нужно ждать, чтобы получить какие-либо внешние ресурсы; все прямо на странице.

Как видите, даже для очень простой страницы оптимизация критического пути рендеринга — нетривиальное занятие: нам нужно понять граф зависимостей между различными ресурсами, нам нужно определить, какие ресурсы являются «критическими», и мы должны выбрать среди различных стратегий включения этих ресурсов на страницу. Не существует единого решения этой проблемы; каждая страница отличается. Вам необходимо выполнить аналогичный процесс самостоятельно, чтобы определить оптимальную стратегию.

Тем не менее, давайте посмотрим, сможем ли мы сделать шаг назад и выявить некоторые общие закономерности производительности.

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

Самая простая страница состоит только из HTML-разметки; никакого CSS, никакого JavaScript или других типов ресурсов. Чтобы отобразить эту страницу, браузер должен инициировать запрос, дождаться прибытия HTML-документа, проанализировать его, построить DOM и, наконец, отобразить его на экране:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Critical Path: No Style</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

Попробуй

Привет, мир, CRP

Время между T 0 и T 1 отражает время обработки сети и сервера. В лучшем случае (если HTML-файл небольшой) весь документ будет получен за один сеанс связи по сети. Из-за особенностей работы транспортных протоколов TCP для файлов большего размера может потребоваться больше циклов передачи. В результате в лучшем случае указанная выше страница имеет один критический путь рендеринга туда и обратно (минимум).

Теперь давайте рассмотрим ту же страницу, но с внешним CSS-файлом:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

Попробуй

DOM + CSSOM CRP

И снова мы совершаем сетевой обход для получения HTML-документа, а затем полученная разметка сообщает нам, что нам также нужен файл CSS; это означает, что браузер должен вернуться на сервер и получить CSS, прежде чем он сможет отобразить страницу на экране. В результате этой странице требуется как минимум два обращения, прежде чем ее можно будет отобразить. Опять же, файл CSS может выполнять несколько обходов, отсюда и упор на «минимум».

Давайте определим словарь, который мы используем для описания критического пути рендеринга:

  • Критический ресурс: ресурс, который может заблокировать первоначальный рендеринг страницы.
  • Длина критического пути: количество обращений туда и обратно или общее время, необходимое для получения всех критических ресурсов.
  • Критические байты: общее количество байтов, необходимое для первого рендеринга страницы, которое представляет собой сумму файлов передачи всех критических ресурсов. Наш первый пример с одной HTML-страницей содержал один критически важный ресурс (HTML-документ); длина критического пути также была равна одному сетевому обходу (при условии, что файл был небольшим), а общее количество критических байтов было просто размером передачи самого HTML-документа.

Теперь давайте сравним это с характеристиками критического пути в примере HTML + CSS выше:

DOM + CSSOM CRP

  • 2 критически важных ресурса
  • 2 или более обхода туда и обратно для минимальной длины критического пути
  • 9 КБ критических байтов

Для построения дерева рендеринга нам нужны как HTML, так и CSS. В результате и HTML, и CSS являются критически важными ресурсами: CSS извлекается только после того, как браузер получает HTML-документ, поэтому длина критического пути составляет как минимум два обращения туда и обратно. Оба ресурса в сумме составляют 9 КБ критических байтов.

Теперь давайте добавим в смесь еще один файл JavaScript.

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="app.js"></script>
  </body>
</html>

Попробуй

Мы добавили app.js , который является одновременно внешним ресурсом JavaScript на странице и ресурсом, блокирующим парсер (то есть критическим). Хуже того, чтобы выполнить файл JavaScript, нам придется заблокировать и дождаться CSSOM; Напомним, что JavaScript может запрашивать CSSOM, и, следовательно, браузер приостанавливает работу до тех пор, пока не будет загружен style.css и не будет создан CSSOM.

DOM, CSSOM, JavaScript CRP

Тем не менее, на практике, если мы посмотрим на «сетевой водопад» этой страницы, вы увидите, что запросы CSS и JavaScript инициируются примерно в одно и то же время; браузер получает HTML, обнаруживает оба ресурса и инициирует оба запроса. В результате приведенная выше страница имеет следующие характеристики критического пути:

  • 3 важнейших ресурса
  • 2 или более обхода туда и обратно для минимальной длины критического пути
  • 11 КБ критических байтов

Теперь у нас есть три критически важных ресурса, которые в сумме составляют до 11 КБ критических байтов, но длина критического пути по-прежнему составляет два цикла туда и обратно, поскольку мы можем передавать CSS и JavaScript параллельно. Выяснение характеристик вашего критического пути рендеринга означает возможность определить критические ресурсы, а также понять, как браузер будет планировать их выборку. Продолжим наш пример.

Пообщавшись с разработчиками нашего сайта, мы поняли, что JavaScript, который мы включили на нашу страницу, не нужно блокировать; у нас есть некоторая аналитика и другой код, которому не нужно блокировать рендеринг нашей страницы. Зная это, мы можем добавить атрибут «async» в тег скрипта, чтобы разблокировать парсер:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="app.js" async></script>
  </body>
</html>

Попробуй

DOM, CSSOM, асинхронный JavaScript CRP

Асинхронный скрипт имеет ряд преимуществ:

  • Скрипт больше не блокирует парсер и не является частью критического пути рендеринга.
  • Поскольку других важных сценариев нет, CSS не нужно блокировать событие domContentLoaded .
  • Чем раньше сработает событие domContentLoaded , тем раньше сможет начать выполняться другая логика приложения.

В результате наша оптимизированная страница теперь вернулась к двум критическим ресурсам (HTML и CSS) с минимальной длиной критического пути в два цикла и общим размером 9 КБ критических байтов.

Наконец, если бы таблица стилей CSS была нужна только для печати, как бы это выглядело?

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" media="print" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="app.js" async></script>
  </body>
</html>

Попробуй

DOM, неблокирующий CSS и асинхронный JavaScript CRP

Поскольку ресурс style.css используется только для печати, браузеру не нужно блокировать его для отображения страницы. Следовательно, как только построение DOM будет завершено, у браузера будет достаточно информации для отображения страницы. В результате на этой странице имеется только один критический ресурс (документ HTML), а минимальная длина критического пути рендеринга составляет один цикл туда и обратно.

Обратная связь