От автора: В последнее время очень много обсуждается оптимизация отзывчивых сайтов для улучшения производительности, и, я считаю, что это здорово. Скорость расширяет границы доступности и делает пользователей счастливыми, как собственно и сам отзывчивый дизайн.
За последний год я очень много времени уделил изучению вопроса, связанного с загрузкой страниц как в рамках текущей работы с клиентами нашей компании, так и из-за скорого выпуска моей новой книги в издательстве A Book Apart. В процессе изучения я еще раз убедился в том, что у нас нет никакой необходимости в том, чтобы жертвовать хорошо известными преимуществами отзывчивых сайтов, ради того, чтобы они стали чертовски быстро загружаться.
В данной статье я расскажу о некоторых новых наблюдениях и подходах, которые позволяют увеличить скорость работы сайта и расширить границы доступности к нему. А также я поделюсь с вами различными инструментами, которые облегчают нам работу в данном направлении. Все упомянутые приемы применяются на сайте нашей компании. Мы неоднократно их тестировали, чтобы убедиться в том, что они работают так, как надо.
Я начну с общих наблюдений, а затем перейду уже к техническим приемам. Сначала, несколько моментов о процессе загрузки страницы и о том, какова наша конечная цель.
Вес страницы — это не единственный показатель; сфокусируйтесь на ощутимой производительности
При обсуждении вопроса о том, как код, заточенный под большое количество устройств, влияет на процесс загрузки страницы, веб-разработчики в качестве одного из самых главных показателей, влияющих на успех, выделяют общий вес страницы. Однако, мы хотели бы отметить, что в то время как вес, размер в байтах, наших сайтов увеличивается (и это насущная проблема – тарифные планы дорогие!), сам по себе вес не обязательно должен увеличивать время, которое ожидает пользователь, чтобы начать пользоваться страницей. Обычно страница становится доступной для использования гораздо раньше ее полной загрузки. То как мы загружаем необходимые файлы сайта также важно, как и то, какое количество файлов мы загружаем.
Более полезным показателем для оценки скорости страницы (с точки зрения пользователя) является то время, которое должно пройти, прежде чем страница станет доступной для использования. Вы часто будете слышать, как веб-разработчики говорят об «ощутимой производительности», поскольку это понятие относится к процессу оценки производительности, которую можно дать невооруженным глазом, и именно эта оценка является наиболее значимой для наших пользователей.
В то время как этот термин может показать субъективным, ощутимую производительность достаточно легко измерить, и мы в состоянии сделать очень многое, чтобы улучшить этот показатель, вне зависимости от того, является ли сайт отзывчивым или нет. Для того чтобы измерить ощутимую производительность, нам необходимо узнать, сколько времени потребуется на то, чтобы страница стала отображаться в браузере. Сайт Webpagetest.org является потрясающим инструментом, который вы можете использовать как раз для этой цели. Просто зайдите на этот сайт, введите URL сайта, который вы хотите протестировать, и нажмите «Запустить тест».
Сайт Webpagetest делает запрос на указанный сайт из реального браузера или устройства, анализирует его загрузку и выдает вам тонны информации. Часть результатов, выданных сайтом Webpagetest непосредственно относится к ощутимой производительности, например, пункт «Начало рендеринга», показатели вы можете посмотреть на приведенной ниже таблице. Как видите, рендеринг отзывчивого сайта компании Filament начинается в промежутке между 200 и 300 миллисекундами при обычной скорости соединения (кстати, это быстро! 🙂 ).
А еще вы можете посмотреть рендеринг страницы на покадровой временной шкале. Это очень полезно для того чтобы посмотреть насколько плавно в целом происходит процесс загрузки страницы. Например, сайт Filament загружает собственные шрифты, иконки и фоновые изображения после начала рендеринга. И вы можете увидеть, как после начала рендеринга все эти файлы возникают в нужном месте с каждым новым кадром (полная версия временной шкалы находится здесь).
Чтобы рендеринг сложного макета был таким быстрым, требуется очень тщательное обдумывание всех решений, поэтому давайте посмотрим, как мы достигли такого результата.
Сокращаем критический путь
Вы, наверное, слышали про термин «критический путь», использующийся для обозначения промежутка времени между отправкой запроса к странице и ее рендерингом. Я считаю этот термин отличной метафорой, т.к. он заставляет меня представить процесс загрузки страницы в виде пути между пунктом А и пунктом Б, для прохождения которого требуется совершить определенное количество шагов. К счастью, многие из этих шагов нам подвластны. Например, CSS и JavaScript запросы могут существенно увеличить время рендеринга страницы. Это происходит потому, что по умолчанию браузеры откладывают рендеринг страницы, пока все упомянутые в теге head CSS и JavaScript файлы не будут загружены, обработаны и исполнены.
Например, вот некоторые типичные ссылки на внешние файлы, которые, во время своей загрузки, препятствуют рендерингу страницы:
1 2 |
<script src="path/to/script.js"></script> <link rel="stylesheet" href="path/to/stylesheet.css"> |
В идеале нам хотелось бы сократить наш критический путь, чтобы, насколько это возможно, он состоял из меньшего количества небольших шагов и выполнялся без всяких обходных путей (имеются в виду внешние запросы). В дополнении к этому нам бы хотелось сообщить браузеру о том, что он может выполнять определенные шаги независимо от рендеринга страницы. Мы можем сделать так, чтобы при выполнении шагов по загрузке страницы браузер следовал по одному из двух путей: либо запрашивать файлы асинхронно, чтобы они могли загружаться и исполняться, пока происходит рендеринг страницы, или использовать код, встроенный прямо в нашу HTML-страницу.
АСИНХРОННАЯ ЗАГРУЗКА
Один из подходов, позволяющих избежать блокирующих запросов, является процесс асинхронного запрашивания файлов, т.е. файлы загружаются и исполняются, следуя собственному алгоритму, независимо от рендеринга страницы. В современных браузерах этого легко можно добиться по отношению к JavaScript файлам. Нужно лишь добавить атрибут async для элемента script.
1 2 3 4 5 |
<head> ... <script src="/path/to/script.js" async></script> ... </head> |
Но… атрибут async поддерживается только последними версиями браузеров (IE 10+), и элемент script не оставляет нам никакой возможности для определения того, какой запрос должен быть выполнен в первую очередь (мы обычно сначала загружаем наш DOM-фреймворк, а после этого уже остальные скрипты, которые обеспечивают поддержку различных возможностей). Для этих целей мы обычно добавляем немного JavaScript кода для асинхронного запрашивания наших файлов: для этой цели мы используем плагин loadJS. Далее показано, как после того, как вы один раз вставили плагин loadJS на страницу, он будет использован для того чтобы запрашивать любой скрипт (я урезал исходный код плагина):
1 2 3 4 5 6 7 8 9 10 |
<head> ... <script> // сюда вставляем loadJS ... function loadJS( src ){ ... } // загружаем файл с loadJS loadJS( "path/to/script.js" ); </script> ... </head> |
Что касается асинхронного запрашивания CSS файлов: HTML не может предложить никаких собственных средств для асинхронного запрашивания таблицы стилей, поэтому нам нужно использовать немного JavaScript кода, чтобы реализовать и этот момент. Для этой цели мы используем плагин loadCSS.
Далее показано, как использовать плагин loadCSS (и снова, исходный код плагина loadCSS урезан):
1 2 3 4 5 6 7 8 9 10 |
<head> ... <script> // сюда вставляем loadCSS ... function loadCSS( href ){ ... } // загружаем файл с loadCSS loadCSS( "path/to/stylesheet.css" ); </script> ... </head> |
ВСТРОЕННЫЙ (ИНЛАЙН) КОД
Другим подходом, позволяющим избежать блокирующих запросов, является вставка содержимого тех файлов, которые необходимо будет запросить, прямо в HTML-документ. Это также называется встраиванием. CSS файлы могут быть встроены в HTML-документ с помощью элемента style, а JavaScript файлы – с помощью элемента script.
1 2 3 4 5 6 7 8 9 10 |
<head> ... <script> /* сюда помещается содержимое файла a.js! */ </script> <style> /* сюда помещается содержимое файла a.css! */ </style> ... </head> |
Встраивание является замечательным способом, который предохраняет нас от медленных, блокирующих рендеринг страницы, запросов и также позволяет нам исполнять код до того, как страница будет загружена (что очень необходимо в некоторых случаях). Но у встраивания есть некоторые недостатки, заключающиеся в том, что браузер не может кэшировать сам код, чтобы потом использовать его на последующих страницах, а также с этим процессом очень легко «переборщить», т.к. встроенный код все равно должен быть обработан до загрузки контента страницы, поэтому вам не следует использовать ничего ненужного. Стандартно, встраивание полезно использовать для критически важных частей CSS и JS кода, которые должны быть обработаны до того, как страница будет загружена, но нам следует пользоваться этим в меру.
Теперь, когда у нас есть несколько инструментов для более быстрой загрузки CSS и JS, нам нужно определить, какой код должен быть встроен в область head, а какой код мы будем запрашивать извне.
Какой код мне следует «встраивать»?
В некоторых случаях, желательно, чтобы не происходил рендеринг страницы, пока CSS и JavaScript не будут обработаны. Например, нам нужно, чтобы браузер загрузил весь необходимый для рендеринга страницы CSS-код, прежде чем браузер начнет сам рендеринг, потому что, если этого не произойдет, то пользователи увидят неоформленный контент. Также, некоторые скрипты на JavaScript работают лучше, если они будут выполнены до рендеринга страницы, например, скрипт «HTML5 shim», который позволяет старым версиям браузера IE распознавать новые HTML-элементы (другие примеры: тестирование возможностей и добавления классов).
В общем, много CSS и JS кода может быть спокойно загружено асинхронно, но обычно не все целиком. «Критические» части кода, нужные для рендеринга, должны быть встроены в HTML. Весь фокус заключается в том, чтобы правильно разбить код на части для встраивания и асинхронной загрузки.
УКОРАЧИВАЕМ ДО 14
В данной ситуации полезно будет подумать о базовых принципах того, как код передается во время загрузки страницы. Каждый запрос на удаленный сервер занимает определенное время, а каждый ответ с сервера содержит ограниченный объем данных. И для того чтобы добиться наиболее быстрой загрузки страницы, нам нужно попытаться уместить необходимый для рендеринга верхней части заданной страницы код в первый ответ с сервера, в котором может содержаться около 14 кб кода в сжатом виде (обычно меньше, но 14 — это уже хорошая цель).
Т.е. нам нужно поместить необходимый для пункта «Начало рендеринга» HTML, CSS и JavaScript код в самое первое 14-килобайтное путешествие.
ОПРЕДЕЛЯЕМ ВСТРОЕННЫЙ CSS
Предположим, что вся ваша таблица стилей может быть встроена в область head вашей страницы, суммарный вес которой при этом не будет превышать 14 кб (в сжатом виде с gzip). В этом случае все достаточно просто. И тогда вы, скорее всего, предпочтете встроить таблицу стилей в вашу страницу и на этот остановиться, т.к. ваша страница будет быстро загружаться и без каких-либо других дополнительных действий.
Конечно, на большинстве сайтов, основная таблица стилей сама по себе уже намного превышает размер в 14 кб, и если это касается и вашего сайта, то вам потребуется выделить «критическую» часть ваших стилей и встроить только ее, оставив остальную часть для асинхронной загрузки. В настоящее время веб-разработчикам доступен целый ряд новых инструментов, способных справиться с этой задачей. Одним из таких инструментов, которым мы сами пользуемся и применяем на сайте нашей компании, является Grunt Critical CSS.
Задача нашего инструмента Grunt Critical CSS состоит в том, чтобы открыть страницу в браузере из командной строки, подстроить ширину окна под определенные параметры ширины и высоты (по умолчанию это 1200 x 900) и проанализировать, какие стили применяются к элементам, которые отображаются в видимой части окна просмотра. Затем Grunt Critical CSS записывает эти стили в отдельный файл, который вы сможете использовать для встраивания на любой странице. На следующем изображении показано, для какой области страницы нам нужно встроить критический CSS – Grunt Critical CSS сгенерировал стили, которые необходимы для рендеринга верхней части страницы, и встроил их в область head нашей страницы (остальные стили буду загружены асинхронно).
На правой части изображения видно, что вначале стили для элементов, расположенных ниже критической области, не задаются (кликните по изображению для увеличения).
Для того чтобы дать задание инструменту Grunt Critical CSS, вы должны иметь общее представление о том, как ставятся задачи в Grunt (информацию вы можете найти на сайте проекта Grunt). Затем нужно произвести базовые настройки. Для этого мы ставим отдельную задачу для каждого основного шаблона нашего сайта (главная, о нас, портфолио и т.д.) для того чтобы сгенерировать файл с критическим CSS для каждого шаблона в отдельности. Далее мы встраиваем полученный CSS в каждый шаблон на стороне сервера. Вот небольшой фрагмент настроек:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
criticalcss: { home: { options: { outputfile : 'css/critical/critical-home.css', filename : 'all.css', url : '//fgwebsite.local' } }, services: { options: { outputfile : 'css/critical/critical-services.css', filename : 'all.css', url : '//fgwebsite.local/services/' } }, about: { ... |
В данных настройках мы указываем файл «all.css», т.е. это главная таблица стилей для всего сайта, которая и будет анализироваться в ходе выполнения задания. Во время процесса загрузки страницы данная таблица стилей будет асинхронно загружаться с помощью loadCSS.
Примечание: вам также следует ознакомиться с заданием Critical Node.js, которое создал Эдди Османи (Addy Osmani). Оно было создано вовремя составления данной статьи. А также познакомьтесь с заданием Penthouse, которое создал Джонас Охлсон (Jonas Ohlsson).
ОПРЕДЕЛЯЕМ ВСТРОЕННЫЙ JS
Что касается JavaScript, то мы обычно отделяем критический и не критический код путем ручной сборки отдельных файлов. Мы стремимся сделать наш JavaScript код настолько кратким и лаконичным, насколько это возможно. Поэтому в этом коде обычно содержатся только проверка возможностей и скрипты, облегчающие загрузку дополнительных файлов. Остальной JavaScript код, в котором обычно содержится DOM фреймворк, например, jQuery и различные виджеты пользовательского интерфейса, помещаются в один «улучшенный» JavaScript файл, который будет загружаться асинхронно браузерами, способными на это.
И поскольку в последнее время мы используем данный подход для каждого проекта, мы создали обновленную версию нашего проекта Enhance, который служит в качестве шаблона для JavaScript кода, который мы обычно включаем в область head нашей страницы. Вторая версия Enhance.js уже не считается библиотекой или фреймворком. Скорее это небольшой легко редактируемый файл, который можно подстроить под нужды вашего проекта. По умолчанию в файле enhance.js содержатся также и другие наши скрипты, например, loadJS и loadCSS, а также небольшой алгоритм для организации правильной работы скриптов.
Мы используем Enhance.js, чтобы проводить небольшую браузерную диагностику, прежде чем решить, нужно ли улучшать базовую, но уже функциональную страницу путем добавления классов и запрашивания дополнительных скриптов и таблиц стилей. Если вы вдруг помните написанную мной в 2008 году статью или же вам знакомо современное выражение «удовлетворяющий требованиям», придуманное на канале BBC Томом Масленом (Tom Maslen), тогда этот процесс будет вам знаком.
По умолчанию Enhance.js настроен для выполнения следующих шагов:
Определяет переменные и функции для загрузки файлов и обеспечения доступа к информации о браузере и разметке.
Запускает один или несколько тестов, чтобы определить способен ли браузер загрузить и обработать дополнительные улучшения.
А) Если он не способен, то сразу же прекращает работу и ничего больше не делает или Б) Если он способен, то приступает к загрузке дополнительных файлов, добавлению классов и т.д.
В коде этот проверочный тест (или тест под названием «удовлетворяет требованиям») часто выглядит как вот этот небольшой фрагмент кода, который проверяет, поддерживаются ли возможности querySelector и addEventListener или нет (тесты на проверку конкретных возможностей меняются от проекта к проекту):
1 2 3 |
if( "querySelector" in document && document.addEventListener ){ // способен! продолжаем улучшение |
..после чего мы обычно добавляем класс .enhanced к html-элементу для расширенных CSS возможностей и загружаем наш улучшенный JavaScript файл.
Собираем все вместе
Теперь, зная о данных подходах, мы можем вернуться в самое начало нашего разговора и посмотреть, как можно сконфигурировать область head для наших страниц. На нашем сайте (filamentgroup.com) мы начинаем с указания некоторых мета-тегов, которые определяют пути к нашим CSS и JS файлам (они пригодятся для загрузки файлов из JS), а затем включаем встроенный JavaScript и CSS код, о котором речь шла выше. И наконец, мы указываем ссылку на главную таблицу стилей с помощью обычной ссылки внутри элемента noscript, чтобы обеспечить ее загрузку только в тех случаях, когда JavaScript будет отключен. Вот упрощенный пример конфигурации, используемой на нашем сайте:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<head> ... <!-- Ссылка на главную таблицу стилей --> <meta name="css-all" content="/path/to/all.css"> <!-- Ссылка на улучшенный JS файл --> <meta name="js-enhancements" content="/path/to/enhanced.js"> <script> <% include "/path/to/initial.js" %> </script> <style> <% include "/path/to/template-xyz-critical.css" %> </style> ... <noscript> <link rel="stylesheet" href="path/to/all.css"> </noscript> </head> |
Благодаря нашему критическому CSS и JS коду, встроенному в область head нашей страницы, мы можем асинхронно загрузить остальные файлы и обеспечить тем самым грамотную и быструю загрузку нашей страницы.
ВЫГОДНО ИСПОЛЬЗУЕМ КЭШ
Описанная выше конфигурация облегчает нам выполнение задачи по обеспечению очень быстрой загрузки страницы. Но мы можем сделать еще столько всего для улучшения производительности, в частности при последующих посещениях страницы.
Вот вам один пример. При первом посещении данного сайта браузером мы устанавливаем куки (cookie) после асинхронного запрашивания определенных файлов (таких как CSS файл со всеми стилями), чтобы сообщить браузеру, что теперь, когда эти файлы были запрошены, ему не мешало бы их закэшировать. Затем, при последующих визитах на страницу, наш код на стороне сервера проверит, сохранились ли куки и если это так, то это позволит нам избежать включения встроенных стилей и просто обратиться к главной таблице стилей с помощью стандартного элемента ссылки. И это способствует более аккуратной загрузке страницы при повторных визитах. Мы также делаем это для наших шрифтов и CSS файлов с иконками.
Вы можете больше узнать о данном подходе из описания к проекту.
Сравниваем выгоды
Просто ради сравнения, я создал тестовый вариант нашей главной страницы, где CSS и JavaScript файлы загружаются стандартным (блокирующем рендеринг) способом, и проверил этот вариант с помощью сервисов «Page Speed Insights» и «WebPageTest». Наша главная страница представляет собой относительно простой макет, поэтому я не ожидал, что результаты тестов будут очень ужасными, но я обнаружил, что результаты подтвердили несомненную выгоду от применения наших рекомендаций, особенно когда условия для просмотра веб-страниц не являются идеальными.
Во-первых, в то время как настоящая версия нашего сайта набирает сейчас 100/100 на сервисе Page Speed Insights, «плохой» вариант набирает 72/100 для «мобильных» экранов и 88/100 для больших «десктопных» экранов.
Результаты на сервисе Page Speed Insights без проведения оптимизации:
Далее, результаты на сервисе WebPage Test представлены в виде таблицы. Средний показатель пункта «Начало рендеринга» при быстром соединении вырос на 900 миллисекунд, что является довольно хорошим результатом, хотя он хуже, чем показатель нашего реального сайта (295 миллисекунд). Конечно, это при условии, что все работает хорошо; что больше всего беспокоит при данном подходе так это показатели, полученные при нестабильном или слабом соединении. Например, при одной из проверок нашего тестового сайта, некоторым файлам для загрузки потребовалось больше секунды, а для загрузки самой страницы потребовалось больше 2,5 секунд!
Результаты на сервисе WebPageTest без проведения оптимизации:
Здесь следует помнить о том, что данные тесты на сервисе WebPageTest были проведены при стабильном кабельном соединении с пропускной способностью 5 Мбит/с, поэтому можно смело предположить, что эффект от блокирующих рендеринг запросов был бы гораздо более заметным при обычном мобильном 3G-соединении.
Всемирная паутина — это недружелюбная и непредсказуемая среда, в которой все может идти не так (и часто так и происходит). Очевидно, что любые действия, позволяющие нам обеспечить беспрерывный критический путь для загрузки страницы, могут представлять огромную разницу для наших пользователей.
Автор: Scott Jehl
Источник: //www.filamentgroup.com/
Редакция: Команда webformyself.
Комментарии (1)