Про оптимізацію рендеринга — з оптимізмом

У мене є мрія, і вона утопічна: я хочу, щоб мої веб-додатки працювали ідеально. JQuery, AngularJs, React, Vue.js — всі обіцяють продуктивність. Але проблема зовсім не у фреймворках і не в JavaScript. Проблема в тому, як браузер рендерить сторінку. А робить він це дуже погано.

Якби браузер відмінно справлявся з рендерингом, то не з'явився б такий інструмент, як React Native. Під капотом React Native все той же JavaScript, а View нативне, і різниця в продуктивності між нативною додатком додатком на React Native не буде помітна для рядового користувача. Іншими словами, проблема не в JavaScript.

Якщо щось оптимізувати, то як раз рендеринг. Інструментів, які нам дає JavaScript API браузера, недостатньо. Два роки я намагаюся зробити роботу своїх продуктів плавної й швидкої, але марно. Я майже змирився з тим, що веб залишиться таким назавжди. У цій статті я зібрав усе, що встиг дізнатися про оптимізацію рендеринга і застосувати на проектах, над якими працював, і розповідаю про своїх надіях на найближче майбутнє. Це майбутнє, в якому я хочу спиратися на стійкий фундамент стандартів і API браузера, а не CSS-хакі і third-party репозиторії для оптимізації продуктивності.



Гібридні програми і продуктивність



Я писав програми з досить тривіальною функціональністю: новинні стрічки з коментарями, категоріями і тегами. У них можна дивитися відео, робити пошук по новинах і т. д. Ну, ще push-нотифікації. Нічого складного. Через NDA я не можу показати вам ці проекти, зате в блозі нашої компанії ми розповідаємо про засади вибору підходу до мобільного розробці.

У розробці гібридних додатків добре. JavaScript мене повністю влаштовував, повністю влаштовували мене і елементи інтерфейсу, які щедро надавали фреймворки Framework7 і Ionic. Навіть плагінів, що дозволяють користуватися нативними функціями, вистачало. Пиши один додаток і отримуй відразу десять — під всі платформи, які тільки придумали. Мрія, та й тільки. Але зараз буде «але», жирне і ставить хрест на всьому.

Як тільки додаток стає помітно складніше, ніж «Hello, world!», починаються проблеми з продуктивністю. Додаток працює краще, ніж мобільна версія сайту, але далеко не так добре, як аналогічне нативне додаток.

Якщо хтось і готовий з цим миритися, то для мене це стало викликом. Мені потрібно було написати гібридне додаток так, щоб його неможливо було відрізнити від нативного. Тоді я трохи покопався і прийшов до простого висновку: з js все добре, проблема в рендерінгу. Я перепробував всі css-хакі від «transform: translate3d(0,0,0)» (який незабаром перестав працювати) і до заміни градієнтів png з альфа-каналом. Це, звичайно ж, не вирішувало проблему, а лише трохи маскувало її. Посилання кілька таких хаків:

» Force Hardware Acceleration in WebKit with translate3d
» 60fps scrolling using pointer-events: none
» CSS box-shadow Can Slow Down Scrolling

Після я працював над іншими проектами, не пов'язаними з мобільними браузерами і пристроями. І тут все непогано, немає ніяких проблем з продуктивністю, адже звідки їм узятися на машині з хорошим залізом. Але якщо проблем з продуктивністю не видно, це ще не означає, що всі оптимізовано.

Medium, у вас проблеми
На сайтах і в додатках ми бачимо нескінченні стрічки: Instagram, Facebook, Twitter, Medium — з цих прикладів, мабуть, можна скласти свою стрічку з подгрузкой. І в цьому немає нічого поганого. Скролл дозволяє переміщатися в межах одного поста і переміщатися між постами. Можна скролл швидко, можна повільно. Додаєш нові елементи в список скільки душі завгодно. Я і сам так робив.



Давайте проведемо експеримент. У вас гучний кулер? Відкрийте Medium.com і мотайте вниз. Як скоро ваш кулер вийде на максимальні оберти? Мій результат — приблизно 45 секунд. І це не Chrome винен. І навіть не те, що моєму ноутбуку багато років. Проблема в тому, що ніхто не займається оптимізацією того, що ми бачимо під viewport.

Що ж відбувається, коли ми мотаємо стрічку? Опинившись внизу сторінки, ми отримуємо від сервера ще трохи постів, і вони додаються в кінець списку. Список зростає нескінченно. Що робить браузер? Нічого. Пости на початку стрічки все ще існують і браузер все ще рендерить їх. І «visibility: hidden» тут ніяк не допоможе, навіть якщо ми будемо вішати цю властивість на кожен пост, який знаходиться за межами viewport'а. До речі, така даремна оптимізація була помічена мною в Ionic. Серйозно. Але потім це виправили. Якщо кому цікаво, ось тема на форумі Ionic, яку я створив, щоб обговорити проблему.

Загадковий світ оптимізації



Що ж заважає писати хороший, оптимізований код? Заважає те, що ми не так багато знаємо про цьому процесі. Більша частина знань прийшла до нас методом проб і помилок, а статті з заголовком на кшталт «Як браузер рендерить сторінку» розповідають нам про те, як HTML поєднується з CSS і як сторінка розбивається на шари. Мені незрозуміло, що відбувається, коли я додаю в DOM новий елемент або додаю елементу новий клас. Які елементи при цьому пройдуть перерахунок і рендеринг?

Ось ми додамо новий елемент в список. Що далі?

  1. Новий елемент потрібно отрендерить і поставити на місце;
  2. потрібно заново зрушити інші елементи списку;
  3. потрібно заново рендери інші елементи списку;
  4. потрібно оновити висоту «батька»;
  5. оновився «батько», і тепер незрозуміло, чи змінилися сусідні елементи.
І так далі до кореня DOM. У підсумку ми рендерим всю сторінку цілком.

Рендеринг у браузері працює інакше. Вот одна з маси статей на цю тему, де автор розповідає про процес суміщення DOM-дерева і CSS-дерева і про те, як браузер згодом малює отриману конструкцію. Все дуже здорово, проте не зовсім ясно, що може зробити розробник, щоб допомогти браузеру. Є неофіційний ресурс CSS Triggers, на якому представлена таблиця, що дозволяє визначити, які CSS-властивості викликають Layout/Paint/Composite-процеси в браузері, але так як сторінки зазвичай містять велику кількість стилів і елементів, єдиний розумний вихід — це постаратися виключити все, що може вдарити по продуктивності.

В загальних рисах оптимізація складається з декількох пунктів:

  • полегшити CSS, зробити стилі зручними для читання для браузера;
  • позбавиться від важких для візуалізації стилів (тіней і прозорості краще уникати зовсім);
  • зменшити число DOM елементів і виробляти якомога менше змін у ньому;
  • Правильно працювати з GPU.
Все це допомагає прискорити рендеринг сторінок, але що робити якщо хочеться більшого?

Відсікання зайвого



Як мені здається, єдиний спосіб, який дійсно працює – домогтися того, щоб на сторінці були тільки ті елементи, які дійсно потрібні користувачеві. Реалізувати таку поведінку проблематично.

Багато знають про 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



Ось тепер на сцену виходить IntersectionObserver, перший промінь надії, що впав на описане мною у вступі майбутнє. Нова фіча, тому ось інформація про підтримку браузерами на сайті caniuse.com. А ось трохи матеріалів по ній:

  • чернетка специфікації
  • репозиторій з чероновиком специфікації, поясненням з прикладами і полифиллом
  • стаття з наочними прикладами в блозі Google
IntersectionObeserver повідомляє, коли нас цікавить елемент з'являється у viewport і коли покидає його. Тепер не потрібно стежити за скроллом і вважати висоту елементів, щоб зрозуміти, які з них потрібно рендери, а які ні.

Тепер трохи практики. Мені захотілося зробити віртуальний скролл з подгрузкой елементів, використовуючи IntersectionObserver. Завдання приблизно така:

  • нескінченна стрічка з постами, в яких є заголовок, картинка і текст;
  • зміст постів і їх висота не відомі заздалегідь;
  • ніяких зупинок на підвантаження контенту;
  • 60 fps.
А ось що я зрозумів, поки писав цей компонент:

  • потрібно переиспользовать елементи, виробляючи мінімум операцій з DOM;
  • не потрібно створювати IntersectionObserver для кожного елемента списку, достатньо двох.
потрібно

Принцип роботи
Контент розбивається на частини по 12 постів кожна. Коли компонент инициализировался, в DOM знаходиться тільки одна така частина. Перша і остання частина мають прихований елемент вгорі і внизу відповідно. За видимістю цих елементів ми стежимо. Коли якийсь із цих елементів потрапляє на екран, ми додаємо нову частину і видаляємо вже непотрібну. Таким чином ми маємо в DOM дві частини по 12 постів одночасно.

Чим це зручніше відстеження скролла? Якщо висота постів невідома, доводиться шукати елемент в DOV та пізнавати її. Це не дуже зручно і не дуже продуктивно.

На виході ми отримуємо компонент, який вміє швидко рендери нескінченний контент невідомої заздалегідь висоти. Можна використовувати щось подібне не лише для стрічки новин, але і для будь-якого контенту, що складається з блоків.

Менше слів більше діла: ось демо. І я прошу звернути увагу на те, що мета тут — побачити роботу IntersectionObserver на реальному прикладі.

Ось тут можна подивитися на FPS, якщо скролл зі швидкістю побіжного перегляду стрічки (зображення кликабельно):

image

І максимально швидко (зображення кликабельно):



FPS дуже рідко падає нижче 60, але всього на пару кадрів і не нижче 45. Хороший результат, враховуючи те, що браузер не знає розмір картинок і тексту заздалегідь.

Висновок
Це не самий вражаючий і корисний приклад використання IntersectionObserver. Куди цікавіше спробувати використати його в зв'язці з компонентами React/Vue/Polimer. Тоді можна при ініціалізації компонента вішати на нього IntersectionObserver і продовжувати ініціалізацію тільки коли він з'явиться у viewport. Це відкриває широкі можливості. Залишається лише схрестити пальці і вірити, що IntersectionObserver отримає свій подальший розвиток.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.