Как увеличить производительность JS манипуляций с DOM (анимации)?
Итак, попытаюсь описать задачу: существует маленькая область на странице (шириной ~80px и высотой ~264px) эта область скрывает внутри себя (под невидимым скроллом) гигантскую область из дочерних блоков, порядка 50-60 тысяч пикселей в высоту. Область помещает внутри себя 9 (можно изменить, но сейчас примем как константу из условий задачи) видимых блоков высотой примерно 29px каждый. Блоки содержат внутри себя упорядоченные числа с интервалом в единицу (можно изменять, в частности, с дробными значениями) от 1 до 1500. Т.е. получается 1500 блоков.
Цель: реализовать замкнутую последовательность чисел, чтобы при прокрутке (т.к. системный скролл не видим (скрыт), но действует и реагирует на все, включая touch, события), после последнего значения (1500), вновь начиналось 1,2,3, и т.д. Необходимо так же добавить изменение CSS-параметров видимых блоков. В центре видимой области - самый насыщенный по цвету (белый, например rgba(255,255,255,1)) и размеру шрифта блок (например, font-size: 1.2em), чем ближе к краям, тем насыщенность цвета падает до прозрачности 0.5, а размер шрифта уменьшается до 0.8em. Создавая тем самым некую иллюзию вращения области (как барабан) при прокрутке.
Результат собственного решения:
Используется react и чистый js, без jQuery. Всю логику удалось реализовать и, в целом, всё работает. Однако, есть проблема связанная с анимированием CSS-свойств. Чем больше становится объектов для обработки, тем соответственно, сильнее падает fps при анимации. Особо остро проблема как раз-таки встала при обработке 1500 необходимых блоков. Что бы выполнять плавный переход между пограничными значениями в 1500 и 1, было решено сдублировать область, получив тем самым 3000 вложенных DOM-элементов. Fps в данном случае падает до 10-14 кадров в секунду, при этом вызывая тормоза и выполняемой другой CSS-анимации на странице (обычные transition свойства background, например).
Описание алгоритма:
1) При формировании компонента происходит дублирование значений, минимум один раз (для плавности в пограничной области), либо больше, до тех пор, пока резкий touch-жест не сможет полностью прокрутить область, вызвав ступор компонента в последнем значении (когда дойдёт до максимальной позиции скролла), опытным путём выявлено, что значение примерно в 5000-6000px в одном направлении.
2) При событии onScroll, я определяю какие компоненты сейчас видимы в заданной области, сравнивая scrollTop обёртки и offsetTop дочерних компонентов.
3) Затем у видимых компонентов (беру примерно в 2 раза больше видимых компонентов, не 9, а 18, что бы опять же при резкой прокрутки не было заметно, что "где-то впереди/сзади" еще не применилась анимация") обрабатываю style.color и style.fontSize, по формулам, которые пропорционально обсчитывают размер и насыщенность шрифта в зависимости от нахождения компонента в видимой области.
Общие замечания:
1) React-state обновляю, только после того, как анимация закончилась (т.е. тормозов из-за частой смены стейта нет), отложенным на 100мс таймером.
2) Тесты на производительность выполненные как вручную с помощью замеров времени выполнения кода с помощью Performance.now(), как и встроенные в Chrome Development Tools рекодеры, показали что, основная проблема, соответственно в DOM-манипуляциях при изменении размера шрифта.
3) Да, выбор видимых элементов - тоже затратная операция, т.к. прохожу по всему массиву видимых...но, как показала практика, при отключении манипуляций со стилями DOM-узлов - всё работает достаточно плавно, на уровне 30+ fps. Но, судя по всему, пока это не столь приоритетное направление для оптимизации.
4) Пробовал манипулировать не со свойствами на прямую, а с CSS-классами: программно заспавнил порядка 100 CSS-правил, каждое из которых отвечало за 1 из 100 возможных положений (~примерно каждые 3 пиксела видимой области) дочернего узла внутри обёртки, соответственно, для плавности. Но увы, результат был тем же самым.
Идея для дальнейшего импрува:
Основная идея для дальнейшей оптимизации состоит в том, чтобы попытаться реализовать что-то вроде "ленивой загрузки" для дочерних компонентов при скроллинге. Изначально не стал с этим заморачиваться, т.к. это дело не быстрое, да и в целом, ранее, на меньшей выборке всё работало как надо.
Чего я ожидаю от ответов к данному вопросу?
1) Какие-либо теоритические консультации, возможно, в самом описанном алгоритме имеются какие-то явные изъяны, которые я, в силу отсутствия опыта оптимизации подобных моментов, мог упустить.
2) Вдруг, кто-то сможет дать ссылку на решения, удовлетворяющие данным требованиям? Где можно посмотреть более адекватный алгоритм и т.д. (я в своё время найти аналоги, тем более для React'a не смог)
3) Любые замечания и предложения, которые помогли бы повысить производительность анимации подобного рода...
Спасибо тем, кто всё же осилил данный вопрос. Надеюсь на конструктивную помощь...
P.s.: поддержка старых браузеров не обязательна
[Update 15.02]: Было найдено бюджетное (в том, плане, что координально переписывать пока ничего не пришлось) костыльное решение, в виде создание обёрток из requestAnimateFrame для операций, где непосредственно происходило изменение style-свойств DOM-элемента. В целом, проблема решилась, удалось поднять fps до 30+. Тем не менее на досуге хотелось бы попробовать нижеизложенную идею с виртуальным скроллом.
Stalker_RED: Хм...думаю, что в конечном счёте, если оптимизировать и адекватно работать ЭТО не будет, то придётся делать так, чтобы адекватно работало, несмотря на смену стека технологий...
Конкретно отвечая на вопрос: нет, не думал, если честно изначально даже в голову не пришло. Видимо потому, что опыт работы с Canvas'ом крайне небольшой (парсил рукописные черно-белые символы для подачи на вход нейронной сети).
Если не сложно, может, посоветуете с чего можно быстро начать, что бы реализовать подобную задачу? Может какие фреймворки и т.д., которые могли бы облегчить рутинные операции и т.д.? Возможно, есть какие-то ссылки, где можно почитать как это работает "под капотом" и за счёт чего я получу необходимый прирост производительности?
Поюзал компонент по ссылке. Если я правильно понял, то данные таблицы/списки, а именно List, на который перенаправило по-умолчанию, выступает в качестве визуализации некоего конечного множества сущностей. Т.е. они имеют начало и конец прокрутки. У меня же должна быть бесконечная прокрутка, как в игровых автоматах (где дёргают за рычаг и барабаны на экране начинают прокручиваться...как раз и в моей задаче необходимо 3 подобных колеса), с соответствующей плавностью и т.д.
Naararouter: советую посмотреть реализацию виртуального скролла. То есть, у вас хоть миллиард компонентов будет, но реально будет в DOM дереве всего 9 (или сколько их там у Вас)
Mikhail Osher: хм...что-то подобное я и имел ввиду, говоря о "ленивой загрузке". Но изначально не совсем понял суть приведённых Вами компонентов. :) Теперь дошло, спасибо, думаю, подобный вариант для решения в текущих условиях подойдет больше всех.
Непонятен один момент: зачем нужно поддерживать стандартный скролл?
А так... В чем проблема?
Дело в том, что DOM-манипуляции заставляют перерисовываться всему окну. Из-за чего он медленно работает - вы требуете к прорисовке браузера свою, дополнительную. При анимации CSS это очень заметно потому, что браузер оптимизирует CSS-анимации, а вы эту оптимизацию убиваете. Направления для решения
Скорее всего, лучше отказаться от DOM вообще, если проседает на такой веще, как шриф и использовать canvas + requestAnimationFrame
В целом, текущая разработка - попытка переосмысления компонента, который мне достался от другого разработчика. Там было множество строчек кода, которые "велосипедно" пытались эмулировать движения прокрутки и т.д., включая touch-события и т.д., но в итоге компонент был не рабочим, разбираться в том, что было наворочено - тоже не малая часть времени, при том, что было сделано ~20% от общего объема работ и предъявленных к компоненту требований. Собственно на тот момент и сейчас, мой компонент выдался, я считаю, более удачным, т.к. большая часть забот по обработке событий скролла досталась браузеру с его стандартным скроллом и, вроде бы, это себя оправдало.
По поводу Canvas, в комментариях к самому вопросу, уже высказали подобное мнение. Вероятно, если альтернатив не поступит, придётся переписывать на Canvas.