Вступление
Недавно я обнаружил интересную ошибку в работе emoji-picker-element
:
Я работаю на экземпляре fedi с 19 тыс. пользовательских эмодзи [...], и когда я открываю панель выбора эмодзи [...], страница замирает как минимум на целую секунду, а после этого на некоторое время замирает общая производительность.
Если вы не знакомы с Mastodon или Fediverse, то на разных серверах могут быть свои собственные эмодзи, как в Slack, Discord и т.д. Наличие 19k (на самом деле ближе к 20k в данном случае) крайне необычно, но не является чем-то неслыханным.
Поэтому я запустил их пример, и, святые угодники, она оказалась медленной:
Здесь было несколько ошибок:
-
20 тысяч пользовательских эмодзи означали 40 тысяч элементов, поскольку каждый из них использовал
<button>
и<img>
. -
Не использовалась виртуализация, поэтому все эти элементы были просто засунуты в DOM.
К моей чести, я использовал <img loading="lazy">
, так что эти 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" и т. д.
Для каждой категории я уже знал размер эмодзи и количество строк и столбцов, поэтому вычисление ожидаемого размера можно было сделать с помощью пользовательских свойств 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?
Всякий раз, когда мне кажется, что Chrome "скрывает" от меня какую-то информацию о производительности, я делаю одно из двух: открываю chrome:tracing или (с недавних пор) включаю экспериментальную опцию "показывать все события" в DevTools.
Это дает вам немного больше низкоуровневой информации, чем стандартная трассировка Chrome, но без необходимости возиться с совершенно другим пользовательским интерфейсом. Я считаю, что это неплохой компромисс между панелью Performance и chrome:tracing
.
И в этом случае я сразу же увидел нечто, что заставило меня повернуть шестеренки в голове:
Что такое ResourceFetcher::requestResource
? Даже без поиска в исходном коде Chromium я догадывался - может быть, дело во всех этих <img>
? Не может быть, верно...? Я использую <img loading="lazy">
!
Ну, я последовал своему чутью и просто закомментировал src
из каждого <img>
, и что вы знаете - все эти загадочные расходы исчезли!
Я также протестировал в Firefox, и это также было значительным улучшением. Таким образом, это привело меня к мысли, что loading="lazy"
- не такой уж крутой, как я предполагал.
На этом этапе я решил, что если я собираюсь избавиться от loading="lazy"
, то я могу пойти на полный шаг и превратить эти 40 тысяч элементов DOM в 20 тысяч. В конце концов, если мне не нужен <img>
, то я могу использовать CSS, чтобы просто установить background-image
на псевдоэлементе ::after
на <button>
, сократив время создания этих элементов вдвое.
.onscreen .custom-emoji::after {
background-image: var(--custom-emoji-background);
}
На данный момент это был простой IntersectionObserver
для onscreen
, когда категория прокручивалась в поле зрения, и у меня был собственный loading="lazy"
, который был гораздо более производительным. На этот раз Tachometer показал улучшение на ~40% в Chrome и ~35% в Firefox. Вот это уже больше похоже на правду!
Примечание: я мог бы использовать событие
contentvisibilityautostatechange
вместоIntersectionObserver
, но я обнаружил кроссбраузерные различия, и к тому же это нагружало бы Safari, заставляя его загружать все изображения. Однако, как только поддержка браузеров улучшится, я обязательно воспользуюсь этим!
Я был рад такому решению и отправил его. В целом бенчмарк показал улучшение на ~45% как в Chrome, так и в Firefox, а оригинальный пример сократился с ~3 секунд до ~1,3 секунды. Человек, сообщивший об ошибке, даже поблагодарил меня и сказал, что теперь выборка эмодзи стала намного удобнее.
Тем не менее, что-то меня не устраивает в этой ситуации. Глядя на трассировку, я вижу, что рендеринг 20 тысяч узлов DOM просто никогда не будет таким же быстрым, как виртуализированный список. А если я захочу поддерживать еще большие экземпляры Fediverse с еще большим количеством эмодзи, это решение не будет масштабироваться.
Однако я впечатлен тем, как много вы получаете "бесплатно" с помощью content-visibility
. Тот факт, что мне не нужно было менять стратегию ARIA или беспокоиться о поиске на странице, был просто находкой. Но перфекциониста во мне все еще раздражает мысль о том, что для достижения максимального совершенства виртуальный список - это то, что нужно.
Может быть, со временем веб-платформа получит настоящий виртуальный список в качестве встроенного примитива? Несколько лет назад были попытки сделать это, но, похоже, они заглохли.
Я с нетерпением жду этого дня, а пока признаю, что content-visibility
- хорошая грубая и готовая альтернатива виртуальному списку. Она проста в реализации, дает приличный прирост производительности и не имеет практически никаких препятствий для доступа. Только не просите меня поддерживать 100 тысяч пользовательских эмодзи!
Автор: qmzik