Критический путь рендеринга веб-страниц

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

После выполнения этих рекомендаций возникает вопрос: а что именно мы оптимизируем? Оказывается, что в большинстве случаев это время полной загрузки страницы со всеми элементами. Однако, это не совсем то, что нужно. На самом деле важно время, за которое пользователь получает «первый экран» страницы с важными функциональными элементами (заголовок, текст, описание товара и т.д.) Другими словами, важен момент начала рендеринга страницы. Здесь и возникает критический путь рендеринга, который определяет все действия, которые должен выполнить браузер для начала отрисовки страницы. С этой штукой мы и будем разбираться в статье.

Что такое критический путь?


Критический путь рендеринга – это набор минимально необходимых для начала отрисовки страницы действий, ресурсов и вычислений.

Критический путь можно измерять в количестве критических ресурсов, минимальном времени загрузки (измеряется в RTT) и объеме критических ресурсов (в байтах).

Для иллюстрации возьмем простейший пример: HTML страницу размером 1 кб без внешних ресурсов. Критический путь будет: 1 ресурс (HTML-документ), 1 RTT (минимально), 1 кб трафика. Однако, таких простых страниц в природе почти не встретить, поэтому покажем, как можно определять критический путь на реальных веб-страницах.

Определение критического пути


Настоящая веб-страница состоит из HTML-документа и некоторого количества внешних ресурсов: CSS-файлы, JS-файлы, шрифты, картинки и т. д. Современные браузеры стараются максимально оптимизировать процесс загрузки страницы, чтобы начать рендеринг как можно быстрее. Однако, браузеры ограничены спецификациями CSS и JS, поэтому должны строить страницу в строгой последовательности. Конечный этап критического пути – построение Render Tree, по которому браузер может начинать рендеринг.

Посмотрим основные шаги, которые включает в себя критический путь:
  1. Получить HTML-документ.
  2. Провести парсинг HTML на предмет включенных ресурсов.
  3. Построить DOM tree (document object model).
  4. Отправить запросы критических элементов.
  5. Получить весь CSS-код (также запустить запросы на JS-файлы).
  6. Построить CSSOM tree.
  7. Выполнить весь полученный JS-код.
  8. Перестроить DOM tree (при необходимости).
  9. Построить Render tree и начать отрисовку страницы.

Из этой последовательности можно сделать несколько важных выводов.

Во-первых в критическом пути участвуют ресурсы c CSS и JS-кодом. Остальные внешние ресурсы там не учитываются.
Во-вторых, JS-код не может выполняться, пока не получены все ресурсы CSS и не построено CSSOM дерево.
В-третьих, страница не может быть отрисована до выполнения JS-кода, так как он может изменять DOM-дерево.
Но не всё так просто: дело, как обычно, в деталях. Наша задача: максимально сократить критический путь рендеринга для нашей страницы.

Способы сокращения критического пути


Для примера возьмем код страницы из демонстрации плагина Autocomplete из jQuery UI.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>jQuery UI Autocomplete - Default functionality</title>
  <link rel="stylesheet" href="http://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css">
  <script src="http://code.jquery.com/jquery-1.10.2.js"></script>
  <script src="http://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
  <link rel="stylesheet" href="http://jqueryui.com/resources/demos/style.css">
  <script>
  $(function() {
    var availableTags = [
      "ActionScript",
      "AppleScript",
      "Scheme"
    ];
    $( "#tags" ).autocomplete({
      source: availableTags
    });
  });
  </script>
</head>
<body>
<div class="ui-widget">
  <label for="tags">Tags: </label>
  <input id="tags">
</div>
</body>
</html>

Из каких элементов состоит критический путь этой страницы?
  • Сама страница (HTML).
  • 2 СSS-файла.
  • 2 JS-файла и JS-код в head страницы.

При условии параллельной загрузки JS-файлов получаем 3 RTT (минимально).
Сокращаем критический путь рендеринга. Что можно сделать в этом случае:
  • Объединить два CSS в один файл.
  • Объединить JS в один файл.
  • Поместить вызов JS-файла и встроенный JS-код в конец страницы до /body.
  • Отложить загрузку CSS для элемента autocomplete.

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

Остановимся подробнее на третьей и четвертой оптимизации. Перемещение вызова JS-файла в конец документа позволит браузеру раньше начать рендеринг страницы. Отложенная загрузка CSS от jQuery UI возможна из-за того, что стили из этого файла нужны только для отображения элемента autocomplete (после набора текста в поле).

Вот так будет выглядеть конечный вариант страницы.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>jQuery UI Autocomplete - Default functionality</title>
  <link rel="stylesheet" href="http://jqueryui.com/resources/demos/style.css">
</head>
<body>
 
<div class="ui-widget">
  <label for="tags">Tags: </label>
  <input id="tags">
</div>
 
<script>document.addEventListener("DOMContentLoaded", function(event) {
    var availableTags = [
      "ActionScript",
      "AppleScript",
      "Scheme"
    ];
    $( "#tags" ).autocomplete({
      source: availableTags
    });
  });
</script> 
<script>document.addEventListener("DOMContentLoaded", function(event) { 
$('head').append('<link rel="stylesheet" href="http://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css" type="text/css">');
});</script>
<script src="http://code.jquery.com/jquery-1.10.2.js"></script>
<script src="http://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
</body>
</html>

Обратите внимание: вызовы плагина jQuery UI обёрнуты в конструкцию:

document.addEventListener("DOMContentLoaded", function(event) { 
// plugin code
 });

Это позволяет размещать код, зависимый от jQuery и его плагинов в любом месте страницы. Таким же методом выполнена отложенная загрузка CSS-файла.

Теперь критический путь сокращен на 1 запрос (CSS) и не требуется его загрузка (трафик) для начала отрисовки. За счет того, что весь JS-код перемещен в конец документа, браузер может начать отрисовку еще до выполнения этого кода.
Если на странице есть скрипты, которые можно выполнить потом (например, скрипты-счетчики, социальные плагины и т. д.), то выкинуть их из критического пути рендеринга можно атрибутом async:

<script src=1.js async></script>

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

Результат


Проверяем, что получилось. Загружаем оба файла в Chrome. Открываем Developer tools, включаем «Toggle device mode», ограничиваем сеть до 450 Kbps 150 ms RT, загружаем без кеша. При загрузке находимся на закладке «Timeline».

Нас интересует момент начала отрисовки страницы (первые события Paint, они отображаются зелёным цветом). Для неоптимизированной версии: 5000 мс, для оптимизированной 500 мс.

Используя описанные приёмы можно значительно ускорить рендеринг ваших страниц, особенно если они насыщены JS-функциональностью и имеют большой объем CSS-кода. При внесении оптимизаций будьте осторожны: тестируйте каждое изменение – изменение порядка загрузки JS-библиотек может сломать функциональность.

Что почитать ещё


Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 18
  • 0
    Вы забыли упомянуть показатель TTFB (Time To First Byte), ведь оптимизация загрузки страницы это только определенный шаг. Да, это уже по части серверной оптимизации,… но все же.
    • 0
      Да, в статье далеко не все рекомендации по ускорению. Такой задачи и не стояло. Кстати, важное дополнение: идеально, если размер критического пути в килобайтах укладывался в 14 кб. Это связано с TCP congestion window размером в 10 пакетов (примерно 14-15 кб).
      • 0
        Time To First что-либо — не очень хорошие метрики. Потому что тот же TTFB легко накрутить послав клиенту первую часть HTML-кода с тайтлом ещё до какой-либо работы бэкенда, что реально даст немногое. Хотя это может помочь тому же критическому пути, если там есть ссылки на нужные CSS и JS. Но в таком случае надо уже собственно критическим путём и заниматься.

        Также и Time To First Paint ни о чём говорит: нарисовался один пиксель или белый фон, всё, метрика сработала. Хотя на экране может ничего не быть. А даже если и есть, интерфейс может не отзываться до загрузки необходимых частей. Поэтому от такой метрики отказываются, а Speed Index — интегральный показатель.
        • 0
          Согласен, лучше глазами смотреть при хорошо тормознутой сети. В этом плане даже Speed Index не спасает — он тоже считает пиксели и не учитывает работоспособность интерфейса.
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Насчет параллельно загрузки согласен, браузер находит критические ресурсы и пытается загрузить их как можно раньше.
          А вот по поводу рендеринга — нет синхронный (обычный) JS блокирует рендеринг страницы, если его включение находится в начале кода страницы (все, что ниже блокируется до получения кода и его выполнения). Чтобы посмотреть это на практике, можете использовать примеры из статьи.
          • НЛО прилетело и опубликовало эту надпись здесь
            • +1
              Основной прирост от переноса JS вниз страницы даже не из-за отложенного парсинга, а из-за сетевых расходов на скачивание ресурса. В примере статьи два JS это около 240 кб сжатого трафика.
              По парсингу: здесь важно разделять парсинг кода JS и CSS. Пока парсинг именно JS не очень быстрый: в примере из статьи jquery сам по себе выполняется у меня (десктоп) 20 мс, а jquery-ui 56 мс. А теперь представим мобильного пользователя: время парсинга JS легко может выйти за 100-200 мс, а это уже заметно.
              По пересчету стилей при подгрузке: да, она происходит, есть даже перерисовка. Но так, как видимых изменений именно этот CSS не вносит, пользователь даже не видит этих действий. А рабочую страницу получает раньше (экономит 10 кб и один запрос).
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  Объясните пожалуйста как перенос скрипта вниз страницы уменьшит скачиваемые трафик?


                  Общий трафик никак, трафик до начала рендеринга страницы: на размер ресурса.

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


                  Я думаю, что перерисовка и вычисление стилей будет быстрее выполнения JS-кода и тем более быстрее сети (особенно мобильной). Это как общее правило. А на самом деле нужно тестировать в реальных условиях (на устройстве, с ограничением скорости сети).
          • +2
            По моему в стаьтье допущена серьезная ошибка — отложенная загрузка. В большинстве случаев (в приведенных примерах точно) не имеет смысла, а скорее всего приведет как раз к задержкам при повторном открытии страницы. Вот смотрите: при первом открытии страницы, когда файлы еще не закешированы, отложенная загрузка даст некоторый прирост. При последующих открытиях страницы все js и css будут уже в кеше браузера и никакие запросы к серверу не уйдут. Если у вас настроены кеш заголовки иначе, то я вам сочуствую и не может идти речи ни о какой оптимизации. При повторных открытиях будут выполнятся скрипты для загрузки стилей и скриптов, которые уже и так в браузере и могли бы уже к этому времени распарситься. По моему имеет смысл откладывать загрузку css со шрифтами и то указывая другой media типв теге, чтобы не блокироваться.
            • 0
              Специально для вас посмотрел: выполенние JS-инструкции о подгрузке CSS-файла занимает 0.8 мс. По-моему не страшная задержка. Если файл в кеше, то и здесь он не будет загружаться снова. Пересчет стилей после подгрузки CSS 2 мс.
              Заметьте, что я отложил загрузку CSS именно для элементов, которые не показываются при начальном рендеринге страницы. Откладывать загрузку CSS с важными стилями (например, шрифтами): плохая идея, получится flash of unstyled content. По крайней мере, это нужно подробно тестировать.
            • –2
              Можно весь CSS поместить в style в HTML
              • +3
                Не надо так делать. Убивается весь эффект от кеширования, раздувается HTML.
                • –1
                  Допустимо только разместить критический CSS при условии что он очень мал
                  • 0
                    В smashingmagazine так и делают (посмотрие лекцию Виталя Фридмана) — с помощью phantomjs, если я правильно помню, выявляются стили, которые необходимы для отображения первых 1000px по высоте и кладут их в html.
                    • 0
                      Inline CSS — это уже очень тонкая оптимизация. По-моему, так стоит делать только, если основной CSS в сжатом виде весит больше 10 кб. Если меньше, то скорее всего HTML и этот CSS уложатся в первые 10 TCP-пакетов (15 кб), то есть загрузка этой части будет достаточно быстрая.
                • 0
                  Посмотрим основные шаги, которые включает в себя критический путь:
                  1. Получить HTML-документ.
                  2. Провести парсинг HTML на предмет включенных ресурсов.
                  3. Построить DOM tree (document object model).
                  4. Отправить запросы критических элементов.
                  5. Получить весь CSS-код (также запустить запросы на JS-файлы).
                  6. Построить CSSOM tree.
                  7. Выполнить весь полученный JS-код.
                  8. Перестроить DOM tree (при необходимости).
                  9. Построить Render tree и начать отрисовку страницы.

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

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