- PVSM.RU - https://www.pvsm.ru -
У меня есть мечта, и она утопична: я хочу, чтобы мои веб-приложения работали идеально. JQuery, AngularJs, React, Vue.js — все обещают производительность. Но проблема совсем не во фреймворках и не в JavaScript. Проблема в том, как браузер рендерит страницу. А делает он это очень плохо.
Если бы браузер отлично справлялся с рендерингом, то не появился бы такой инструмент, как React Native. Под капотом React Native всё тот же JavaScript, а View нативное, и разница в производительности между нативным приложением и приложением на React Native не будет заметна для рядового пользователя. Другими словами, проблема не в JavaScript.
Если что-то оптимизировать, то как раз рендеринг. Инструментов, которые нам даёт JavaScript и API браузера, недостаточно. Два года я пытаюсь сделать работу своих продуктов плавной и быстрой, но тщетно. Я почти смирился с тем, что веб останется таким навсегда. В этой статье я собрал всё, что успел узнать об оптимизации рендеринга и применить на проектах, над которыми работал, и рассказываю о своих надеждах на ближайшее будущее. Это будущее, в котором я хочу опираться на устойчивый фундамент стандартов и API браузера, а не CSS-хаки и third-party репозитории для оптимизации производительности.
Я писал приложения с довольно тривиальной функциональностью: новостные ленты с комментариями, категориями и тегами. В них можно смотреть видео, делать поиск по новостям и т.д. Ну, ещё push-нотификации. Ничего сложного. По причине NDA я не могу показать вам эти проекты, зато в блоге нашей компании мы рассказываем о принципах выбора подхода к мобильной разработке [1].
В разработке гибридных приложений хорошо. JavaScript меня полностью устраивал, полностью устраивали меня и элементы интерфейса, которые щедро предоставляли фреймворки Framework7 и Ionic. Даже плагинов, позволяющих пользоваться нативными функциями, хватало. Пиши одно приложение и получай сразу десять — под все платформы, какие только придумали. Мечта да и только. Но сейчас будет «но», жирное и ставящее крест на всём.
Как только приложение становится заметно сложнее, чем “Hello, world!”, начинаются проблемы с производительностью. Приложение работает лучше, чем мобильная версия сайта, но далеко не так хорошо, как аналогичное нативное приложение.
Если кто-то и готов с этим мириться, то для меня это стало вызовом. Мне нужно было написать гибридное приложение так, чтобы его невозможно было отличить от нативного. Тогда я чуть-чуть покопался и пришёл к одному простому выводу: с js всё хорошо, проблема в рендеринге. Я перепробовал всё css-хаки от “transform: translate3d(0,0,0)” (который вскоре перестал работать) и до замены градиентов png с альфа-каналом. Это, конечно же, не решало проблему, а лишь чуть маскировало её. По ссылкам несколько таких хаков:
» Force Hardware Acceleration in WebKit with translate3d [2]
» 60fps scrolling using pointer-events: none [3]
» CSS box-shadow Can Slow Down Scrolling [4]
После я работал над другими проектами, не связанными с мобильными браузерами и устройствами. И тут всё неплохо, нет никаких проблем с производительностью, ведь откуда им взяться на машине с хорошим железом. Но если проблем с производительностью не видно, это ещё не значит, что всё оптимизировано.
На сайтах и в приложениях мы видим бесконечные ленты: Instagram, Facebook, Twitter, Medium — из этих примеров, пожалуй, можно составить свою ленту с подгрузкой. И в этом нет ничего плохого. Скролл позволяет перемещаться в пределах одного поста и перемещаться между постами. Можно скроллить быстро, можно медленно. Добавляешь новые элементы в список сколько душе угодно. Я и сам так делал.
Давайте проведем эксперимент. У вас шумный кулер? Откройте Medium.com и мотайте вниз. Как скоро ваш кулер выйдет на максимальные обороты? Мой результат — примерно 45 секунд. И это не Chrome виноват. И даже не то, что моему ноутбуку много лет. Проблема в том, что никто не занимается оптимизацией того, что мы видим во viewport.
Что же происходит, когда мы мотаем ленту? Оказавшись внизу страницы, мы получаем от сервера ещё немного постов, и они добавляются в конец списка. Список растёт бесконечно. Что делает браузер? Ничего. Посты в начале ленты всё ещё существуют и браузер всё еще рендерит их. И “visibility: hidden” тут никак не поможет, даже если мы будем вешать это свойство на каждый пост, который находится за пределами viewport’а. Кстати, такая бесполезная оптимизация была замечена мною в Ionic. Серьезно. Но потом это исправили. Если кому интересно, вот тема на форуме Ionic [5], которую я создал, чтобы обсудить проблему.
Что же мешает писать хороший, оптимизированный код? Мешает то, что мы не так много знаем об этом процессе. Большая часть знаний пришла к нам методом проб и ошибок, а статьи с заголовком вроде «Как браузер рендерит страницу» рассказывают нам о том, как HTML совмещается с CSS и как страница разбивается на слои. Мне непонятно, что происходит, когда я добавляю в DOM новый элемент или добавляю элементу новый класс. Какие элементы при этом пройдут пересчёт и рендеринг?
Вот мы добавим новый элемент в список. Что дальше?
И так далее до корня DOM. В итоге мы рендерим всю страницу целиком.
Рендеринг в браузере работает иначе. Вот [6] одна из массы статей на эту тему, где автор рассказывает о процессе совмещения DOM-дерева и CSS-дерева и о том, как браузер впоследствии рисует полученную конструкцию. Всё очень здорово, однако не совсем ясно, что может сделать разработчик, чтобы помочь браузеру. Есть неофициальный ресурс CSS Triggers [7], на котором представлена таблица, позволяющая определить, какие CSS-свойства вызывают Layout/Paint/Composite-процессы в браузере, но так как страницы обычно содержат большое количество стилей и элементов, единственный разумный выход — это постараться исключить всё, что может ударить по производительности.
В общих чертах оптимизация состоит из нескольких пунктов:
Всё это помогает немного ускорить рендеринг страницы, но что делать если хочется большего?
Как мне кажется, единственный способ, который действительно работает – добиться того, чтобы на странице были только те элементы, которые действительно нужны пользователю. Реализовать такое поведение проблематично.
Многие знают про Virtual list/Virtual scroll/Grid View/Table View. Названия разные, но суть одна: это компонент для эффективного отображения очень длинных списков на странице. В основном подобные интерфейсные компоненты используют в мобильной разработке.
GitHub полон js-репозиториев а-ля virtual list, virtual grid и т.д. Оптимизация вполне рабочая, это факт. В списке из 10 тысяч элементов можно создать контейнер длинной в 10 000 px, умноженных на высоту одного элемента, далее следить за скроллом и рендерить только видимые пользователю элементы плюс ещё чуть-чуть. Сами элементы позиционируются при помощи “translate: transformY(<индекс элемента> * <высота элемента> px)”. Я недавно изучал Vue.js 2.0 и написал такой компонент за пару часов.
Есть несколько вариантов реализации, и разница между ними лишь в том, как мы позиционируем элементы и разбиваем ли их на группы, но это не столь важно. Проблема в том, что событие scroll при идеальных условиях срабатывает ровно столько раз, сколько пикселей было проскроллено. То есть очень много. Добавьте к этому необходимость производить вычисления при каждом срабатывании события. На мобильных устройствах событие scroll и сама механика scroll работает по-разному. Вывод из этого следует довольно простой: scroll event плохо подходит для таких задач.
Здесь стоит упомянуть ещё об одной проблеме. Все компоненты, которые я видел, требуют, чтобы размер элементов списка был одинаковым или, в лучшем случае, был известен заранее для каждого элемента. Это осложняет реализацию.
Вот теперь на сцену выходит IntersectionObserver, первый луч надежды, упавший на описанное мной во вступлении будущее. Фича новая, поэтому вот информация о поддержке браузерами на сайте caniuse.com [9]. А вот немного материалов по ней:
IntersectionObeserver сообщает, когда интересующий нас элемент появляется во viewport и когда покидает его. Теперь не нужно следить за скроллом и считать высоту элементов, чтобы понять, какие из них нужно рендерить, а какие нет.
Теперь немного практики. Мне захотелось сделать виртуальный скролл с подгрузкой элементов, используя IntersectionObserver. Задача примерно такая:
А вот что я понял, пока писал этот компонент:
нужн
Контент разбивается на части по 12 постов каждая. Когда компонент инициализировался, в DOM находится только одна такая часть. Первая и последняя часть имеют скрытый элемент вверху и внизу соответственно. За видимостью этих элементов мы следим. Когда какой-то из этих элементов попадает на экран, мы добавляем новую часть и удаляем уже ненужную. Таким образом мы имеем в DOM две части по 12 постов одновременно.
Чем это удобнее отслеживания скролла? Если высота постов неизвестна, приходится искать элемент в DOV и узнавать её. Это не очень удобно и не очень производительно.
На выходе мы получаем компонент, который умеет быстро рендерить бесконечный контент неизвестной заранее высоты. Можно использовать что-то подобное не только для ленты новостей, но и для любого контента, состоящего из блоков.
Меньше слов больше дела: вот демо [10]. И я прошу обратить внимание на то, что цель здесь — увидеть работу IntersectionObserver на реальном примере.
Вот тут можно посмотреть на FPS, если скроллить со скоростью беглого просмотра ленты (изображение кликабельно):
И максимально быстро (изображение кликабельно):
Автор: Лайв Тайпинг
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/217283
Ссылки в тексте:
[1] мы рассказываем о принципах выбора подхода к мобильной разработке: http://livetyping.com/ru/blog/cross-platform-vs-native-apps-comparing-and-selecting-approaches
[2] Force Hardware Acceleration in WebKit with translate3d: https://davidwalsh.name/translate3d
[3] 60fps scrolling using pointer-events: none: https://www.thecssninja.com/javascript/pointer-events-60fps
[4] CSS box-shadow Can Slow Down Scrolling: http://nerds.airbnb.com/box-shadows-are-expensive-to-paint/
[5] тема на форуме Ionic: https://forum.ionicframework.com/t/does-collection-repeat-really-help/31826
[6] Вот: https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction
[7] CSS Triggers: https://csstriggers.com/
[8] облегчить CSS: https://css-tricks.com/efficiently-rendering-css/
[9] caniuse.com: http://caniuse.com/#feat=intersectionobserver
[10] демо: https://dmitryskripkin.github.io/vue-virtual-scroll-demo/
[11] Image: https://habrastorage.org/files/7aa/cc0/e69/7aacc0e692f143719e164f363fe3c70d.png
[12] Источник: https://habrahabr.ru/post/316136/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.