[Перевод] Улучшение производительности рендеринга с помощью CSS content-visibility

Вступление

Недавно я обнаружил интересную ошибку в работе emoji-picker-element:

Я работаю на экземпляре fedi с 19 тыс. пользовательских эмодзи […], и когда я открываю панель выбора эмодзи […], страница замирает как минимум на целую секунду, а после этого на некоторое время замирает общая производительность.

Если вы не знакомы с Mastodon или Fediverse, то на разных серверах могут быть свои собственные эмодзи, как в Slack, Discord и т.д. Наличие 19k (на самом деле ближе к 20k в данном случае) крайне необычно, но не является чем-то неслыханным.

Поэтому я запустил их пример, и, святые угодники, она оказалась медленной:

c132bc86ae6dfa032f251e97281f9c39.png

Здесь было несколько ошибок:

  • 20 тысяч пользовательских эмодзи означали 40 тысяч элементов, поскольку каждый из них использовал

  • Не использовалась виртуализация, поэтому все эти элементы были просто засунуты в DOM.

К моей чести, я использовал , так что эти 20 тысяч изображений не загружались все сразу. Но несмотря ни на что, рендеринг 40 тысяч элементов будет ужасно медленным — Lighthouse рекомендует не более 1 400!

Первой моей мыслью, конечно, было: «У кого, черт возьми, есть 20 тысяч пользовательских эмодзи?». Второй мыслью было:»Вздох Похоже, мне придется заняться виртуализацией».

Я старательно избегал виртуализации в emoji-picker-element, а именно потому, что 1) это сложно, 2) я не думал, что мне это нужно, и 3) это влияет на доступность.

Я уже проходил этот путь: Pinafore — это, по сути, один большой виртуальный список. Я использовал роль ARIA feed, сделал все вычисления самостоятельно и добавил опцию отключения «бесконечной прокрутки», поскольку некоторым людям она не нравится. Это не первое мое родео! Я просто с ужасом думал о том, сколько кода мне придется написать, и задавался вопросом о том, как это отразится на размере моего «крошечного» ~12kB emoji picker.

Однако через несколько дней мне в голову пришла мысль:, а как насчет CSS content-visibility? Я видел, что много времени тратится на верстку и рисование, и плюс это могло бы помочь «заиканию». Это может быть гораздо более простым решением, чем полная виртуализация.

Если вы не знакомы, content-visibility — это новая функция CSS, которая позволяет «скрывать» определенные части DOM с точки зрения верстки и рисования. Она в основном не влияет на дерево доступности (поскольку узлы DOM все еще там), не влияет на поиск на странице (⌘+F/Ctrl+F) и не требует виртуализации. Все, что ему нужно, — это оценка размеров внеэкранных элементов, чтобы браузер мог зарезервировать там место.

К счастью для меня, у меня была хорошая атомарная единица для определения размера: категории эмодзи. Пользовательские эмодзи на Fediverse, как правило, делятся на небольшие категории: «blobs», «cats» и т. д.

9363f291e497184db061d894d28c2d59.png

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

.category {
  content-visibility: auto;
  contain-intrinsic-size:
    /* width */
    calc(var(--num-columns) * var(--total-emoji-size))
    /* height */
    calc(var(--num-rows) * var(--total-emoji-size));
}

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

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

Мой первый бенчмарк был очень прост в написании, и прирост производительности был налицо… Он просто немного разочаровал.

При начальной загрузке я получил примерно 15% улучшения в Chrome и 5% в Firefox. (В Safari content-visibility есть только в Technology Preview, поэтому я не могу проверить ее в Tachometer). Это не повод для беспокойства, но я знал, что виртуальный список может работать гораздо лучше!

Поэтому я копнул немного глубже. Затраты на верстку почти исчезли, но остались другие затраты, которые я не мог объяснить. Например, что это за большой неопознанный сгусток в трассировке Chrome?

661855bb65bfef09c86bb7ace031acc1.png

Всякий раз, когда мне кажется, что Chrome «скрывает» от меня какую-то информацию о производительности, я делаю одно из двух: открываю chrome: tracing или (с недавних пор) включаю экспериментальную опцию «показывать все события» в DevTools.

Это дает вам немного больше низкоуровневой информации, чем стандартная трассировка Chrome, но без необходимости возиться с совершенно другим пользовательским интерфейсом. Я считаю, что это неплохой компромисс между панелью Performance и chrome:tracing.

И в этом случае я сразу же увидел нечто, что заставило меня повернуть шестеренки в голове:

7a0d0234606263fb066c3d21199e9b9b.png

Что такое ResourceFetcher::requestResource? Даже без поиска в исходном коде Chromium я догадывался — может быть, дело во всех этих ? Не может быть, верно…? Я использую !

Ну, я последовал своему чутью и просто закомментировал src из каждого , и что вы знаете — все эти загадочные расходы исчезли!

Я также протестировал в Firefox, и это также было значительным улучшением. Таким образом, это привело меня к мысли, что loading="lazy" — не такой уж крутой, как я предполагал.

На этом этапе я решил, что если я собираюсь избавиться от loading="lazy", то я могу пойти на полный шаг и превратить эти 40 тысяч элементов DOM в 20 тысяч. В конце концов, если мне не нужен , то я могу использовать CSS, чтобы просто установить background-image на псевдоэлементе ::after на