Оптимизация необходима для увеличения скорости загрузки и работы сайтов, что в конечном итоге повышает удовлетворённость пользователей. Всё это делает пользователей и разработчиков счастливее, а заодно существенно улучшает ранжирование сайтов поисковиками. Например, Google уделяет особое внимание оптимизированности фронтенда. Если вы достаточно долго бились над тем, чтобы ваш сайт заработал побольше баллов в Google Pagespeed Insights, то, надеемся, эта статья поможет вам лучше понять, для чего всё это нужно и каково разнообразие стратегий оптимизации фронтенда. Недавно вся наша команда разработчиков начала использовать ReactJS. Это заставило меня задуматься, каким образом нам нужно разрабатывать наш фронтенд. Вскоре я осознал, что в нашем подходе браузер должен играть одну из главных ролей.  У нас нет возможности управлять браузером или сильно влиять на его поведение. Но поняв, как он работает, мы можем оптимизировать передаваемый ему код. К счастью, работа браузеров построена на достаточно устойчивых и хорошо задокументированных принципах, которые в долгосрочной перспективе вряд ли сильно изменятся. Так что мы можем достаточно уверенно ставить перед собой цели, к достижению которых будем стремиться. Хотя браузеры и не в нашей власти, но мы можем управлять кодом, стеком, архитектурой и паттернами. Они быстрее изменяются и обеспечивают нам более широкий спектр возможностей.  Я начал с обдумывания того, каким я вижу код, который должен получиться у нашей команды. И на основании этого видения я сформулировал мнение о том, как нужно подходить к написанию кода. В этой статье мы рассмотрим все необходимые аспекты работы браузера. Давайте рассмотрим работу браузера на примере исполнения несложного кода: После получения HTML-кода браузер парсит документ и преобразует в понятное для себя представление. Оно универсально для всех браузеров и определяется спецификацией HTML5 DOM. Далее браузер выполняет ряд действий, по завершении которых на экране отображается веб-страница. В общих чертах порядок действий такой:  Браузер считывает разметку сверху вниз и преобразует в узлы DOM-дерева.  Конечно, из любого правила есть исключения, но в целом нужно загружать стили как можно раньше, а скрипты — как можно позже. Поскольку нам нужно полностью завершить парсинг HTML и CSS прежде чем исполнять скрипты, мы доставляем стили пораньше, чтобы успеть обработать их до того, как придёт время компилировать и исполнять скрипты.  Ниже мы рассмотрим, как можно в рамках оптимизации настраивать доставку стилей и скриптов. Эти процедуры применимы ко всем данным: HTML, CSS, JavaScript, изображениям и другим ресурсам. При минификации мы удаляем «избыточные» символы: пробелы, комментарии, точки с запятой и так далее. При сжатии повторяющиеся фрагменты данных в коде и ресурсах заменяются указателями на «исходные», оригинальные, экземпляры фрагментов. Мы будем активно применять компрессию файлов, а за их распаковку будет отвечать клиентская часть. Применение минификации и компрессии позволит уменьшить объём данных примерно на 80–90%. В данном случае речь идёт не об ускорении загрузки страницы, а о проявлении заботы о пользователях с медленным подключением. Ваши страницы должны быть доступны для всех пользователей вне зависимости от скорости их каналов! Применительно к элементам страницы используйте ARIA-атрибуты, снабжайте изображения текстовыми описаниями по тэгу alt и применяйте прочие средства. Понять, что ещё можно сделать на странице для повышения её доступности, вам помогут инструменты наподобие WAVE. При обнаружении узлов стилей (внешних, внутренних или встроенных стилей) браузер прекращает рендерить DOM и на основе этих узлов строит CSSOM-дерево. Поэтому про CSS говорят, что он блокирует рендеринг. Здесь описаны некоторые преимущества и недостатки разных типов стилей. CSSOM-узлы создаются аналогично DOM-узлам. Они будут объединены позже, а на данном этапе это выглядит так:  Построение CSSOM-дерева блокирует процесс рендеринга страницы. Поэтому в рамках дерева рендеринга необходимо загружать стили как можно раньше и максимально уменьшать их размер. Там, где это будет эффективно, необходимо применять отложенную загрузку стилей. С помощью медиа-атрибутов мы определяем условия, которые нужно соблюсти для загрузки тех или иных стилей. Например, медиа-атрибуты позволяют подгружать разные стили в зависимости от разрешения экрана на клиентском устройстве, и так далее. Сегодня мобильные устройства сильно уступают в производительности «полноценным» компьютерам, поэтому для обеспечения хорошей производительности и скорости загрузки мы должны передавать им данные как можно меньшего объёма. К примеру, с помощью медиа-атрибутов можно сделать так, что сначала будут передаваться стили для мобильных устройств, а затем — для десктопов и ноутбуков. При таком подходе стили будут загружаться без блокировки загрузки самой страницы. Если у вас есть стили, которые можно безболезненно загрузить и обработать уже после отображения первого полезного экрана, то с помощью скрипта можно отложить их загрузку. Это могут быть стили для данных, которые расположены ниже первого полезного экрана, то есть чтобы их увидеть, нужно пролистать страницу. Также можно отложить загрузку стилей для данных, корректность отображения которых некритична до тех пор, пока страница не начнёт реагировать на действия пользователя, то есть не станет интерактивной. Примеры реализации отложенной загрузки стилей:
https://jakearchibald.com/2016/link-in-body/
https://www.giftofspeed.com/defer-loading-css/ Высокий уровень специфичности стилей имеет два очевидных недостатка. Во-первых, нам приходится передавать по сети больше данных, относящихся к элементам и стилям, а во-вторых, возрастает нагрузка на клиентские устройства, которым приходится всё это обрабатывать. Для многих это будет очевидным, но всё же. Если у вас есть хоть какой-то опыт разработки фронтенда, то вы наверняка знаете, что одной из ключевых проблем CSS является непредсказуемое увеличение размера стилей. Они словно специально созданы так, чтобы постоянно разрастаться. Поэтому рекомендуется использовать инструменты наподобие UnCSS, которые помогают удалить из CSS всё лишнее. Есть и немало веб-инструментов. Браузер формирует узлы DOM/ CSSOM до тех пор, пока не встретит внешние или внутренние скрипты JavaScript. Дело в том, что скрипт может захотеть получить доступ к предшествующим HTML и стилям или даже как-то преобразовать их. Поэтому браузер приостанавливает парсинг узлов, завершает построение CSSOM-дерева, исполняет скрипт, а затем возобновляет парсинг. Поэтому говорят, что JavaScript блокирует парсинг. Каждый браузер оснащён сканером предварительной загрузки, который проверяет, упоминаются ли скрипты в DOM, и предварительно их загружает. При этом загружаемые скрипты будут выполняться в предписанном порядке, но только после того, как будут сформированы предшествующие CSS-узлы. Допустим, у нас есть такой скрипт: Этот скрипт окажет такой эффект на наши DOM и CSSOM:  Стратегии оптимизации доставки JavaScript
Оптимизация работы со скриптами является одной из главных задач, решение которой на большинстве сайтов оставляет желать лучшего. Если назначить скрипту атрибут async, то браузер будет грузить его в другом потоке с более низким приоритетом без блокирования загрузки страницы. Как только скрипт будет загружен, браузер его выполнит.  Поскольку мы не знаем, когда именно будет выполнен скрипт, возможны две ситуации:  Атрибут async рекомендуется использовать со скриптами, которые не влияют на DOM или CSSOM. Также асинхронная загрузка идеально подходит для внешних скриптов, работа которых не зависит от кода и которые некритичны для UX. Яркие примеры: скрипты аналитики и отслеживания, хотя это вовсе не исчерпывающий перечень. Атрибут defer аналогичен async в том смысле, что при его использовании не блокируется загрузка страницы. Но скрипты будут выполняться в порядке появления в коде только после завершения парсинга HTML.   Это хорошее решение для скриптов, влияющих на дерево рендеринга, но некритичных для загрузки контента для первого значимого экрана, а также для работы которых не требуется запуск предшествующих скриптов. Ещё один хороший сценарий использования стратегии задержки загрузки. Также вы можете воспользоваться чем-нибудь вроде addEventListener. Полезная ветка по теме. К сожалению, async и defer бесполезны в случае со встроенными скриптами, потому что по умолчанию браузеры компилируют и выполняют их сразу же, как только встречают в коде. Если скрипт встроен в HTML, то он запускается немедленно, а применение атрибутов async и defer ко внешним ресурсам позволяет нам всего лишь абстрагироваться либо задержать передачу скриптов в DOM/ CSSOM. К этому подходу можно прибегнуть лишь при одном условии: если внесение в DOM многочисленных изменений приводит к нежелательным результатам. В этом случае попробуйте сначала клонировать весь узел DOM, внести в клон необходимые изменения, а потом заменить оригинал клоном. Так вы избежите многочисленных перерисовок и снизите нагрузку на процессор и память. Кроме того, клонирование защитит вас от FOUC (Flashes Of Unstyled Content) и внесения спонтанных изменений. Но помните, что при клонировании узла вы не клонируете слушателей событий. Хотя в некоторых случаях это как раз и может быть целью. Когда-то, когда у нас не было jQuery-методов .on() и .off(), а слушатели не умели вызывать именованные функции, мы пользовались этим подходом для сброса слушателей. У этих атрибутов «говорящие» названия, так что нет смысла объяснять, что они делают. Но появились они сравнительно недавно и пока не поддерживаются всеми браузерами. По этой причине большинству разработчиков эти атрибуты сейчас вряд ли интересны. Подробнее можете почитать о них здесь:
https://css-tricks.com/prefetching-preloading-prebrowsing/
https://www.keycdn.com/blog/resource-hints/ Все узлы прочитаны – можно объединять DOM и CSSOM. Браузер приступает к формированию дерева рендеринга. Выражаясь метафорически, узлы — это «слова», объектные модели — «предложения», а дерево — «страница».   После формирования дерева рендеринга браузер переходит к формированию макета. Для этого он определяет размеры и координаты всех элементов на странице.  Последний этап, страница появляется на экране.  Общая длительность всех этих этапов — от десятых долей секунды до нескольких секунд. И нам нужно сделать так, чтобы страницы грузились ещё быстрее.  Если какой-нибудь JavaScript вносит изменение в любую часть страницы, то заново выполняются этапы формирования дерева рендеринга и макета с последующим выводом на экран. К счастью, современные браузеры умеют перерисовывать не страницу целиком, а лишь изменённую часть. Но мы не можем полагаться в этом на них, когда речь идёт о производительности. Вместе с тем очевидно, что JavaScript во многом зависит от событий на клиентской стороне. Нам нужно, чтобы он манипулировал нашим DOM, именно этим он и займётся. Главное – не допустить или уменьшить нежелательные побочные эффекты. Кстати, рекомендую ознакомиться с выступлением Тали Гарсиэль. Оно было в 2012 году, но представленная в нём информация всё ещё актуальна. Также можете почитать прекрасный туториал по работе браузеров. Для более детального изучения вопроса штудируйте спецификацию HTML5.  Давайте теперь поговорим о том, как эффективнее всего передавать браузеру данные, необходимые для рендеринга страницы. Когда браузер совершает запрос к URL, в ответ сервер отправляет какой-то HTML-код. Пойдём от простого к сложному. Допустим, это HTML-код нашей страницы: Существует такое понятие, как «критический путь рендеринга» (Critical Rendering Path, CRP). Оно характеризует количество процедур, необходимых для рендеринга страницы. На данный момент схема нашего CRP выглядит так:  Браузер совершает GET-запрос, ожидает, пока мы отправим в ответ один килобайт HTML-кода нашей страницы (пока что мы не добавили CSS или JavaScript), затем строит DOM-дерево и рендерит страницу. У CRP есть три параметра (метрики). Первый из них — длина. Очевидно, что она должна быть как можно меньше. Для получения HTML, необходимого для рендеринга страницы, браузер обменивается данными с сервером (round trip). Больше ему пока ничего не нужно, то есть сейчас длина CRP равна 1.  Усложним задачу и добавим внутренние стили и JavaScript. Схема нашего CRP изменилась:  В связи с появлением внутренних стилей и скриптов, которые нужно обработать, критический путь увеличился на два новых этапа: построение CSSOM-дерева и выполнение скриптов. К счастью, мы не совершаем никаких внешних запросов, так что они никак не влияют на длину пути.  Но не всё так просто. Обратите внимание, что размер нашего HTML увеличился до 2 Кб. А это может (и будет) иметь неприятные последствия. Это второй параметр критического пути рендеринга. Он измеряется в количестве байтов, которые нужно передать браузеру для рендеринга страницы. Обратите внимание: речь идёт не обо всех ресурсах, которые будут в конце концов загружены на страницу, а только о тех, которые необходимы для отображения страницы, с которой пользователи смогут взаимодействовать. Как вы понимаете, размер этих ресурсов должен быть как можно меньше. Если вы думаете, что можно без проблем избавиться от внешних ресурсов, делая их только внутренними или встроенными, то вы ошибаетесь. Этот подход очень плохо масштабируется. Если помещать в современные страницы все необходимые им ресурсы, то они станут неподъёмными, да и браузеры с трудом будут их обрабатывать. Есть одна любопытная статья, в которой проанализировали производительность страниц, созданных с использованием ReactJS (внешние стили не используются, только встроенные). Выяснилось, что размер DOM увеличивается в четыре раза, и DOM-дерево строится вдвое дольше, да ещё и требуется в полтора раза больше времени, чтобы страница стала реагировать на действия пользователя.  Кроме того, внешние ресурсы можно кэшировать. Поэтому, если пользователь снова пришёл на страницу или ранее заходил на страницы, использующие те же самые ресурсы (вроде my global.css), то браузер подтянет их из кэша, а не будет запрашивать по сети. От этого мы только в выигрыше. Так что давайте использовать стили и скрипты в качестве внешних ресурсов.  На данный момент у нас один внешний СSS-файл, один внешний JavaScript-файл и один асинхронно загружаемый внешний JavaScript-файл. Вот как изменилась схема нашего критического пути:  В ответ на свой запрос браузер получает страницу и строит DOM-дерево. Как только он натыкается на любой внешний источник, начинает работать сканер предварительной загрузки. Он инициирует скачивание всех внешних ресурсов, какие только может найти в HTML. При этом CSS и JavaScript присваивается более высокий приоритет, чем другим ресурсам. Сканер находит styles.css и app.js и для их скачивания создаёт ещё один критический путь. При этом сканер не обращает внимание на analytics.js, потому что этот скрипт имеет атрибут async. Конечно, браузер его скачает, но только в другом потоке и с низким приоритетом. Загрузка этого скрипта не блокирует рендеринг страницы, поэтому он никак не меняет длину критического пути. К слову, на это обращают внимание алгоритмы Google, ранжирующие сайты по степени оптимизации. Третий и последний параметр нашего критического пути — количество файлов, которые браузер должен скачать для последующего рендеринга страницы. Сейчас их три: HTML, CSS и JavaScript (асинхронно загружаемый скрипт не считается). Как подсказывает логика, чем меньше будет размер файлов с критически важными ресурсами, тем лучше. Итак, в нашем примере браузер для рендеринга страницы должен скачать три файла (HTML, CSS и JavaScript), и он это сделает всего за два сеанса обмена данными с сервером (round trip). Вы думаете, что в описанном случае критический путь уже не может быть длиннее?  Протокол HTTP/1.1 накладывает ограничение на количество файлов, которые браузер может одновременно скачивать с одного хоста. В совсем старых браузерах это не больше двух файлов за раз, в Chrome — шесть, в Edge — десять. Здесь можно посмотреть, сколько одновременных подключений к одному хосту поддерживают разные версии браузеров. Наша задача — обойти это ограничение посредством раздачи части ресурсов с теневых доменов. Внимание! Критически важные CSS должны раздаваться только с вашего корневого домена. В противном случае длительный поиск DNS и высокая задержка могут обесценить вашу оптимизацию. Если ваш сайт и пользовательские браузеры поддерживают передачу данных по протоколу HTTP/2, вы можете забыть об ограничении на количество одновременно скачиваемых файлов. Но такое совпадение сегодня встречается не так часто, как хотелось бы.  Протестировать свой сайт на наличие поддержки HTTP/2 можно здесь. Ещё одна проблема! Для предотвращения перегрузки сети и потери пакетов протокол TCP не позволяет передавать больше 14 Кб в течение одного сеанса обмена данными. Это ограничение касается всех веб-запросов, включая передачу HTML, CSS и скриптов. Если размер HTML-файла или набора запрашиваемых ресурсов превышает 14 Кб, то нам придётся делать больше одного round trip. Иными словами, тяжёлые ресурсы существенно удлиняют наш критический путь рендеринга за счёт дополнительных запросов данных. Давайте теперь ещё больше увеличим размер нашей страницы. Конечно, для одной кнопки получается слишком много CSS и JavaScript, но будем считать, что это очень важная кнопка. Допустим, мы эффективно минимизировали наш HTML и сжали его с помощью gzip. Теперь он весит всего 2 Кб и без проблем передаётся за один round trip. И браузер начинает строить DOM-дерево на основании одного файла с критически важными ресурсами — HTML.   Метрики CRP: длина – 1, количество файлов – 1, размер данных – 2 Кб Затем браузер берёт CSS-файл, с помощью сканера предварительной загрузки находит все внешние ресурсы (CSS и JavaScript) и отправляет запрос на их скачивание. Но обратите внимание: CSS-файл весит 14 Кб, так что для его получения браузеру потребуется дополнительный сеанс обмена данными.   Метрики CRP: длина – 2, количество файлов – 2, размер данных – 16 Кб Браузер продолжает скачивать ресурсы. Их набралось на 12 Кб, так что теоретически он может скачать их за третий round trip. Однако необходимо скачать семь файлов, при этом мы не используем протокол HTTP/2, а наш браузер — Chrome, который может одновременно скачивать только шесть файлов.  Метрики CRP: длина – 3, количество файлов – 8, размер данных – 28 Кб Скачиваем последний файл и рендерим DOM.  Метрики CRP: длина – 4, количество файлов – 9, размер данных – 30 Кб В результате наш критический путь рендеринга состоит из четырёх сеансов обмена данными, мы скачиваем девять файлов с ресурсами общим объёмом 30 Кб. Теперь, если мы будем знать длительность задержки при подключении у конкретного пользователя, то сможем достаточно точно прогнозировать скорость загрузки страницы. С помощью инструмента PageSpeed Insights мы можем определять, где у нас проблемы с производительностью. Для той же цели полезна вкладка audit в Chrome Developer Tools. Это необычайно полезный набор инструментов. В сети вы найдёте огромное количество руководств по его использованию, а начать можете с руководства самой Google. Вероятно, вы предпочтёте работать на Macbook Pro с SSD 1 Тб и оперативной памятью 32 Гб. Но чтобы получить более полное представление о производительности своего сайта, тестировать его нужно только в неблагоприятных условиях: при медленном подключении и слабом процессоре. Это можно эмулировать во вкладке network в вышеупомянутых Chrome Developer Tools. В целом после получения каждого файла с внешними стилями и скриптами браузер строит CSSOM-дерево, а затем исполняет скрипты. Поэтому, даже если вы можете доставить несколько файлов за один round trip, их обработка браузером по отдельности приведёт к избыточному расходу времени и ресурсов. И чтобы этого избежать, рекомендуется объединять несколько файлов в более крупные. Я не могу однозначно сказать, что лучше: передавать CSS и JavaScript в виде внутренних или встроенных ресурсов, чтобы не приходилось получать их извне, или ради уменьшения размера DOM передавать их в виде внешних ресурсов, чтобы браузеры их кэшировали. Но зато я с уверенностью могу сказать, что стили для контента, расположенного на начальном экране, лучше передавать в виде внутренних ресурсов. В этом случае при рендеринге первого значимого экрана вам не придётся дожидаться получения внешних ресурсов. Очевидный и простой шаг. Существует много разных способов решения этой задачи. С помощью простенького «ванильного» JavaScript можно реализовать отложенную загрузку изображений, которые находятся ниже начального экрана, или необязательны для того, чтобы пользователи могли как можно раньше начать взаимодействовать со страницей. Подробнее об этом читайте здесь.  Из-за «дороговизны» загрузки шрифтов рекомендуется по мере возможности использовать веб-шрифты с последующим прогрессивным рендерингом шрифтов и иконок. И не забудьте заранее определить запасные шрифты, если по каким-то причинам веб-шрифты окажутся недоступны для скачивания. Возможно, предложенный подход не выглядит идеальным, но зато он позволяет избежать неприятной ситуации с FOIT (Flash Of Invisible Text). Вам действительно нужны все эти многочисленные скрипты и стили? Вместо каких-то скриптов старайтесь использовать нативные HTML-элементы. Также можно заменять внешние/ внутренние стили и иконки на встроенные, например, SVG. Если сети доставки контента применять для раздачи ресурсов, то для конкретного пользователя можно будет выбрать ближайшую из них, что позволит снизить задержку, а значит и сократить время загрузки. Теперь вы достаточно подкованы, чтобы самостоятельно продолжить изучение оптимизации фронтенда. От себя могу порекомендовать бесплатный учебный курс на Udacity, а также Гугловскую документацию по оптимизации. Ещё глубже освоить эту тему поможет бесплатная книга High Performance Browser Networking. Исключительное значение имеет критический путь рендеринга (Critical Render Path). Он даёт нам ясное понимание, какие задачи нужно решать в ходе оптимизации веб-сайта. CRP характеризуется тремя параметрами: Я дал вам начальное представление об оптимизации фронтенда под браузеры и базовые знания, которые помогут вам интерпретировать данные Google PageSpeed Insights с точки зрения производительности вашего сайта. В общих чертах, для уменьшения значения метрик CRP рекомендуется оптимизировать структуру DOM и процедуры работы с сетью, а также применить ряд других стратегий. Пользователи и поисковики будут счастливы. Конечно, всё будет крайне непросто реализовать применительно к крупным корпоративным сайтам, но рано или поздно вам придётся этим заняться. Поэтому перестаньте накапливать технический долг и начните внедрять устойчивые стратегии оптимизации производительности. В следующей статье я расскажу о реальном примере внедрения всех описанных выше подходов в нашей компании. На одном домене у нас работает более 2000 страниц под управлением CMS, дающих несколько тысяч просмотров в день. Будет весело.