Оптимизация фронтенда под браузеры

в 13:31, , рубрики: Блог компании Badoo, браузеры, оптимизация, Программирование, Разработка веб-сайтов

enter image description here

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

  • С точки зрения User Experience (UX) фронтенд должен обеспечивать быструю загрузку и работу веб-страниц.
  • А с точки зрения Developer Experience (DX) нам хочется, чтобы и сам фронтенд тоже работал быстро, был прост в использовании и вообще являлся примером для подражания.

Всё это делает пользователей и разработчиков счастливее, а заодно существенно улучшает ранжирование сайтов поисковиками. Например, Google уделяет особое внимание оптимизированности фронтенда. Если вы достаточно долго бились над тем, чтобы ваш сайт заработал побольше баллов в Google Pagespeed Insights, то, надеемся, эта статья поможет вам лучше понять, для чего всё это нужно и каково разнообразие стратегий оптимизации фронтенда.

Предыстория

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

Описание подхода

Во-первых

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

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

Во-вторых

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

Далее

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

Что делает браузер

Давайте рассмотрим работу браузера на примере исполнения несложного кода:

<!DOCTYPE html>
<html>
  <head>
    <title>The "Click the button" page</title>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="styles.css" />
  </head>

  <body>
    <h1>
      Click the button.
    </h1>

    <button type="button">Click me</button>

    <script>
      var button = document.querySelector("button");
      button.style.fontWeight = "bold";
      button.addEventListener("click", function () {
        alert("Well done.");
      });
    </script>
  </body>
</html>

Как рендерится страница в браузере

После получения HTML-кода браузер парсит документ и преобразует в понятное для себя представление. Оно универсально для всех браузеров и определяется спецификацией HTML5 DOM. Далее браузер выполняет ряд действий, по завершении которых на экране отображается веб-страница. В общих чертах порядок действий такой:

  1. На основании полученного HTML строится DOM-дерево (Document Object Model).
  2. На основании полученного CSS строится CSSOM-дерево (CSS Object Model).
  3. Применительно к построенным DOM и CSSOM выполняются скрипты.
  4. На основе DOM и CSSOM формируется дерево рендеринга.
  5. На основании дерева рендеринга генерируется макет страницы с указанием размеров и координат всех элементов.
  6. Страница рендерится – выводится на экран.

enter image description here

Первый этап: HTML

Браузер считывает разметку сверху вниз и преобразует в узлы DOM-дерева.

enter image description here

Стратегии оптимизации доставки HTML

  • Стили — в первую очередь, скрипты — в последнюю.

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

Ниже мы рассмотрим, как можно в рамках оптимизации настраивать доставку стилей и скриптов.

  • Минификация и компрессия.

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

Применение минификации и компрессии позволит уменьшить объём данных примерно на 80–90%.

  • Повышение доступности.

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

Понять, что ещё можно сделать на странице для повышения её доступности, вам помогут инструменты наподобие WAVE.

Второй этап: CSS

При обнаружении узлов стилей (внешних, внутренних или встроенных стилей) браузер прекращает рендерить DOM и на основе этих узлов строит CSSOM-дерево. Поэтому про CSS говорят, что он блокирует рендеринг. Здесь описаны некоторые преимущества и недостатки разных типов стилей.

// Внешние стили
<link rel="stylesheet" href="styles.css">

// Внутренние стили
<style>
  h1 {
    font-size: 18px;
  }
</style>

// Встроенные стили
<button style="background-color: blue;">Click me</button>

CSSOM-узлы создаются аналогично DOM-узлам. Они будут объединены позже, а на данном этапе это выглядит так:

enter image description here

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

Стратегии оптимизации доставки CSS

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

Сегодня мобильные устройства сильно уступают в производительности «полноценным» компьютерам, поэтому для обеспечения хорошей производительности и скорости загрузки мы должны передавать им данные как можно меньшего объёма. К примеру, с помощью медиа-атрибутов можно сделать так, что сначала будут передаваться стили для мобильных устройств, а затем — для десктопов и ноутбуков. При таком подходе стили будут загружаться без блокировки загрузки самой страницы.

// Этот CSS загружается и блокирует рендеринг страницы при любых обстоятельствах.
// media="all" — это значение по умолчанию. Это равносильно тому, как если бы мы вообще не объявляли медиа-атрибуты.
<link rel="stylesheet" href="mobile-styles.css" media="all">

// Этот CSS на мобильных устройствах будет загружаться в фоновом режиме и не повлияет на загрузку самой страницы.
<link rel="stylesheet" href="desktop-styles.css" media="min-width: 590px">

// Этот медиа-запрос в CSS будет обработан только в случае вывода на печать (print view).
<style>
  @media print {
    img {
      display: none;
    }
  }
</style>

  • Отложенная загрузка CSS.

Если у вас есть стили, которые можно безболезненно загрузить и обработать уже после отображения первого полезного экрана, то с помощью скрипта можно отложить их загрузку. Это могут быть стили для данных, которые расположены ниже первого полезного экрана, то есть чтобы их увидеть, нужно пролистать страницу. Также можно отложить загрузку стилей для данных, корректность отображения которых некритична до тех пор, пока страница не начнёт реагировать на действия пользователя, то есть не станет интерактивной.

<html>
  <head>
    <link rel="stylesheet" href="main.css">
  </head>

  <body>
    <div class="main">
      Important above the fold content.
    </div>

    <div class="secondary">
      Below the fold content.
      Stuff you won't see until after page loads and you scroll down.
    </div>

    <script>
      window.onload = function () {
        // load secondary.css
      }
    </script>
  </body>
</html>

Примеры реализации отложенной загрузки стилей:
https://jakearchibald.com/2016/link-in-body/
https://www.giftofspeed.com/defer-loading-css/

  • Снижение специфичности.

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

// Более специфичные селекторы == плохо
.header .nav .menu .link a.navItem {
  font-size: 18px;
}

// Менее специфичные селекторы == хорошо
a.navItem {
  font-size: 18px;
}

  • Доставка только необходимого.

Для многих это будет очевидным, но всё же. Если у вас есть хоть какой-то опыт разработки фронтенда, то вы наверняка знаете, что одной из ключевых проблем CSS является непредсказуемое увеличение размера стилей. Они словно специально созданы так, чтобы постоянно разрастаться. Поэтому рекомендуется использовать инструменты наподобие UnCSS, которые помогают удалить из CSS всё лишнее. Есть и немало веб-инструментов.

Третий этап: JavaScript

Браузер формирует узлы DOM/ CSSOM до тех пор, пока не встретит внешние или внутренние скрипты JavaScript.

// Внешний скрипт
<script src="app.js"></script>

// Внутренний скрипт
<script>
  alert("Oh, hello");
</script>

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

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

var button = document.querySelector("button");
button.style.fontWeight = "bold";
button.addEventListener("click", function () {
  alert("Well done.");
});

Этот скрипт окажет такой эффект на наши DOM и CSSOM:

enter image description here

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

  • Асинхронная загрузка скриптов.

Если назначить скрипту атрибут async, то браузер будет грузить его в другом потоке с более низким приоритетом без блокирования загрузки страницы. Как только скрипт будет загружен, браузер его выполнит.

<script src="async-script.js" async></script>

Поскольку мы не знаем, когда именно будет выполнен скрипт, возможны две ситуации:

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

enter image description here

Атрибут async рекомендуется использовать со скриптами, которые не влияют на DOM или CSSOM. Также асинхронная загрузка идеально подходит для внешних скриптов, работа которых не зависит от кода и которые некритичны для UX. Яркие примеры: скрипты аналитики и отслеживания, хотя это вовсе не исчерпывающий перечень.

  • Отложенная загрузка скриптов.

Атрибут defer аналогичен async в том смысле, что при его использовании не блокируется загрузка страницы. Но скрипты будут выполняться в порядке появления в коде только после завершения парсинга HTML.

enter image description here

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

<script src="defer-script.js" defer></script>

Ещё один хороший сценарий использования стратегии задержки загрузки. Также вы можете воспользоваться чем-нибудь вроде addEventListener. Полезная ветка по теме.

// Все объекты в DOM и все изображения, скрипты, ссылки и подфреймы уже загружены.
window.onload = function () {
};

// Вызывается по мере готовности DOM, что может произойти до готовности изображений и другого внешнего содержимого.
document.onload = function () {
};

// Способ с jQuery 
$(document).ready(function () {
});

К сожалению, async и defer бесполезны в случае со встроенными скриптами, потому что по умолчанию браузеры компилируют и выполняют их сразу же, как только встречают в коде. Если скрипт встроен в HTML, то он запускается немедленно, а применение атрибутов async и defer ко внешним ресурсам позволяет нам всего лишь абстрагироваться либо задержать передачу скриптов в DOM/ CSSOM.

  • Клонирование узлов перед последующими манипуляциями.

К этому подходу можно прибегнуть лишь при одном условии: если внесение в DOM многочисленных изменений приводит к нежелательным результатам. В этом случае попробуйте сначала клонировать весь узел DOM, внести в клон необходимые изменения, а потом заменить оригинал клоном. Так вы избежите многочисленных перерисовок и снизите нагрузку на процессор и память. Кроме того, клонирование защитит вас от FOUC (Flashes Of Unstyled Content) и внесения спонтанных изменений.

// Эффективное манипулирование узлом благодаря предварительному клонированию.

var element = document.querySelector(".my-node");
var elementClone = element.cloneNode(true); // (true) также клонирует дочерние узлы, (false) не клонирует дочерние узлы

elementClone.textContent = "I've been manipulated...";
elementClone.children[0].textContent = "...efficiently!";
elementClone.style.backgroundColor = "green";

element.parentNode.replaceChild(elementClone, element);

Но помните, что при клонировании узла вы не клонируете слушателей событий. Хотя в некоторых случаях это как раз и может быть целью. Когда-то, когда у нас не было jQuery-методов .on() и .off(), а слушатели не умели вызывать именованные функции, мы пользовались этим подходом для сброса слушателей.

  • Использование атрибутов Preload/ Prefetch/ Prerender/ Preconnect.

У этих атрибутов «говорящие» названия, так что нет смысла объяснять, что они делают. Но появились они сравнительно недавно и пока не поддерживаются всеми браузерами. По этой причине большинству разработчиков эти атрибуты сейчас вряд ли интересны. Подробнее можете почитать о них здесь:
https://css-tricks.com/prefetching-preloading-prebrowsing/
https://www.keycdn.com/blog/resource-hints/

Четвёртый этап: дерево рендеринга

Все узлы прочитаны – можно объединять DOM и CSSOM. Браузер приступает к формированию дерева рендеринга. Выражаясь метафорически, узлы — это «слова», объектные модели — «предложения», а дерево — «страница».

enter image description here

Пятый этап: макет страницы

После формирования дерева рендеринга браузер переходит к формированию макета. Для этого он определяет размеры и координаты всех элементов на странице.

enter image description here

Шестой этап: вывод на экран

Последний этап, страница появляется на экране.

enter image description here

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

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

Вместе с тем очевидно, что JavaScript во многом зависит от событий на клиентской стороне. Нам нужно, чтобы он манипулировал нашим DOM, именно этим он и займётся. Главное – не допустить или уменьшить нежелательные побочные эффекты.

Кстати, рекомендую ознакомиться с выступлением Тали Гарсиэль. Оно было в 2012 году, но представленная в нём информация всё ещё актуальна. Также можете почитать прекрасный туториал по работе браузеров.

Для более детального изучения вопроса штудируйте спецификацию HTML5.

Как браузеры совершают сетевые вызовы

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

Когда браузер совершает запрос к URL, в ответ сервер отправляет какой-то HTML-код. Пойдём от простого к сложному. Допустим, это HTML-код нашей страницы:

<!DOCTYPE html>
<html>
  <head>
    <title>The "Click the button" page</title>
  </head>

  <body>
    <h1>
      Button under construction...
    </h1>
  </body>
</html>

Существует такое понятие, как «критический путь рендеринга» (Critical Rendering Path, CRP). Оно характеризует количество процедур, необходимых для рендеринга страницы. На данный момент схема нашего CRP выглядит так:

enter image description here

Браузер совершает GET-запрос, ожидает, пока мы отправим в ответ один килобайт HTML-кода нашей страницы (пока что мы не добавили CSS или JavaScript), затем строит DOM-дерево и рендерит страницу.

Длина критического пути

У CRP есть три параметра (метрики). Первый из них — длина. Очевидно, что она должна быть как можно меньше.

Для получения HTML, необходимого для рендеринга страницы, браузер обменивается данными с сервером (round trip). Больше ему пока ничего не нужно, то есть сейчас длина CRP равна 1.

Усложним задачу и добавим внутренние стили и JavaScript.

<!DOCTYPE html>
<html>
  <head>
    <title>The "Click the button" page</title>
    <style>
      button {
        color: white;
        background-color: blue;
      }
    </style>
  </head>

  <body>
    <button type="button">Click me</button>

    <script>
      var button = document.querySelector("button");
      button.addEventListener("click", function () {
        alert("Well done.");
      });
    </script>
  </body>
</html>

Схема нашего CRP изменилась:

enter image description here

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

Но не всё так просто. Обратите внимание, что размер нашего HTML увеличился до 2 Кб. А это может (и будет) иметь неприятные последствия.

Размер критически важных ресурсов

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

Если вы думаете, что можно без проблем избавиться от внешних ресурсов, делая их только внутренними или встроенными, то вы ошибаетесь. Этот подход очень плохо масштабируется. Если помещать в современные страницы все необходимые им ресурсы, то они станут неподъёмными, да и браузеры с трудом будут их обрабатывать. Есть одна любопытная статья, в которой проанализировали производительность страниц, созданных с использованием ReactJS (внешние стили не используются, только встроенные). Выяснилось, что размер DOM увеличивается в четыре раза, и DOM-дерево строится вдвое дольше, да ещё и требуется в полтора раза больше времени, чтобы страница стала реагировать на действия пользователя.

Кроме того, внешние ресурсы можно кэшировать. Поэтому, если пользователь снова пришёл на страницу или ранее заходил на страницы, использующие те же самые ресурсы (вроде my global.css), то браузер подтянет их из кэша, а не будет запрашивать по сети. От этого мы только в выигрыше. Так что давайте использовать стили и скрипты в качестве внешних ресурсов.

На данный момент у нас один внешний СSS-файл, один внешний JavaScript-файл и один асинхронно загружаемый внешний JavaScript-файл.

<!DOCTYPE html>
<html>
  <head>
    <title>The "Click the button" page</title>
    <link rel="stylesheet" href="styles.css" media="all">
    <script type="text/javascript" src="analytics.js" async></script>  // async
  </head>

  <body>
    <button type="button">Click me</button>

    <script type="text/javascript" src="app.js"></script>
  </body>
</html>

Вот как изменилась схема нашего критического пути:

enter image description here

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

Сканер находит styles.css и app.js и для их скачивания создаёт ещё один критический путь. При этом сканер не обращает внимание на analytics.js, потому что этот скрипт имеет атрибут async. Конечно, браузер его скачает, но только в другом потоке и с низким приоритетом. Загрузка этого скрипта не блокирует рендеринг страницы, поэтому он никак не меняет длину критического пути. К слову, на это обращают внимание алгоритмы Google, ранжирующие сайты по степени оптимизации.

Файлы с критически важными ресурсами

Третий и последний параметр нашего критического пути — количество файлов, которые браузер должен скачать для последующего рендеринга страницы. Сейчас их три: HTML, CSS и JavaScript (асинхронно загружаемый скрипт не считается). Как подсказывает логика, чем меньше будет размер файлов с критически важными ресурсами, тем лучше.

И снова о длине пути

Итак, в нашем примере браузер для рендеринга страницы должен скачать три файла (HTML, CSS и JavaScript), и он это сделает всего за два сеанса обмена данными с сервером (round trip). Вы думаете, что в описанном случае критический путь уже не может быть длиннее?

Ограничение HTTP/1.х

Протокол HTTP/1.1 накладывает ограничение на количество файлов, которые браузер может одновременно скачивать с одного хоста. В совсем старых браузерах это не больше двух файлов за раз, в Chrome — шесть, в Edge — десять. Здесь можно посмотреть, сколько одновременных подключений к одному хосту поддерживают разные версии браузеров.

Наша задача — обойти это ограничение посредством раздачи части ресурсов с теневых доменов.

Внимание! Критически важные CSS должны раздаваться только с вашего корневого домена. В противном случае длительный поиск DNS и высокая задержка могут обесценить вашу оптимизацию.

HTTP/2

Если ваш сайт и пользовательские браузеры поддерживают передачу данных по протоколу HTTP/2, вы можете забыть об ограничении на количество одновременно скачиваемых файлов. Но такое совпадение сегодня встречается не так часто, как хотелось бы.

Протестировать свой сайт на наличие поддержки HTTP/2 можно здесь.

Ограничение на объём передаваемых данных в рамках одного round trip

Ещё одна проблема!

Для предотвращения перегрузки сети и потери пакетов протокол TCP не позволяет передавать больше 14 Кб в течение одного сеанса обмена данными. Это ограничение касается всех веб-запросов, включая передачу HTML, CSS и скриптов.

Если размер HTML-файла или набора запрашиваемых ресурсов превышает 14 Кб, то нам придётся делать больше одного round trip. Иными словами, тяжёлые ресурсы существенно удлиняют наш критический путь рендеринга за счёт дополнительных запросов данных.

Больше килобайт

Давайте теперь ещё больше увеличим размер нашей страницы.

<!DOCTYPE html>
<html>
  <head>
    <title>The "Click the button" page</title>
    <link rel="stylesheet" href="styles.css">     // 14kb
    <link rel="stylesheet" href="main.css">       // 2kb
    <link rel="stylesheet" href="secondary.css">  // 2kb
    <link rel="stylesheet" href="framework.css">  // 2kb
    <script type="text/javascript" src="app.js"></script>  // 2kb
  </head>

  <body>
    <button type="button">Click me</button>

    <script type="text/javascript" src="modules.js"></script>    // 2kb
    <script type="text/javascript" src="analytics.js"></script>  // 2kb
    <script type="text/javascript" src="modernizr.js"></script>  // 2kb
  </body>
</html>

Конечно, для одной кнопки получается слишком много CSS и JavaScript, но будем считать, что это очень важная кнопка.

Допустим, мы эффективно минимизировали наш HTML и сжали его с помощью gzip. Теперь он весит всего 2 Кб и без проблем передаётся за один round trip. И браузер начинает строить DOM-дерево на основании одного файла с критически важными ресурсами — HTML.

enter image description here

Метрики CRP: длина – 1, количество файлов – 1, размер данных – 2 Кб

Затем браузер берёт CSS-файл, с помощью сканера предварительной загрузки находит все внешние ресурсы (CSS и JavaScript) и отправляет запрос на их скачивание. Но обратите внимание: CSS-файл весит 14 Кб, так что для его получения браузеру потребуется дополнительный сеанс обмена данными.

enter image description here

Метрики CRP: длина – 2, количество файлов – 2, размер данных – 16 Кб

Браузер продолжает скачивать ресурсы. Их набралось на 12 Кб, так что теоретически он может скачать их за третий round trip. Однако необходимо скачать семь файлов, при этом мы не используем протокол HTTP/2, а наш браузер — Chrome, который может одновременно скачивать только шесть файлов.

enter image description here

Метрики CRP: длина – 3, количество файлов – 8, размер данных – 28 Кб

Скачиваем последний файл и рендерим DOM.

enter image description here

Метрики CRP: длина – 4, количество файлов – 9, размер данных – 30 Кб

В результате наш критический путь рендеринга состоит из четырёх сеансов обмена данными, мы скачиваем девять файлов с ресурсами общим объёмом 30 Кб. Теперь, если мы будем знать длительность задержки при подключении у конкретного пользователя, то сможем достаточно точно прогнозировать скорость загрузки страницы.

Стратегии оптимизации работы браузера с сетью

  • PageSpeed Insights.

С помощью инструмента PageSpeed Insights мы можем определять, где у нас проблемы с производительностью. Для той же цели полезна вкладка audit в Chrome Developer Tools.

  • Использование Chrome Developer Tools.

Это необычайно полезный набор инструментов. В сети вы найдёте огромное количество руководств по его использованию, а начать можете с руководства самой Google.

  • Разработка сайтов в комфортном окружении, а тестирование — в неблагоприятных условиях.

Вероятно, вы предпочтёте работать на Macbook Pro с SSD 1 Тб и оперативной памятью 32 Гб. Но чтобы получить более полное представление о производительности своего сайта, тестировать его нужно только в неблагоприятных условиях: при медленном подключении и слабом процессоре. Это можно эмулировать во вкладке network в вышеупомянутых Chrome Developer Tools.

  • Объединение ресурсов/ файлов.

В целом после получения каждого файла с внешними стилями и скриптами браузер строит CSSOM-дерево, а затем исполняет скрипты. Поэтому, даже если вы можете доставить несколько файлов за один round trip, их обработка браузером по отдельности приведёт к избыточному расходу времени и ресурсов. И чтобы этого избежать, рекомендуется объединять несколько файлов в более крупные.

  • Размещение внутренних стилей для контента начального экрана (above the fold) в разделе header.

Я не могу однозначно сказать, что лучше: передавать CSS и JavaScript в виде внутренних или встроенных ресурсов, чтобы не приходилось получать их извне, или ради уменьшения размера DOM передавать их в виде внешних ресурсов, чтобы браузеры их кэшировали.

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

  • Минификация/ обрезка изображений.

Очевидный и простой шаг. Существует много разных способов решения этой задачи.

  • Откладывание загрузки изображений до завершения загрузки страницы.

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

  • Асинхронная загрузка шрифтов.

Из-за «дороговизны» загрузки шрифтов рекомендуется по мере возможности использовать веб-шрифты с последующим прогрессивным рендерингом шрифтов и иконок. И не забудьте заранее определить запасные шрифты, если по каким-то причинам веб-шрифты окажутся недоступны для скачивания. Возможно, предложенный подход не выглядит идеальным, но зато он позволяет избежать неприятной ситуации с FOIT (Flash Of Invisible Text).

  • Избавьтесь от лишних JavaScript/ CSS.

Вам действительно нужны все эти многочисленные скрипты и стили? Вместо каких-то скриптов старайтесь использовать нативные HTML-элементы. Также можно заменять внешние/ внутренние стили и иконки на встроенные, например, SVG.

  • CDNы.

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

Теперь вы достаточно подкованы, чтобы самостоятельно продолжить изучение оптимизации фронтенда. От себя могу порекомендовать бесплатный учебный курс на Udacity, а также Гугловскую документацию по оптимизации. Ещё глубже освоить эту тему поможет бесплатная книга High Performance Browser Networking.

Суммируем

Исключительное значение имеет критический путь рендеринга (Critical Render Path). Он даёт нам ясное понимание, какие задачи нужно решать в ходе оптимизации веб-сайта. CRP характеризуется тремя параметрами:

  1. Размер критически важных ресурсов.
  2. Количество файлов с критически важными ресурсами.
  3. Длина критического пути (количество round trip для получения данных, необходимых для рендеринга страницы).

Я дал вам начальное представление об оптимизации фронтенда под браузеры и базовые знания, которые помогут вам интерпретировать данные Google PageSpeed Insights с точки зрения производительности вашего сайта.

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

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

В следующей статье я расскажу о реальном примере внедрения всех описанных выше подходов в нашей компании. На одном домене у нас работает более 2000 страниц под управлением CMS, дающих несколько тысяч просмотров в день. Будет весело.

Автор: Badoo

Источник

Поделиться

* - обязательные к заполнению поля