Pull to refresh

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

Reading time 5 min
Views 38K
В среде веб-разработчиков все больше распространяется знание о том, что скорость важна. Многие стараются ускориться: используют сжатие 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-библиотек может сломать функциональность.

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


Tags:
Hubs:
+14
Comments 18
Comments Comments 18

Articles