Naararouter
@Naararouter
Junior Fullstack Web Developer

Как увеличить производительность 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+. Тем не менее на досуге хотелось бы попробовать нижеизложенную идею с виртуальным скроллом.
  • Вопрос задан
  • 947 просмотров
Решения вопроса 2
abyrkov
@abyrkov
JavaScripter
Непонятен один момент: зачем нужно поддерживать стандартный скролл?
А так...
В чем проблема?
Дело в том, что DOM-манипуляции заставляют перерисовываться всему окну. Из-за чего он медленно работает - вы требуете к прорисовке браузера свою, дополнительную. При анимации CSS это очень заметно потому, что браузер оптимизирует CSS-анимации, а вы эту оптимизацию убиваете.
Направления для решения
Скорее всего, лучше отказаться от DOM вообще, если проседает на такой веще, как шриф и использовать canvas + requestAnimationFrame
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы