TL;DR: повторно используйте элементы DOM и удалите те, которые находятся далеко от области просмотра. Используйте заполнители для учета задержанных данных. Вот демо и код бесконечного скроллера.
Бесконечные скроллеры появляются по всему Интернету. Список исполнителей Google Music один, временная шкала Facebook и прямая трансляция Twitter тоже одна. Вы прокручиваете страницу вниз, и прежде чем дойдете до конца, новый контент волшебным образом появляется, казалось бы, из ниоткуда. Это удобный опыт для пользователей, и его привлекательность легко увидеть.
Однако техническая задача бесконечного скроллера сложнее, чем кажется. Диапазон проблем, с которыми вы сталкиваетесь, когда хотите поступить «Правильно», огромен. Все начинается с простых вещей, таких как ссылки в нижнем колонтитуле, которые становятся практически недоступными, потому что контент продолжает отталкивать нижний колонтитул. Но проблемы становятся сложнее. Как вы обрабатываете событие изменения размера, когда кто-то переключает свой телефон из портретного режима в альбомный, или как предотвратить болезненную остановку телефона, когда список становится слишком длинным?
Правильная вещь™
Мы подумали, что это достаточная причина, чтобы создать эталонную реализацию, показывающую способ решения всех этих проблем многоразовым способом, сохраняя при этом стандарты производительности.
Для достижения нашей цели мы собираемся использовать три метода: переработку DOM, надгробия и привязку прокрутки.
Наш демонстрационный пример будет представлять собой окно чата в стиле Hangouts, в котором мы сможем пролистывать сообщения. Первое, что нам нужно, — это бесконечный источник сообщений чата. Технически ни один из существующих бесконечных скроллеров не является по-настоящему бесконечным, но с учетом объема данных, которые можно загрузить в эти скроллеры, они вполне могут быть таковыми. Для простоты мы просто жестко запрограммируем набор сообщений чата и будем случайным образом выбирать сообщения, автора и отдельные изображения с небольшой искусственной задержкой, чтобы вести себя немного более похоже на реальную сеть.
переработка ДОМ
Переработка DOM — это недостаточно используемый метод, позволяющий поддерживать низкое количество узлов DOM. Общая идея состоит в том, чтобы использовать уже созданные элементы DOM, находящиеся за кадром, вместо создания новых. Следует признать, что узлы DOM сами по себе дешевы, но они не бесплатны, поскольку каждый из них требует дополнительных затрат на память, макет, стиль и отрисовку. Младшие устройства будут работать заметно медленнее, если не полностью непригодными для использования, если на веб-сайте слишком большой DOM для управления. Также имейте в виду, что каждая пересылка и повторное применение ваших стилей — процесс, который запускается всякий раз, когда класс добавляется или удаляется из узла — становится дороже с увеличением DOM. Повторное использование узлов DOM означает, что мы собираемся значительно снизить общее количество узлов DOM, что ускорит все эти процессы.
Первое препятствие — это сама прокрутка. Поскольку в любой момент времени у нас будет лишь небольшое подмножество всех доступных элементов в DOM, нам нужно найти другой способ заставить полосу прокрутки браузера правильно отражать количество контента, которое теоретически там находится. Мы будем использовать сторожевой элемент размером 1 на 1 пиксель с преобразованием, чтобы заставить элемент, содержащий элементы — взлетно-посадочную полосу — иметь желаемую высоту. Мы переместим каждый элемент взлетно-посадочной полосы на отдельный слой, чтобы убедиться, что слой самой взлетно-посадочной полосы совершенно пуст. Ни цвета фона, ничего. Если слой взлетно-посадочной полосы непустой, он не подлежит оптимизации браузером, и нам придется сохранить на нашей видеокарте текстуру высотой в пару сотен тысяч пикселей. Определенно нежизнеспособно на мобильном устройстве.
Всякий раз, когда мы прокручиваем, мы проверяем, подошло ли окно просмотра достаточно близко к концу взлетно-посадочной полосы. Если это так, мы расширим взлетно-посадочную полосу, переместив элемент-сторож и переместив элементы, покинувшие область просмотра, в нижнюю часть взлетно-посадочной полосы и наполнив их новым содержимым.
То же самое касается и прокрутки в другом направлении. Однако в нашей реализации мы никогда не будем сжимать полосу прокрутки, чтобы положение полосы прокрутки оставалось постоянным.
Надгробия
Как мы упоминали ранее, мы пытаемся заставить наш источник данных вести себя так же, как в реальном мире. С задержкой сети и всем остальным. Это означает, что если наши пользователи используют быструю прокрутку, они могут легко прокрутить последний элемент, для которого у нас есть данные. Если это произойдет, мы поместим элемент-захоронение — заполнитель, который будет заменен элементом с реальным содержимым после поступления данных. Надгробия также перерабатываются и имеют отдельный пул для повторно используемых элементов DOM. Нам это нужно, чтобы мы могли сделать хороший переход от надгробия к элементу, наполненному содержимым, что в противном случае было бы очень неприятно для пользователя и могло бы фактически заставить его потерять представление о том, на чем он сосредоточился.
Интересная проблема здесь заключается в том, что реальные элементы могут иметь большую высоту, чем элемент-надгробие, из-за разного количества текста на каждый элемент или прикрепленного изображения. Чтобы решить эту проблему, мы будем корректировать текущую позицию прокрутки каждый раз, когда поступают данные и над областью просмотра заменяется надгробие, привязывая позицию прокрутки к элементу, а не к значению пикселя. Эта концепция называется привязкой прокрутки.
Привязка прокрутки
Наша привязка прокрутки будет активироваться как при замене надгробий, так и при изменении размера окна (что также происходит при переворачивании устройств!). Нам нужно будет выяснить, какой элемент является самым верхним видимым в области просмотра. Поскольку этот элемент может быть виден только частично, мы также сохраним смещение от верхней части элемента, где начинается область просмотра.
Если размер области просмотра изменен и взлетно-посадочная полоса изменилась, мы можем восстановить ситуацию, которая кажется пользователю визуально идентичной. Победить! За исключением того, что измененный размер окна означает, что каждый элемент потенциально изменил свою высоту, так как же нам узнать, как далеко следует разместить привязанный контент? Мы этого не делаем! Чтобы это выяснить, нам придется расположить каждый элемент над привязанным элементом и сложить все их высоты; это может вызвать значительную паузу после изменения размера, а мы этого не хотим. Вместо этого мы прибегаем к предположению, что каждый элемент выше имеет тот же размер, что и надгробие, и соответствующим образом корректируем положение прокрутки. Когда элементы прокручиваются на полосу, мы корректируем положение прокрутки, эффективно откладывая работу по макету до того момента, когда это действительно необходимо.
Макет
Я пропустил важную деталь: макет. При каждом перезапуске элемента DOM обычно перенаправляется вся взлетно-посадочная полоса, что приводит к тому, что частота кадров значительно ниже нашей цели в 60 кадров в секунду. Чтобы этого избежать, мы берем на себя бремя верстки и используем абсолютно позиционированные элементы с трансформациями. Таким образом, мы можем сделать вид, что все элементы, расположенные дальше по взлетно-посадочной полосе, все еще занимают место, хотя на самом деле там только пустое пространство. Поскольку мы сами создаем макет, мы можем кэшировать позиции, в которых находится каждый элемент, и сразу же загружать правильный элемент из кэша, когда пользователь прокручивает страницу назад.
В идеале элементы должны перекрашиваться только один раз, когда они прикрепляются к DOM, и их не будет беспокоить добавление или удаление других элементов на взлетно-посадочной полосе. Это возможно, но только в современных браузерах.
Передовые настройки
Недавно в Chrome добавлена поддержка CSS Containment — функции, которая позволяет нам, разработчикам, сообщать браузеру, что элемент является границей для макетирования и рисования. Поскольку мы здесь сами занимаемся макетированием, это идеальное приложение для сдерживания. Всякий раз, когда мы добавляем элемент на взлетно-посадочную полосу, мы знаем, что на другие элементы не должна влиять ретрансляция. Таким образом, каждый элемент должен contain: layout
. Мы также не хотим влиять на остальную часть нашего веб-сайта, поэтому сама подиум также должна получить эту директиву стиля.
Еще одна вещь, которую мы рассмотрели, — это использование IntersectionObservers
в качестве механизма определения того, когда пользователь прокрутил достаточно далеко, чтобы мы могли начать переработку элементов и загрузку новых данных. Однако IntersectionObservers настроены на высокую задержку (как при использовании requestIdleCallback
), поэтому с IntersectionObservers мы можем чувствовать себя менее отзывчивыми, чем без них. Даже наша текущая реализация, использующая событие scroll
, страдает от этой проблемы, поскольку события прокрутки отправляются по принципу «максимально возможно». В конечном итоге, Compositor Worklet Houdini станет высокоточным решением этой проблемы.
Это все еще не идеально
Наша текущая реализация переработки DOM не идеальна, поскольку она добавляет все элементы, которые проходят через область просмотра, вместо того, чтобы просто заботиться о тех, которые на самом деле находятся на экране. Это означает, что когда вы прокручиваете очень быстро, вы тратите столько работы на макетирование и рисование в Chrome, что он не успевает за ним. В конечном итоге вы не увидите ничего, кроме фона. Это не конец света, но определенно есть над чем поработать.
Мы надеемся, что вы понимаете, насколько сложными могут стать простые проблемы, если вы хотите совместить отличный пользовательский интерфейс с высокими стандартами производительности. Поскольку прогрессивные веб-приложения становятся основным продуктом на мобильных телефонах, это станет более важным, и веб-разработчикам придется продолжать инвестировать в использование шаблонов, учитывающих ограничения производительности.
Весь код можно найти в нашем репозитории . Мы сделали все возможное, чтобы ее можно было использовать повторно, но не будем публиковать ее как настоящую библиотеку на npm или как отдельный репозиторий. Основное использование – образовательное.