Недавно я проводил аудит одного сайта и наткнулся на паттерн preload/polyfill
, который уже видел у нескольких клиентов. В наши дни использование этого паттерна, ранее популярного, не рекомендуется. Однако его полезно рассмотреть для того, чтобы проиллюстрировать важность осторожного использования механизма предварительной загрузки материалов веб-браузерами. Он интересен и тем, что позволяет продемонстрировать реальный пример того, как порядок элементов в документе может повлиять на производительность (именно об этом идёт речь в данном замечательном материале Гарри Робертса).
Материал, перевод которого мы сегодня публикуем, посвящён разбору ситуаций, в которых неправильное и несовременное обращение с CSS-ресурсами ухудшает работу веб-страниц.
О loadCSS
Я — большой поклонник Filament Group — они выпускают невероятное количество замечательных проектов. Кроме того, они постоянно создают бесценные инструменты и выкладывают их в общий доступ ради улучшения веба. Один из таких инструментов — это loadCSS, который долгое время был тем самым средством, который я рекомендовал всем использовать для загрузки некритических CSS-ресурсов.
Хотя теперь это изменилось (и компания Filament Group опубликовала отличную статью о том, что её сотрудники предпочитают использовать в наши дни), я всё ещё, проводя аудит сайтов, часто вижу, как loadCSS
используют в продакшне.
Один из паттернов, который мне доводилось встречать — это паттерн preload/polyfill
. Используя этот подход, любые файлы со стилями загружают в режиме предварительной загрузки (атрибут rel
соответствующих ссылок устанавливается в значение preload
). После этого, когда они будут готовы к использованию, применяют их события onload
для их подключения к странице.
<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="path/to/mystylesheet.css"></noscript>
Так как не все браузеры поддерживают конструкцию <a href="https://caniuse.com/#feat=link-rel-preload"><link rel="preload"></a>
, проект loadCSS
даёт разработчикам удобный полифилл, который добавляют на страницу после описания соответствующих ссылок:
<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.rel='stylesheet'">
<noscript>
<link rel="stylesheet" href="path/to/mystylesheet.css">
</noscript>
<script>
/*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */
(function(){ ... }());
</script>
Непорядок в сетевых приоритетах
Я никогда не был ярым фанатом этого паттерна. Предварительная загрузка — это грубоватый инструмент. Материалы, загружаемые с помощью ссылок с атрибутом rel="preload"
, с успехом борются с другими материалами за сетевые ресурсы. Использование preload
предполагает, что таблицы стилей, которые загружают асинхронно из-за того, что они не играют важнейшей роли в выводе странице, получают от браузеров очень высокий приоритет.
Следующее изображение, взятое из WebPageTest, очень хорошо демонстрирует эту проблему. В строках 3-6 можно видеть асинхронную загрузку CSS-файлов с использованием паттерна preload
. Но, в то время как разработчики считают эти файлы не настолько важными, чтобы их загрузка блокировала бы рендеринг, использование preload
означает, что они будут загружены до получения браузером остальных ресурсов.
CSS-файлы, при загрузке которых используется паттерн preload, прибывают в браузер раньше других ресурсов, даже несмотря на то, что они не являются ресурсами, чрезвычайно необходимыми при начальном рендеринге страницы
Блокировка HTML-парсера
Проблем, связанных с приоритетом загрузки ресурсов, уже достаточно для того, чтобы в большинстве ситуаций избегать применения паттерна preload
. Но в данном случае ситуация усугубляется присутствием ещё одного файла стилей, который загружается обычным образом.
<link rel="stylesheet" href="path/to/main.css" />
<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.rel='stylesheet'">
<noscript>
<link rel="stylesheet" href="path/to/mystylesheet.css">
</noscript>
<script>
/*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */
(function(){ ... }());
</script>
Тут имеются те же проблемы, когда использование preload
приводит к тому, что не самые важные файлы получают высокий приоритет. Но так же важно, и возможно, менее очевидно то, какое воздействие это оказывает на возможность браузера по парсингу страницы.
Опять же, об этом уже подробно написано, поэтому я рекомендую почитать тот материал для того, чтобы лучше понять происходящее. Тут я расскажу об этом вкратце.
Обычно загрузка стилей блокирует рендеринг страницы. Браузеру нужно запросить и распарсить стили для того, чтобы получить возможность вывести страницу. Однако это не мешает браузеру парсить остальной HTML-код.
Скрипты, с другой стороны, блокируют парсер в том случае, если они не помечены как defer
или async
.
Так как браузеру приходится предполагать, что скрипт, возможно, будет манипулировать либо самим содержимым страницы, либо стилями, применяемыми к ней, ему нужно проявлять осторожность в отношении момента запуска этого скрипта. Если браузер знает о том, что загружается какой-то CSS-код, он будет ждать прибытия этого CSS-кода, а уже после этого запустит скрипт. А так как браузер не может продолжить парсинг документа до выполнения скрипта, это означает, что стили уже не просто блокируют рендеринг. Они не дают браузеру парсить HTML.
Это поведение справедливо как для внешних скриптов, так и для скриптов, встроенных в страницу. Если CSS загружается, встроенные скрипты не запускаются до прибытия этого CSS в браузер.
Изучение проблемы
Самый понятный способ визуализации этой проблемы заключается в использовании инструментов разработчика Chrome (мне невероятно нравится тот уровень, до которого доросли эти инструменты).
Среди инструментов Chrome есть вкладка Performance
, с помощью которой можно записать профиль загрузки страницы. Я тут рекомендую искусственно замедлить сетевое соединение для того, чтобы проблема проявилась бы ярче.
В данном случае я провёл испытание, воспользовавшись настройкой сети Fast 3G. Если присмотреться к тому, что происходит с главным потоком, то можно понять, что запрос на загрузку CSS-файла происходит в самом начале парсинга HTML (примерно через 1.7 секунд после начала загрузки страницы).
Небольшой прямоугольник, находящийся ниже блока парсинга HTML, представляет запрос на получение CSS-файла
В течение следующего отрезка времени, который равняется примерно секунде, главный поток бездействует. Тут можно видеть небольшие островки деятельности. Это — срабатывание событий, указывающих на завершение загрузки стилей, это отправка механизмом предварительной загрузки ресурсов других запросов. Но браузер совершенно прекращает парсинг HTML.
Если взглянуть на общую картину, то окажется, что после начала загрузки CSS главный поток бездействует более 1.1 секунды
Итак, прошло 2.8 секунды, стиль загружен, браузер его обрабатывает. Только тогда мы видим обработку встроенного скрипта, а уже после этого браузер, наконец, возвращается к парсингу HTML.
CSS прибывает примерно через 2.8 секунды, после чего мы видим, что браузер продолжает парсинг HTML
Firefox — приятное исключение
Вышеописанное поведение характерно для Chrome, Edge и Safari. Firefox — приятное исключение из списка популярных браузеров.
Все другие браузеры приостанавливают парсинг HTML, но используют упреждающий парсер (средство предварительной загрузки материалов) для просмотра кода на предмет наличия в нём ссылок на внешние ресурсы и для выполнения запросов на загрузку этих ресурсов. Firefox, однако, идёт в этом деле на шаг дальше: он спекулятивно строит дерево DOM, даже несмотря на то, что ожидает выполнения скрипта.
Если только скрипт не будет манипулировать DOM, что приведёт к необходимости отказаться от результатов спекулятивного парсинга, этот подход позволяет Firefox получить преимущество. Конечно, если браузеру придётся отбросить спекулятивно построенное дерево DOM, это значит, что он, строя это дерево, ничего полезного не сделал.
Это — интересный подход. Мне было страшно любопытно узнать о том, насколько он эффективен. Сейчас, однако, в профилировщике производительности Firefox сведений об этом нет. Там нельзя узнать о том, работал ли спекулятивный парсер, о том, нужно ли переделывать сделанную им работу, и, о том, если её всё же нужно переделывать, как это скажется на производительности.
Я поговорил с теми, кто отвечает за инструменты разработчика Firefox, и могу сказать, что у них есть интереснейшие идеи относительно того, как в будущем представлять подобные сведения в профилировщике. Надеюсь, у них всё получится.
Решение проблемы
В случае с клиентом, о котором я упоминал в самом начале, первый шаг решения этой проблемы выглядел крайне просто: избавиться от паттерна preload/polyfill
. Предварительная загрузка некритичного CSS-кода — бессмысленное действие. Здесь нужно переключиться на использование, вместо rel="preload"
, атрибута media="print"
. Именно это и рекомендуют специалисты из Filament Group. Такой подход, кроме того, позволяет полностью избавиться от полифилла.
<link rel="stylesheet" href="/path/to/my.css" media="print" onload="this.media='all'">
Это уже помещает нас в более выгодную позицию: теперь сетевые приоритеты гораздо лучше соответствуют реальной важности загружаемых материалов. И мы, кроме того, избавляемся от встроенного скрипта.
В данном случае тут всё ещё имеется ещё один встроенный скрипт, находящийся в заголовке документа, ниже строки, инициирующей запрос на загрузку CSS. Если переместить этот скрипт так, чтобы он находился бы перед строкой, загружающей CSS, это позволит избавиться от блокировки парсера. Если снова проанализировать страницу с помощью инструментов разработчика Chrome, разница окажется совершенно очевидной.
До внесения изменений в код страницы HTML-парсер остановился на строке 1939, встретив встроенный скрипт, и оставался здесь около секунды. После оптимизации он смог добраться до строки 5281
Раньше парсер останавливался на строке 1939 и ждал загрузки CSS, а теперь он доходит до строки 5281. Там, в конце страницы, есть ещё один встроенный скрипт, который снова останавливает парсер.
Это — решение проблемы на скорую руку. Перед нами не тот вариант, который представляет собой окончательное решение проблемы. Изменение порядка элементов и избавление от паттерна preload/polyfill
— это лишь первый шаг. Лучше всего решить эту проблему можно, встроив критически важный CSS-код в страницу, а не загружая его из внешнего файла. Паттерн preload/polyfill
предназначен для использования в дополнение к встроенному CSS. Это позволяет нам полностью игнорировать проблемы, связанные со скриптами и обеспечить то, чтобы у браузера, после выполнения первого запроса, были бы все необходимые ему для рендеринга страницы стили.
Но пока, надо отметить, мы можем добиться неплохого прироста производительности, внеся в проект совсем небольшие изменения, касающиеся способа загрузки стилей и порядка элементов в DOM.
Итоги
- Если вы используете
loadCSS
и паттернpreload/polyfill
, перейдите на паттерн загрузки стилейprint
. - Если у вас имеются внешние стили, загружаемые обычным образом (то есть — с использованием обычных ссылок на файлы этих стилей), переместите все встроенные скрипты, допускающие перемещение, выше ссылок на загрузку стилей.
- Встраивайте критически важные стили в страницу для того, чтобы обеспечить как можно более быстрое начало рендеринга.
Уважаемые читатели! Сталкивались ли вы с проблемами замедления рендеринга страниц из-за CSS?
Автор: ru_vds