Рефакторинг CSS: Оптимизация размера и производительности (часть 3)

Источник: «Refactoring CSS: Optimizing Size And Performance (Part 3)»
Рефакторинг кодовой базы должен привести к аналогичной или повышенной производительности и улучшению состояния кодовой базы. В конце концов, если развёртывание обновлённой базы вызовет проблемы с загрузкой или производительностью, это приведёт к снижению трафика и доходов. К счастью, существует множество методов оптимизации, которые можно применить для решения потенциальных проблем с размером файлов и производительностью.

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

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

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

Оптимизация размера файла стилей

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

Оптимизация и минификация

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

Эти инструменты работают аналогичным образом. Сначала неоптимизированный код разбирается и транспонируется в соответствии с правилами, заданными в конфигурации. В результате получается код, использующий меньшее количество символов, но сохраняющий форматирование (переносы строк и пробелы).

/* До - исходный и неоптимизированный код */
.container {
padding: 24px 16px 24px 16px;
background: #222222;
}

/* После - оптимизированный код с форматированием */
.container {
padding: 24px 16px;
background: #222;
}

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

/* Перед - оптимизированный код с форматированием */
.container {
padding: 24px 16px;
background: #222;
}

/* После - оптимизированный и минифицированный код */
.container{padding:24px 16px;background:#222}

Даже в этом базовом примере нам удалось уменьшить общий размер файла с 76 байт до 55 байт, что составляет 23%. В зависимости от кодовой базы, инструментов и конфигурации оптимизации, оптимизация и минификация CSS могут быть ещё более эффективными.

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

Оптимизация медиа-запросов

Когда при написании медиа-запросов в CSS мы используем несколько файлов (PostCSS или Sass), обычно не вкладываем код в один медиа-запрос для всего проекта. Для улучшения сопровождаемости, модульности и структуры кода мы обычно пишем одни и те же выражения медиа-запросов для нескольких компонентов CSS.

Рассмотрим следующий пример неоптимизированной кодовой базы CSS.

.page {
display: grid;
grid-gap: 16px;
}

@media (min-width: 768px) {
.page {
grid-template-columns: 268px auto;
grid-gap: 24px;
}
}

/* ... */

.products-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 16px;
}

@media (min-width: 768px) {
.products-grid {
grid-template-columns: repeat(3, 1fr);
grid-gap: 20px;
}
}

Как видите, у нас повторяется @media (min-width: 768px) для каждого компонента, что улучшает читаемость и сопровождение. Запустим оптимизацию и минификацию на этом примере кода и посмотрим, что получится.

.page{display:grid;grid-gap:16px}@media (min-width: 768px){.page{grid-template-columns:268px auto;grid-gap:24px}}.products-grid{display:grid;grid-template-columns:repeat(2,1fr);grid-gap:16px}@media (min-width: 768px){.products-grid{grid-template-columns:repeat(3,1fr);grid-gap:20px}}

Это может быть немного сложно для чтения, но все, что нам нужно заметить, — это повторяющийся медиа-запрос @media (min-width: 768px). Мы уже пришли к выводу, что хотим сократить количество символов в таблице стилей и можем вложить несколько селекторов в один медиа-запрос, так почему же минификатор не удалил дублирующееся выражение? На это есть простая причина.

Порядок правил в CSS имеет значение, поэтому для объединения дублирующихся медиа-запросов необходимо переместить блоки кода. Это приведёт к изменению порядка правил, что может вызвать нежелательные побочные эффекты в стилях.

Однако объединение медиа-запросов потенциально может сделать размер файла ещё меньше, в зависимости от кодовой базы и структуры. Такие инструменты и пакеты, как postcss-sort-media-queries, позволяют удалить дублирующиеся медиа-запросы и ещё больше уменьшить размер файла.

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

Я бы рекомендовал сначала проверить, перевешивает ли выгода от оптимизации потенциальные риски. Это можно легко сделать, проведя аудит CSS и проверив статистику медиа-запросов. Если да, то я бы рекомендовал добавить его позже и провести автоматическое регрессионное тестирование для выявления всех неожиданных побочных эффектов и ошибок, которые могут возникнуть в результате.

Удаление неиспользуемого CSS кода

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

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

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

Избавление от блокирующего рендеринг CSS

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

Пример блокирующего рендеринг CSS с зависимостью таблицы стилей и файла шрифта.
Пример блокирующего рендеринг CSS с зависимостью таблицы стилей и файла шрифта. (С сайта web.dev по лицензии Creative Commons Attribution 4.0 License)

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

За последние несколько месяцев показатель Largest Contentful Paint (LCP) стал важной метрикой. LCP важен не только для производительности, но и для SEO — сайты с лучшими показателями LCP лучше ранжируются в результатах поиска. Удаление блокирующих рендеринг ресурсов, таких как CSS, является одним из способов повышения показателя LCP.

Однако если отложить загрузку и обработку таблицы стилей, то это приведёт к появлению Flash Of Unstyled Content (FOUC) — содержимое будет отображаться пользователю сразу, а стили будут загружены и применены несколько мгновений спустя. Такое переключение может выглядеть неожиданно и даже сбить с толку некоторых пользователей.

Критический CSS

С помощью Критического CSS мы можем обеспечить загрузку сайта с минимальным количеством стилей, которые гарантированно будут использоваться на странице при её первоначальном отображении. Таким образом, мы можем сделать FOUC гораздо менее заметным или даже устранить его в большинстве случаев. Например, если на главной странице есть компонент заголовка с навигацией и компонент hero, расположенный над разворотом, то это означает, что критический CSS будет содержать все необходимые глобальные и компонентные стили для этих компонентов, а стили для других компонентов страницы будут отложены.

Этот CSS встраивается в HTML под тегом style, поэтому стили загружаются и анализируются вместе с HTML-файлом. Хотя это приведёт к некоторому увеличению размера HTML-файла (который также должен быть минифицирован), все остальные некритичные CSS будут отложены и не будут загружаться сразу, и сайт будет отображаться быстрее. В целом, преимущества перевешивают увеличение размера HTML-файла.

<head>
<style type="text/css"><!-- Minified Critical CSS markup --></style>
</head>

Существует множество автоматизированных инструментов и NPM пакетов, которые, в зависимости от вашей конфигурации, могут извлекать критические CSS и генерировать отложенные таблицы стилей.

Откладывание стилей

Как именно сделать CSS неблокируемым? Мы знаем, что на него нельзя ссылаться в элементе HTML head при первой загрузке HTML-страницы. Этот способ описал в своей статье Demian Renzulli.

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

<!-- Deferred stylesheet -->
<link rel="preload" as="style" href="path/to/stylesheet.css" onload="this.onload=null;this.rel='stylesheet'">

<!-- Fallback -->
<noscript>
<link rel="stylesheet" href="path/to/stylesheet.css">
</noscript>

С помощью ссылки rel="preload" as="style" обеспечивается асинхронный запрос файла таблицы стилей, а JavaScript-обработчик onload обеспечивает загрузку и обработку файла браузером после завершения загрузки HTML-документа. Для того чтобы эта функция не выполнялась несколько раз и не вызывала ненужных повторных рендеров, необходимо произвести очистку, поэтому для onload нужно установить значение null.

Именно так работает Smashing Magazine со своими таблицами стилей. Каждый шаблон (главная страница, категории статей, страницы статей и т.д.) имеет специфический для шаблона критический CSS, встроенный в тег style HTML в элементе head, и отложенную таблицу стилей main.css, которая содержит все некритические стили.

Однако вместо переключения параметра rel здесь мы видим, как медиа-запрос переключается с автоматически отложенного низкоприоритетного print медиа на высокоприоритетный атрибут all после завершения загрузки страницы. Это альтернативный, не менее жизнеспособный подход к отсрочке загрузки некритичных таблиц стилей.

<link href="/css/main.css" media="print" onload="this.media='all'" rel="stylesheet">

Разделение и условная загрузка стилей с помощью медиа-запросов

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

<link href="print.css" rel="stylesheet" media="print">
<link href="mobile.css" rel="stylesheet" media="all">
<link href="tablet.css" rel="stylesheet" media="screen and (min-width: 768px)">
<link href="desktop.css" rel="stylesheet" media="screen and (min-width: 1366px)">

Таким образом, если используется подход mobile-first, то стили для экранов большего размера не будут загружаться и разбираться на мобильных устройствах, которые могут работать в медленных или ненадёжных сетях.

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

Откладывание загрузки файлов шрифтов и стилей

Откладывание таблиц стилей шрифтов (например, файлов Google Font) также может быть полезно для начальной производительности рендеринга. Мы пришли к выводу, что таблицы стилей блокируют рендеринг, но и файлы шрифтов, на которые ссылается таблица стилей, тоже. Файлы шрифтов также вносят значительную долю накладных расходов в начальную производительность рендеринга.

Загрузка таблиц стилей и файлов шрифтов — сложная тема, и для того, чтобы объяснить все возможные подходы, потребовалась бы целая новая статья. К счастью, Zach Leatherman описал множество эффективных стратегий в этом замечательном исчерпывающем руководстве и обобщил плюсы и минусы каждого подхода. Если вы используете шрифты Google Fonts, Harry Roberts описал стратегию для наиболее быстрой загрузки шрифтов Google Fonts.

Если вы решите отсрочить таблицы стилей шрифтов, то в результате получите Flash of Unstyled Text (FOUT). Первоначально страница будет отображаться с резервным шрифтом до тех пор, пока не будут загружены и разобраны файлы отложенных шрифтов и таблицы стилей, после чего будут применены новые стили. Это изменение может быть очень заметным и, в зависимости от конкретного случая, может вызвать смещение макета и запутать пользователей.

Barry Pollard описал некоторые стратегии, которые могут помочь нам справиться с FOUT, и рассказал о готовящейся функции CSS size-adjust, обеспечивающей более простой и естественный способ борьбы с FOUT.

Оптимизации на стороне сервера

Сжатие HTTP

В дополнение к минификации и оптимизации размера файлов, статических активов, таких как HTML, CSS-файлы, JavaScript-файлы и т.д. Для дополнительного уменьшения размера загружаемых файлов можно использовать алгоритмы сжатия HTTP, такие, как Gzip и Brotli.

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

Кэширование таблиц стилей

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

Управление кэшированием может осуществляться с помощью HTTP-заголовка Cache-Control на уровне сервера (например, с помощью файла .htaccess на сервере Apache).

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

Cache-Control: public, max-age=604800

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

Cache-Control: public, max-age=604800, immutable

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

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

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

CDN или собственный хостинг

Сеть доставки контента (CDN) — группа географически распределённых серверов, которые обычно используются для надёжной и быстрой доставки статических активов, таких как изображения, видео, HTML-файлы, CSS-файлы, файлы JavaScript и т.д.

Хотя CDN могут показаться отличной альтернативой самостоятельному размещению статических ресурсов, Harry Roberts провёл глубокое исследование на эту тему и пришёл к выводу, что самостоятельное размещение ресурсов более выгодно с точки зрения производительности.

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

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

Аудит размера и производительности CSS-файлов

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

Давайте проведём аудит производительности сайта, упомянутого в первой статье цикла, — сайта с 2 МБ минифицированного CSS.

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

Разбивка содержимого по типу MIME (при первом просмотре).
Разбивка содержимого по типу MIME (при первом просмотре).

Ещё больше выводов мы можем сделать на основе графиков Web Vitals. Взглянув на график Largest Contentful Paint (LCP), мы можем получить подробный обзор ресурсов, блокирующих рендеринг, и того, насколько сильно они влияют на начальный рендеринг.

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

График для Largest Contentful Paint, который происходит за 8561 мс.
График для Largest Contentful Paint, который происходит за 8561 мс. Обратите внимание на оранжевую лампочку на временной шкале в списке ресурсов — эти ресурсы блокируют рендеринг.

Заключение

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

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

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

Части цикла статей Рефакторинг CSS

  1. Часть 1: Рефакторинг CSS: Введение
  2. Часть 2: Рефакторинг CSS: Стратегия, регрессионное тестирование и сопровождение
  3. Часть 3: Рефакторинг CSS: Оптимизация размера и производительности

Ссылки

Дополнительные материалы

Предыдущая Статья

Знакомство с Laravel Sushi — драйвером массива для Eloquent

Следующая Статья

Ленивая загрузка в JavaScript