Как мы улучшили производительность SmashingMag

Опубликовано: 2022-03-10
Краткий обзор ↬ В этой статье мы подробно рассмотрим некоторые изменения, которые мы сделали на этом самом сайте — работающем на JAMStack с React — для оптимизации веб-производительности и улучшения показателей Core Web Vitals. С некоторыми ошибками, которые мы сделали, и некоторыми неожиданными изменениями, которые помогли повысить все показатели по всем направлениям.

Каждая история веб-производительности похожа, не так ли? Все всегда начинается с долгожданного обновления сайта. День, когда проект, полностью отполированный и тщательно оптимизированный, запускается, занимает высокие позиции и превышает показатели производительности в Lighthouse и WebPageTest. В воздухе царит празднование и искреннее чувство выполненного долга, прекрасно отражающееся в ретвитах, комментариях, новостных рассылках и тредах в Slack.

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

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

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

Снимок экрана источников производительности, показывающий баллы Lighthouse от 40 до 60.
Вот с чего мы начали. Учитывая, что баллы Lighthouse находятся где-то между 40 и 60, мы решили заняться производительностью (еще раз) лицом к лицу. (Источник изображения: Lighthouse Metrics) (Большой предварительный просмотр)

Случилось так, что мы потеряли счет производительности в суете повседневной рутины. Мы проектировали и создавали вещи, настраивали новые продукты, рефакторинг компонентов и публиковали статьи. Таким образом, к концу 2020 года ситуация немного вышла из-под контроля, и желтовато-красные баллы Lighthouse постепенно стали появляться по всем направлениям. Мы должны были это исправить.

Вот где мы были

Некоторые из вас, возможно, знают, что мы работаем на JAMStack, где все статьи и страницы хранятся в виде файлов Markdown, файлы Sass скомпилированы в CSS, JavaScript разделен на фрагменты с помощью Webpack, а Hugo создает статические страницы, которые мы затем обслуживаем непосредственно из CDN Edge. Еще в 2017 году мы создали весь сайт с помощью Preact, но затем перешли на React в 2019 году и использовали его вместе с несколькими API для поиска, комментариев, аутентификации и оформления заказа.

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

На данный момент вся сборка для развертывания около 2500 статей в режиме реального времени занимает около 6 минут . Процесс сборки сам по себе со временем также стал настоящим зверем, с критическими вставками CSS, разделением кода Webpack, динамическими вставками рекламных и функциональных панелей, RSS (повторной) генерацией и, в конечном итоге, A/B-тестированием на периферии.

В начале 2020 года мы начали большой рефакторинг компонентов макета CSS. Мы никогда не использовали CSS-in-JS или стилизованные компоненты, а использовали старую добрую компонентную систему Sass-модулей, которые компилировались в CSS. Еще в 2017 году весь макет был создан с помощью Flexbox и перестроен с помощью CSS Grid и CSS Custom Properties в середине 2019 года. Однако некоторые страницы нуждались в особой обработке из-за новых рекламных роликов и панелей с новыми товарами. Таким образом, пока макет работал, он работал не очень хорошо, и его было довольно сложно поддерживать.

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

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

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

Таким образом, все эти сценарии должны были произойти в какой -то момент, и это истощало впечатления от чтения, хотя сценарий появился довольно поздно. Честно говоря, мы кропотливо работали над сайтом и новыми компонентами, не следя за производительностью (и у нас было еще несколько вещей, о которых нужно помнить на 2020 год). Переломный момент наступил неожиданно. Гарри Робертс провел с нами свой (отличный) мастер-класс по веб-производительности в качестве онлайн-семинара, и на протяжении всего семинара он использовал Smashing в качестве примера, выделяя проблемы, которые у нас были, и предлагая решения этих проблем, а также полезные инструменты и рекомендации.

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

Выявление узких мест

Мы часто склонны полагаться на конкретные оценки, чтобы понять, насколько хорошо мы работаем, но слишком часто отдельные оценки не дают полной картины. Как красноречиво заметил в своей статье Дэвид Ист, веб-производительность — это не единственная ценность; это дистрибутив. Даже если работа в Интернете сильно и тщательно оптимизирована во всех отношениях, она не может быть просто быстрой. Это может быть быстро для некоторых посетителей, но в конечном итоге это будет медленнее (или медленно) для некоторых других.

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

По сути, наша задача состоит в том, чтобы увеличить долю быстрых событий и уменьшить долю медленных событий. Но для этого нам нужно получить правильное представление о том, что такое распределение на самом деле. Теперь инструменты аналитики и инструменты мониторинга производительности будут предоставлять эти данные по мере необходимости, но мы специально рассмотрели CrUX, отчет об опыте использования Chrome. CrUX создает обзор распределения производительности с течением времени, собирая трафик от пользователей Chrome. Большая часть этих данных относится к Core Web Vitals, о которых Google объявил еще в 2020 году, и которые также вносят свой вклад и отображаются в Lighthouse.

Крупнейшая статистика Contentful Paint (LCP), показывающая резкое падение производительности в период с мая по сентябрь 2020 г.
Распределение производительности для Largest Contentful Paint в 2020 году. В период с мая по сентябрь производительность сильно упала. Данные из CrUX. (Большой превью)

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

Не потребовалось много времени, чтобы понять, что как раз в это время мы запустили новую панель навигации в прямом эфире. Эта панель навигации, используемая на всех страницах, основывалась на JavaScript для отображения элементов навигации в меню при касании или щелчке, но на самом деле часть JavaScript была включена в пакет app.js. Чтобы улучшить Time To Interactive, мы решили извлечь навигационный скрипт из пакета и использовать его как встроенный.

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

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

Теперь одной из распространенных причин регрессии является высокая стоимость JavaScript, поэтому мы также изучили Webpack Bundle Analyzer и карту запросов Саймона Херна, чтобы получить визуальное представление о наших зависимостях JavaScript. Сначала выглядел вполне здоровым.

Наглядная карта зависимостей JavaScript
На самом деле ничего революционного: карта запросов поначалу не казалась чрезмерной. (Большой превью)

Несколько запросов поступило в CDN, службу согласия на использование файлов cookie Cookiebot, Google Analytics, а также наши внутренние службы для обслуживания панелей продуктов и пользовательской рекламы. Было не так уж много узких мест — пока мы не присмотрелись повнимательнее.

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

На самом деле, поскольку мы публикуем на SmashingMag довольно много статей с большим количеством кода и дизайна, за эти годы мы накопили буквально тысячи статей, содержащих тяжелые GIF-файлы, фрагменты кода с подсветкой синтаксиса, вставки CodePen, видео / аудио. вставки и вложенные потоки бесконечных комментариев.

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

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

Учитывая эти страницы, карта выглядела немного по-другому. Обратите внимание на огромную толстую очередь, ведущую к проигрывателю Vimeo и Vimeo CDN, с 78 запросами из статьи Smashing.

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

Чтобы изучить влияние на основной поток, мы подробно изучили панель «Производительность» в DevTools. В частности, мы искали задачи, которые длятся дольше 50 мс (выделены красным прямоугольником в правом верхнем углу) и задачи, содержащие стили пересчета (фиолетовая полоса). Первое укажет на дорогостоящее выполнение JavaScript, а второе выявит недействительность стиля, вызванную динамическим внедрением контента в DOM и неоптимальным CSS. Это дало нам несколько практических советов, с чего начать. Например, мы быстро обнаружили, что загрузка нашего веб-шрифта требовала значительных затрат на перерисовку, в то время как фрагменты JavaScript все еще были достаточно тяжелыми, чтобы блокировать основной поток.

Снимок экрана панели производительности в DevTools, показывающий фрагменты JavaScript, которые все еще были достаточно тяжелыми, чтобы блокировать основной поток.
Изучаем панель Performance в DevTools. Было несколько длинных задач, которые занимали более 50 мс и блокировали основной поток. (Большой превью)

В качестве основы мы очень внимательно изучили Core Web Vitals, пытаясь убедиться, что мы хорошо оцениваем их все. Мы решили сосредоточиться именно на медленных мобильных устройствах — с медленным 3G, 400 мс RTT и скоростью передачи 400 кбит/с, просто чтобы быть пессимистичными. Поэтому неудивительно, что Lighthouse тоже был не очень доволен нашим сайтом, ставя сплошные красные баллы для самых тяжелых статей и неустанно жалуясь на неиспользуемый JavaScript, CSS, закадровые изображения и их размеры.

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

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

Улучшение порядка ресурсов в <head>

По иронии судьбы, самое первое, что мы рассмотрели, даже не было тесно связано со всеми задачами, которые мы определили выше. На семинаре по производительности Гарри потратил значительное количество времени на объяснение порядка ресурсов в <head> каждой страницы, подчеркнув, что для быстрой доставки важного контента необходимо быть очень стратегическим и внимательным к тому, как ресурсы упорядочены в исходном коде. .

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

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

В общем, мы тщательно организовали порядок в <head> , уменьшили количество предварительно загруженных ресурсов, конкурирующих за пропускную способность, и сосредоточились на правильном получении критически важного CSS. Если вы хотите глубже погрузиться в некоторые критические соображения, связанные с порядком <head> , Гарри выделяет их в статье CSS и производительность сети. Одно только это изменение принесло нам около 3–4 очков очков Lighthouse по всем направлениям.

Переход от автоматического критического CSS к ручному критическому CSS

Однако перемещение тегов <head> было простой частью истории. Более сложным было создание и управление критически важными файлами CSS. Еще в 2017 году мы вручную создали важный CSS для каждого шаблона, собрав все стили, необходимые для рендеринга первых 1000 пикселей в высоту на экранах любой ширины. Это, конечно, было громоздкой и немного скучной задачей, не говоря уже о проблемах с обслуживанием для укрощения целого семейства критических файлов CSS и полного файла CSS.

Поэтому мы рассмотрели варианты автоматизации этого процесса как части процедуры сборки. На самом деле недостатка в доступных инструментах не было, поэтому мы протестировали несколько и решили провести несколько тестов. Нам удалось настроить их и запустить довольно быстро. Результат оказался достаточно хорошим для автоматизированного процесса, поэтому после нескольких настроек конфигурации мы подключили его и запустили в производство. Это произошло где-то в июле-августе прошлого года, что хорошо видно по всплеску и падению производительности в приведенных выше данных CrUX. Мы постоянно возвращались к настройке, часто возникали проблемы с простыми вещами, такими как добавление определенных стилей или удаление других. Например, стили запроса согласия на использование файлов cookie, которые на самом деле не включаются на страницу, если не инициализирован сценарий файлов cookie.

В октябре мы внесли некоторые серьезные изменения в макет сайта, и при изучении критически важного CSS мы снова столкнулись с точно такими же проблемами — сгенерированный результат был довольно подробным и не совсем таким, как мы хотели. . Поэтому в качестве эксперимента в конце октября мы все объединили свои силы, чтобы пересмотреть наш критический подход к CSS и изучить, насколько меньше будет созданный вручную критический CSS . Мы глубоко вздохнули и провели дни с инструментом покрытия кода на ключевых страницах. Мы сгруппировали CSS-правила вручную и удалили дубликаты и устаревший код в обоих местах — критически важном CSS и основном CSS. Это была действительно необходимая чистка, так как многие стили, написанные еще в 2017–2018 годах, с годами устарели.

В результате мы получили три критически важных CSS-файла ручной работы и еще три файла, над которыми сейчас ведется работа:

  • Critical-homepage-manual.css (8,2 КБ, Brotlified)
  • Critical-article-manual.css (8 КБ, Brotlified)
  • Critical-articles-manual.css (6 КБ, Brotlified)
  • Critical-books-manual.css (предстоящая работа )
  • Critical-events-manual.css (предстоящая работа )
  • Critical-job-board-manual.css (предстоящая работа )

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

Следует признать, что созданные вручную критические файлы CSS были ненамного меньше по размеру: мы уменьшили размер критических файлов CSS примерно на 14% . Однако они включали все, что нам было нужно, в правильном порядке от начала до конца, без дубликатов и переопределяющих стилей. Это казалось шагом в правильном направлении, и это дало нам прирост Lighthouse еще на 3–4 балла. Мы делали успехи.

Изменение загрузки веб-шрифта

Когда у нас под рукой есть font-display , загрузка шрифтов, похоже, была проблемой в прошлом. К сожалению, в нашем случае это не совсем правильно. Вы, уважаемые читатели, кажется, посещаете ряд статей Smashing Magazine. Вы также часто возвращаетесь на сайт, чтобы прочитать еще одну статью — возможно, через несколько часов или дней, а может быть, через неделю. Одна из проблем, с которой мы столкнулись при использовании font-display на сайте, заключалась в том, что у читателей, которые много перемещались между статьями, мы заметили множество вспышек между резервным шрифтом и веб-шрифтом (чего обычно не должно происходить, поскольку шрифты правильно кэшируется).

Это не было похоже на приличный пользовательский интерфейс, поэтому мы рассмотрели варианты. На Smashing мы используем два основных шрифта — Mija для заголовков и Elena для основного текста. Мия имеет два начертания (обычный и жирный), а Елена — три начертания (обычный, курсив, жирный). Мы отказались от полужирного курсивного начертания Елены несколько лет назад во время редизайна только потому, что использовали его всего на нескольких страницах. Мы подмножаем другие шрифты, удаляя неиспользуемые символы и диапазоны Unicode.

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

Внимательно посмотрите на первоначальную загрузку главной страницы (замедление в три раза):

При поиске решения у нас было четыре основные цели:

  1. При первом посещении сразу же визуализируйте текст резервным шрифтом;
  2. Сопоставьте метрики шрифтов резервных шрифтов и веб-шрифтов, чтобы свести к минимуму сдвиги макета;
  3. Загружать все веб-шрифты асинхронно и применять их все сразу (макс. 1 перекомпоновка);
  4. При последующих посещениях визуализируйте весь текст непосредственно в веб-шрифтах (без перепрошивки и перекомпоновки).

Изначально мы действительно пытались использовать font-display: swap on font-face . Это казалось самым простым вариантом, однако, как упоминалось выше, некоторые читатели посетят несколько страниц, поэтому в итоге мы получили много мерцаний с шестью шрифтами, которые мы отображали по всему сайту. Кроме того, с одним только отображением шрифта мы не могли группировать запросы или перерисовки.

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

Так что тогда?

С 2017 года мы используем подход Two-Stage-Render для загрузки веб-шрифтов, который в основном описывает два этапа рендеринга: один с минимальным подмножеством веб-шрифтов, а другой — с полным набором весов шрифтов. Когда-то мы создали минимальные подмножества Mija Bold и Elena Regular, которые были наиболее часто используемыми весами на сайте. Оба подмножества включают только латинские символы, знаки препинания, цифры и несколько специальных символов. Эти шрифты ( ElenaInitial.woff2 и MijaInitial.woff2 ) были очень маленькими по размеру — часто всего около 10–15 КБ. Мы обслуживаем их на первом этапе рендеринга шрифтов, отображая всю страницу в этих двух шрифтах.

CLS, вызванный мерцанием веб-шрифтов
CLS, вызванный мерцанием веб-шрифтов (тени под авторскими изображениями двигаются из-за смены шрифта). Генерируется с помощью Layout Shift GIF Generator. (Большой превью)

Мы делаем это с помощью API загрузки шрифтов, который дает нам информацию о том, какие шрифты успешно загружены, а какие еще нет. За кулисами это происходит путем добавления класса .wf-loaded-stage1 в body со стилями, отображающими содержимое в этих шрифтах:

 .wf-loaded-stage1 article, .wf-loaded-stage1 promo-box, .wf-loaded-stage1 comments { font-family: ElenaInitial,sans-serif; } .wf-loaded-stage1 h1, .wf-loaded-stage1 h2, .wf-loaded-stage1 .btn { font-family: MijaInitial,sans-serif; }

Поскольку файлы шрифтов довольно малы, мы надеемся, что они довольно быстро передаются по сети. Затем, когда читатель может начать читать статью, мы асинхронно загружаем полные веса шрифтов и добавляем .wf-loaded-stage2 в тело :

 .wf-loaded-stage2 article, .wf-loaded-stage2 promo-box, .wf-loaded-stage2 comments { font-family: Elena,sans-serif; } .wf-loaded-stage2 h1, .wf-loaded-stage2 h2, .wf-loaded-stage2 .btn { font-family: Mija,sans-serif; }

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

 /* Loading web fonts with Font Loading API to avoid multiple repaints. With help by Irina Lipovaya. */ /* Credit to initial work by Zach Leatherman: https://noti.st/zachleat/KNaZEg/the-five-whys-of-web-font-loading-performance#sWkN4u4 */ // If the Font Loading API is supported... // (If not, we stick to fallback fonts) if ("fonts" in document) { // Create new FontFace objects, one for each font let ElenaRegular = new FontFace( "Elena", "url(/fonts/ElenaWebRegular/ElenaWebRegular.woff2) format('woff2')" ); let ElenaBold = new FontFace( "Elena", "url(/fonts/ElenaWebBold/ElenaWebBold.woff2) format('woff2')", { weight: "700" } ); let ElenaItalic = new FontFace( "Elena", "url(/fonts/ElenaWebRegularItalic/ElenaWebRegularItalic.woff2) format('woff2')", { style: "italic" } ); let MijaBold = new FontFace( "Mija", "url(/fonts/MijaBold/Mija_Bold-webfont.woff2) format('woff2')", { weight: "700" } ); // Load all the fonts but render them at once // if they have successfully loaded let loadedFonts = Promise.all([ ElenaRegular.load(), ElenaBold.load(), ElenaItalic.load(), MijaBold.load() ]).then(result => { result.forEach(font => document.fonts.add(font)); document.documentElement.classList.add('wf-loaded-stage2'); // Used for repeat views sessionStorage.foutFontsStage2Loaded = true; }).catch(error => { throw new Error(`Error caught: ${error}`); }); }

Однако что, если первое небольшое подмножество шрифтов не проходит по сети быстро? Мы заметили, что это происходит чаще, чем нам бы хотелось. В этом случае, после истечения тайм-аута в 3 секунды, современные браузеры возвращаются к системному шрифту (в нашем стеке шрифтов это будет Arial), затем переключаются на ElenaInitial или MijaInitial , чтобы позже переключиться на полный Elena или Mija соответственно. . Это произвело слишком много мигания при нашей дегустации. Сначала мы думали убрать рендер первого этапа только для медленных сетей (через Network Information API), но потом решили убрать его вообще.

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

Для этого мы использовали font-style-matcher и (кхм, кхм) несколько магических чисел. Это также причина, по которой мы изначально использовали -apple-system и Arial в качестве глобальных запасных шрифтов; Сан-Франциско (рендеринг через -apple-system ) казался немного лучше, чем Arial, но если он недоступен, мы решили использовать Arial только потому, что он широко распространен в большинстве операционных систем.

В CSS это будет выглядеть так:

 .article__summary { font-family: -apple-system,Arial,BlinkMacSystemFont,Roboto Slab,Droid Serif,Segoe UI,Ubuntu,Cantarell,Georgia,sans-serif; font-style: italic; /* Warning: magic numbers ahead! */ /* San Francisco Italic and Arial Italic have larger x-height, compared to Elena */ font-size: 0.9213em; line-height: 1.487em; } .wf-loaded-stage2 .article__summary { font-family: Elena,sans-serif; font-size: 1em; /* Original font-size for Elena Italic */ line-height: 1.55em; /* Original line-height for Elena Italic */ }

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

Как только шрифты загружены, мы сохраняем их в кэше сервис-воркера. При последующих посещениях мы сначала проверяем, находятся ли уже шрифты в кеше. Если они есть, мы извлекаем их из кеша сервис-воркера и применяем их немедленно. А если нет, то начинаем все сначала с fallback-web-font-switcheroo .

Это решение сократило количество перекомпоновок до минимума (одна) при относительно быстрых соединениях, а также постоянно и надежно сохраняло шрифты в кеше. В будущем мы искренне надеемся заменить магические числа на f-mods. Возможно, Зак Лезерман был бы горд.

Выявление и разрушение монолитного JS

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

  • uc.js , скрипт подсказки cookie (70 мс)
  • пересчет стилей, вызванный входящим файлом full.css (176 мс) (критический CSS не содержит стилей ниже высоты 1000 пикселей во всех окнах просмотра)
  • рекламные скрипты, запускаемые по событию загрузки для управления панелями, корзиной и т. д. + пересчет стилей (276 мс)
  • переключение веб-шрифта, перерасчет стиля (290 мс)
  • оценка app.js (580 мс)

Сначала мы сосредоточились на самых вредных — так сказать, на самых длинных Long Tasks.

Снимок экрана, сделанный из DevTools, показывающий проверку стиля для главной страницы сокрушительного журнала.
Внизу Devtools показывает инвалидацию стиля — переключение шрифта затронуло 549 элементов, которые пришлось перекрасить. Не говоря уже об изменениях макета, которые это вызывало. (Большой превью)

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

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

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

Кстати, очень важно отметить, что в нашем случае мы заметили, что большинство длинных задач были вызваны не массивным JavaScript, а пересчетом макета и разбором CSS, а это означало, что нам нужно было немного поработать с CSS. cleaning, especially watching out for situations when styles are overwritten. In some way, it was good news because we didn't have to deal with complex JavaScript issues that much. However, it turned out not to be straightforward as we are still cleaning up the CSS this very day. We were able to remove two Long Tasks for good, but we still have a few outstanding ones and quite a way to go. Fortunately, most of the time we aren't way above the magical 50ms threshold.

The much bigger issue was the JavaScript bundle we were serving, occupying the main thread for a whopping 580ms. Most of this time was spent in booting up app.js which contains React, Redux, Lodash, and a Webpack module loader. The only way to improve performance with this massive beast was to break it down into smaller pieces. So we looked into doing just that.

With Webpack, we've split up the monolithic bundle into smaller chunks with code-splitting , about 30Kb per chunk. We did some package.json cleansing and version upgrade for all production dependencies, adjusted the browserlistrc setup to address the two latest browser versions, upgraded to Webpack and Babel to the latest versions, moved to Terser for minification, and used ES2017 (+ browserlistrc) as a target for script compilation.

We also used BabelEsmPlugin to generate modern versions of existing dependencies. Finally, we've added prefetch links to the header for all necessary script chunks and refactored the service worker, migrating to Workbox with Webpack (workbox-webpack-plugin).

A screenshot showing JavaScript chunks affecting performance with each running no longer than 40ms on the main thread
JavaScript chunks in action, with each running no longer than 40ms on the main thread. (Большой превью)

Remember when we switched to the new navigation back in mid-2020, just to see a huge performance penalty as a result? The reason for it was quite simple. While in the past the navigation was just static plain HTML and a bit of CSS, with the new navigation, we needed a bit of JavaScript to act on opening and closing of the menu on mobile and on desktop. That was causing rage clicks when you would click on the navigation menu and nothing would happen, and of course, had a penalty cost in Time-To-Interactive scores in Lighthouse.

We removed the script from the bundle and extracted it as a separate script . Additionally, we did the same thing for other standalone scripts that were used rarely — for syntax highlighting, tables, video embeds and code embeds — and removed them from the main bundle; instead, we granularly load them only when needed.

Performance stats for the smashing magazine front page showing the function call for nav.js that happened right after a monolithic app.js bundle had been executed
Notice that the function call for nav.js is happening after a monolithic app.js bundle is executed. That's not quite right. (Большой превью)

However, what we didn't notice for months was that although we removed the navigation script from the bundle, it was loading after the entire app.js bundle was evaluated, which wasn't really helping Time-To-Interactive (see image above). We fixed it by preloading nav.js and deferring it to execute in the order of appearance in the DOM, and managed to save another 100ms with that operation alone. By the end, with everything in place we were able to bring the task to around 220ms.

A screenshot of the the Long task reduced by almost 200ms
By prioritizing the nav.js script, we were able to reduce the Long task by almost 200ms. (Большой превью)

We managed to get some improvement in place, but still have quite a way to go, with further React and Webpack optimizations on our to-do list. At the moment we still have three major Long Tasks — font switch (120ms), app.js execution (220ms) and style recalculations due to the size of full CSS (140ms). For us, it means cleaning up and breaking up the monolithic CSS next.

It's worth mentioning that these results are really the best-scenario- results. On a given article page we might have a large number of code embeds and video embeds, along with other third-party scripts and customer's browser extensions that would require a separate conversation.

Dealing With 3rd-Parties

Fortunately, our third-party scripts footprint (and the impact of their friends' fourth-party-scripts) wasn't huge from the start. But when these third-party scripts accumulated, they would drive performance down significantly. This goes especially for video embedding scripts , but also syntax highlighting, advertising scripts, promo panels scripts and any external iframe embeds.

Obviously, we defer all of these scripts to start loading after the DOMContentLoaded event, but once they finally come on stage, they cause quite a bit of work on the main thread. This shows up especially on article pages, which are obviously the vast majority of content on the site.

The first thing we did was allocating proper space to all assets that are being injected into the DOM after the initial page render. It meant width and height for all advertising images and the styling of code snippets. We found out that because all the scripts were deferred, new styles were invalidating existing styles, causing massive layout shifts for every code snippet that was displayed. We fixed that by adding the necessary styles to the critical CSS on the article pages.

We've re-established a strategy for optimizing images (preferably AVIF or WebP — still work in progress though). All images below the 1000px height threshold are natively lazy-loaded (with <img loading=lazy> ), while the ones on the top are prioritized ( <img loading=eager> ). The same goes for all third-party embeds.

We replaced some dynamic parts with their static counterparts — eg while a note about an article saved for offline reading was appearing dynamically after the article was added to the service worker's cache, now it appears statically as we are, well, a bit optimistic and expect it to be happening in all modern browsers.

As of the moment of writing, we're preparing facades for code embeds and video embeds as well. Plus, all images that are offscreen will get decoding=async attribute, so the browser has a free reign over when and how it loads images offscreen, asynchronously and in parallel.

A screenshot of the main front page of smashing magazine being highlighted by the Diagnostics CSS tool for each image that does not have a width/height attribute
Diagnostics CSS in use: highlighting images that don't have width/height attributes, or are served in legacy formats. (Большой превью)

To ensure that our images always include width and height attributes, we've also modified Harry Roberts' snippet and Tim Kadlec's diagnostics CSS to highlight whenever an image isn't served properly. It's used in development and editing but obviously not in production.

One technique that we used frequently to track what exactly is happening as the page is being loaded, was slow-motion loading .

First, we've added a simple line of code to the diagnostics CSS, which provides a noticeable outline for all elements on the page.

* { outline: 3px solid red }
* { outline: 3px solid red } 
A screenshot of an article published on smashing magazine with red lines on the layout to help check the stability and rendering on the page
A quick trick to check the stability of the layout, by adding * { outline: 3px red } and observing the boxes as the browser is rendering the page. (Большой превью)

Then we record a video of the page loaded on a slow and fast connection. Then we rewatch the video by slowing down the playback and moving back and forward to identify where massive layout shifts happen.

Here's the recording of a page being loaded on a fast connection:

Recording for the loading of the page with an outline applied, to observe layout shifts.

And here's the recording of a recording being played to study what happens with the layout:

Auditing the layout shifts by rewatching a recording of the site loading in slow motion, watching out for height and width of content blocks, and layout shifts.

By auditing the layout shifts this way, we were able to quickly notice what's not quite right on the page, and where massive recalculation costs are happening. As you probably have noticed, adjusting the line-height and font-size on headings might go a long way to avoid large shifts.

With these simple changes alone, we were able to boost performance score by a whopping 25 Lighthouse points for the video-heaviest article, and gain a few points for code embeds.

Enhancing The Experience

We've tried to be quite strategic in pretty much everything from loading web fonts to serving critical CSS. However, we've done our best to use some of the new technologies that have become available last year.

We are planning on using AVIF by default to serve images on SmashingMag, but we aren't quite there yet, as many of our images are served from Cloudinary (which already has beta support for AVIF), but many are directly from our CDN yet we don't really have a logic in place just yet to generate AVIFs on the fly. That would need to be a manual process for now.

We're lazy rendering some of the offset components of the page with content-visibility: auto . For example, the footer, the comments section, as well as the panels way below the first 1000px height threshold, are all rendered later after the visible portion of each page has been rendered.

Мы немного поиграли со link rel="prefetch" и даже со link rel="prerender" (предварительная выборка NoPush) на некоторые части страницы, которые, скорее всего, будут использоваться для дальнейшей навигации — например, для предварительной загрузки ресурсов для первого статьи на первой полосе (все еще обсуждаются).

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

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

Работа, которую нужно сделать

Мы потратили довольно значительное количество времени, работая над всеми мелкими и крупными изменениями на сайте. Мы заметили довольно значительные улучшения на настольных компьютерах и заметный прирост на мобильных устройствах. На момент написания наши статьи набирают в среднем от 90 до 100 баллов Lighthouse на ПК и около 65-80 баллов на мобильных устройствах .

Оценка Lighthouse на рабочем столе показывает от 90 до 100.
Оценка производительности на рабочем столе. Домашняя страница уже сильно оптимизирована. (Большой превью)
Оценка Lighthouse в мобильных шоу от 65 до 80.
На мобильных устройствах мы едва ли когда-либо достигаем показателя Lighthouse выше 85. Основными проблемами по-прежнему являются время до взаимодействия и общее время блокировки. (Большой превью)

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

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

Мы также изучаем варианты дальнейших экспериментов с пакетированием на мобильных устройствах, чтобы уменьшить влияние app.js на производительность, хотя на данный момент это кажется нетривиальным. Наконец, мы рассмотрим альтернативы нашему решению для запроса файлов cookie, перестроим наши контейнеры с помощью CSS clamp() , заменим метод отношения нижнего края на aspect-ratio и рассмотрим возможность предоставления как можно большего количества изображений в AVIF.

Вот оно, народ!

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

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

Наконец, если вы хотите быть в курсе статей, подобных этой, подпишитесь на нашу электронную рассылку, чтобы получать полезные веб-советы, полезные материалы, инструменты и статьи, а также сезонную подборку Smashing Cats.