Вступление
Недавно я обнаружил интересную ошибку в работе 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 тысяч пользовательских эмодзи!
Aquahawk
То, что виртуальный дом написанный на js производительнее нативного дома является для меня непостижимой вещью. И уж тем более непостижимо зачем добавлять сразу все десятки тысяч айтемов в список, еще ios 3 в UITableView списки делались так чтобы переиспользовать объекты строк и менять в них контент, чтобы пооизводительность списка не зависела от количества элементов.
cyberhippie
Знаешь, есть саркастический термин модернфутбол? Вот такое и модернпрограммирование!))
director-rentv
Почему написанный на жс виртуальный дом быстрее - так он попросту легче, каждая реализация заботится только о том, что и как нужно ей, в то время как браузер тащит за собой огромную кучу обратной совместимости + других вещей, которые в конкретной ситуации для тех же фреймворков (коль скоро в виртуальном доме заговорили) к черту не нужны. Когда разрабы браузера делают реально нативную реализацию чего-нибудь эдакого изолированного (strucuredClone, JSON.stringify/parse например), она почти всегда на порядок быстрее накостыленного на жсе варианта
Про базовую виртуализацию и пример с UITableView можно сказать только то, что покуда в мобилках у нас есть единый поставщик базовых компонентов, единые гайдлайны и прочее, то в вебе этого попросту нет. Сейчас есть грустные попытки сделать подобные штуки (в частности, помню кастомизируемый селект, но он, вроде бы, заглох). А без этого кто во что горазд. Вон, Карловский со своим $mol носится (без негатива :)), там-сям кто-то чето на конфе ляпнет про виртуализацию, кто-то чето на каком-то фреймворке с миллионом ограничений реализует, а суммарно воз вроде и ныне там: каждая компания - сам себе велосипедостроительный завод, поставляющий отборные костыли.
Ну это я утрирую, конечно. Разрабы браузеров и комитеты стандартизации действительно трудятся на благо рядовых формошлепов, и время от времени шикарные штуки уровня ResizeObserver появляются. Сейчас вот добивают гвозди в крышку гроба костыльных модалок и тултипов