.NET Fest: полная программа конференции на сайте. Присоединяйся к самому большому .NET ивенту
×Закрыть

Робота з графікою в браузері: реактивний та комплексний рендеринг, задачі та інструменти

«Не стільки того світу, що у вікні»

Мене звати Олексій, останні років десять займаюсь тим, що все більше рухаюся в напрямі розробки графічно навантажених систем, залишаючись при цьому у веб-контексті. «Традиційні фронтенди» також можна частково відносити сюди, мені здається. Особливо коли мова йде не стільки про оптимізацію чи про насиченість зображеннями, а скоріше про дизайн програми, який залежить від графічних можливостей середовища та його обмежень.

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

Стаття розрахована на хвилин 15-20 читання, якщо не занурюватись глибоко в численні посилання. Написана вона за матеріалами виступу на tech talk із серії «Front-end evenings» в Cogniance, звідки доступне відео та слайди. Мотивом для огляду цієї теми слугує мій досвід навколографічного комерційного програмування останніх двох років (інфографіка, карти). А починалося все зі шкільних олімпіадних задач на зразок Мальтійського хреста, що обертається. Можливо, хтось теж знайомився з чудовим світом Turbo Pascal та його режимом екранних буферів саме під керівництвом Олександра Рудика.

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

Сьогодні я працюю в компанії HERE Technologies над проектом Maps API for JavaScript, який поєднує растрову та векторну графіку для малювання карт у браузері. Звісно, спектр застосувань різноманітного рендерингу в сучасному вебі широкий. GIS-проекти є лише його частиною, хоча й зазіхають на провідну роль, коли мова йде про бібліотеки для геометричних обчислень, про що буде згадано нижче.

Короткий огляд: робота з DOM, його можливостями та обмеженнями

Виокремлення поняття «графічне програмування» має дещо штучний характер, адже так званий фронтенд (якщо слідувати усталеній сегрегації) — взагалі весь про відображення стану (state) на екран. Хоч традиційно він і асоціюється з «формами-кнопками», навіть там сьогодні не обійтися без анімацій, переходів між станами та візуальних доповнень. Навіть мінімалістичні інтерфейси часто мають якусь кількість motion-ефектів, підсилюючи інтуїтивність або слугуючи естетичними акцентами. І все ж у вікні браузера ми відштовхуємось від DOM, з його ієрархією, семантикою, специфічним API. Він дає багато можливостей для структурування програми, як візуального, так і архітектурного, а площина відповідальності простягається від смаків та доступності до продуктивності, включно з perceived performance.

Отже, об’єктна модель документу — що із нею (не) так і чому іноді нам її недостатньо. Власне, приклади для останнього — ігри, складні анімовані сцени, діаграми, літаючі літери, рухливі частинки з лініями (дуже популярна штука, якщо мета — отримати вражаючий ефект на головній сторінці сервісу, де обов’язково є абревіатури AI, ML чи AR). DOM теж не одразу здається, пропонуючи гнучкість та виразність SVG, а головне — звичну деревоподібну структуру, якою літають (спливають та вловлюються) івенти. А отже, можна відносно швидко будувати інтерактивні інтерфейси. З іншого боку, майже одразу постає питання продуктивності. Насичуючи документ тисячами елементів, з підписками та стилями, додаючи до цього блокуючі repaint та reflow, скролінг, різноманітні вкладені меню (згадайте Google Sheets) — маємо геть нетривіальні перформанс-проблеми та виклики.

Поняття реактивності

Останніми роками популярними стають ті чи інші форми функціонального програмування. Як на мене, тут має місце своєрідна комбінація нонконформізму (часто — елітизму) математизованої частини спільноти, поєднаного з практичними аспектами — той таки «performance gain», прагнення до декларативності як інструменту спрощення. Коли подібні ідеї існують не лише в науковому середовищі й близькому до нього, але й проникають в індустрію, підхоплюються великими компаніями (з відповідними піар-бюджетами), народжується щось на зразок моди на окремі підходи. Так, сьогодні вже не виглядає дивним дещо ідеалістичне твердження про UI як “f(state) => screen”. Формулювання неточне, але ідея в тому, що незважаючи на побічні ефекти (наприклад, трекінг), дизайн програми можна побудувати як набір декларативних за своєю суттю компонентів, що працюють лише зі зрізом стану всієї програми, відображаючи цей локальний стан на розмітку та стилі.

Одразу виникає питання інтерактивності, організації роботи з даними: як тими, що вводить користувач, так і оновленнями від інших частин системи, у тому числі зі сторони сервера. Дійшло до такого розквіту інструментарію, що тепер говорять про цілі екосистеми навколо окремих бібліотек. Мабуть, найпопулярнішою все ще лишається зв’язка React+Redux, хоча вже достатньо альтернатив фактично на кожному шарі стеку. У самій назві звучить сучасний базворд — «реактивність», привчаючи до концепції моментального, в ідеалі, оновлення вигляду компонент як реакції на оновлення стану.

Формалізуючи поняття реактивності, спільнота зламала не один десяток списів. Компанії продовжують використовувати власні визначення, тоді як мені найбільш подобається таке, хоч і навмисно надто широке: «Реактивне програмування передбачає роботу з асинхронними потоками даних».

Картинка з гісту «The introduction to Reactive Programming you’ve been missing», CC BY-NC 4.0

У якомусь сенсі робота з графікою природним чином може бути організована згідно з реактивним підходом, навіть у випадку чистого WebGL. На стороні CPU (ще у JS-коді) готуються різноманітні дані, які потім завантажуються на GPU, де відбувається їх паралельне опрацювання. Усе задля того, щоб отримати достатню кількість атрибутів для десятків тисяч полігонів і врешті відмалювати їх на екрані. Термін «pipeline» досить влучно описує те, що відбувається на шляху від складних геометрій до сукупності кольорових пікселів.

Звісно, славнозвісна «автоматична передача» («automatic propagation») даних чи змін у цих даних — напівміфічна річ, тому в реальному світі вона ховається за різноманітними реалізаціями конкретних бібліотек чи мов програмування.

Застосування (whereis)

Саме час трохи зупинитись на переліку застосувань того, що я називаю тут «графічним програмуванням». Він далеко не вичерпний, та основні напрями такі:

  • Інтерфейси, сповнені анімаціями, настільки складними та комбінованими, що завеликий DOM стає надто дорогим. Згадується приклад застосунку Flipboard та їхнє рішення react-canvas.
  • Різноманітні візуалізації даних. Почали набирати оберти нові спеціалізації (так, люди пишуть це в резюме): «creative coder», «data/generative artist».
  • Карти. Якщо сучасні навігаційні системи апріорі векторні, то браузерні карти лише в процесі переходу від растрових тайлів (комбінації картинок, згенерованих на сервері) до домінування вектору. Приємно ж, коли можливо одночасно переміщатися по карті та робити zoom або ж нахиляти чи обертати поверхню, бачити будівлі з об’ємом і тінями. Саме GIS є одним з тих напрямів, що розвиває геометричні обчислення й робить відповідні напрацювання доступними. Адже для багатьох з них середовищем виконання вже є клієнт — браузер або мобільний застосунок.
  • Медійні програвачі та редактори. Робота з музикою (уявіть Guitar Pro в браузері), відео, презентаціями (де мало не всі види контенту разом), текстом з ефектами. Усе це вимагає або величезної кількості оптимізацій, або повної відмови від звичних HTML-фронтендів та переходу на WebGL-рендеринг. Лише невеликий приклад з реального проекту — відеоплеєр, де контрол timeline містить попередній перегляд кадру в залежності від позиції курсору. Курсор рухається зі «швидкістю» 60 FPS, а отже й інтерфейс має реагувати відповідно.
  • Ігри. Цілі світи, перенасичені динамікою. Для багатьох — узагалі перший досвід взаємодії з комп’ютером і серйозний мотив для занурення в програмування. Тут можна було б згадати «A Study Path for Game Programmer», застосування фреймворка Phaser, а також інді-маркет-платформу itch.io (мені особливо імпонує механізм деплою командою «butler push»).

Можна було б зупинитися на текстових ефектах, але це окремий світ — з переходами, грою з кольорами, тінями та прозорістю, не кажучи про різноманіття шрифтів. Замість цього можна згадати бібліотеку Blotter, картинка нижче звідти.

Чи є життя поза DOM?

Чим же загрожує відмова від звичного світу HTML/CSS? Основні аспекти API, котрих більше немає, а отже, реалізація стає вашою безпосередньою відповідальністю:

  • Нашарування та каскадування. Дерева елементів, у звичному розумінні, нема. Замість нього щось на зразок «світу» або «сцени», де якось треба розмістити об’єкти, дати їм можливість дізнатися про сусідів, організувати виправдану для задачі розмітку.
  • Підписка на події. Навіть така базова, здавалося б, штука, як «onclick», тепер постає як послідовність викликів: де саме відбувся click — у яких координатах, що знаходиться «під курсором» у той момент, як описати життєвий цикл цієї події?
  • Стилі. Взагалі, якою мовою послуговуватися при описі зовнішнього вигляду елементів, як відділити це поняття від структури чи геометрії? Як групувати елементи задля присвоєння спільних атрибутів, чи варто впроваджувати набори ортогональних або взаємозалежних властивостей?
  • Виокремлення (select, pick). Із цього варто було б починати. Залежно від задач, вам може знадобитися більш-менш виразний та доступний спосіб доступу до компонентів, елементів чи інших атомарних частинок, з яких складається світ.

Альтернативи, застарілі підходи

Заслуговують згадки певні спроби, а точніше — серйозні комерційні продукти, що ставили собі за мету розширити можливості додатків у тогочасних браузерах. Або, іншими словами, відійти від стандартів настільки, що залишалося лише сподіватись, що індустрія підтягнеться. Обох спіткала дещо спільна доля, а у декого з адептів не обійшлося без серйозної драми, коли стало ясно, що хвиля популярності спадає, а на зміну приходить новий етап розвитку самої веб-платформи.

Звісно, мова йде про Flash та Silverlight. Якщо перший, як платформа, мав свою мову (пізніше — діалект ECMAScript), спільноту й дійсно надавав небачені на той час можливості як в плані графіки, так і відчуття інтерактивності, то останній мав дещо іншу історію, так само як і не такий сумнозвісний кінець. Говорячи мовою комерційних ідолів, знадобилися не лише зусилля відкритої спільноти, але й конкретні рухи зі сторони Apple, для того, щоб посунути дітище Adobe зі сцени. Розробка Microsoft, у свою чергу, протрималась на хвилі активного маркетингу років 5-6, остаточно зійшовши нанівець лише в 2015 році.

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

Абстракції та інструменти

Та годі з історією та її відгалуженнями. Спробуємо оцінити той набір абстракцій та інструментів, який сьогодні. Почнімо зі структури (тут важко знаходити відповідники англійським термінам):

  • Render engine, render loop (повна ітерація на кожні 17 мс, в ідеалі). У класичному випадку всередині циклу охоплюються важливі (часто — видимі) елементи, опитується їх актуальний стан (положення, орієнтація, геометрія, колір, прозорість) та відправляється на рендер.
  • Сцена, камера, світло, матеріали. Іноді (наприклад, в картах), можна зустріти поняття «point of view». Як організувати розміщення об’єктів у просторі, що взагалі вважати простором (які обмеження накласти, у якій системі координат описати), чи об’єднувати об’єкти в групи (кластеризація, нашарування), з якого боку освітлювати і, власне, дивитись (проекція, тілесний кут), як враховувати тіні й відблиски — неповний перелік питань.
  • Навігація, інтеракції (як реагувати на дії користувача). Критично важлива сторона UX. Від цього залежить позиціонування користувача, свобода його дій — із чим він працюватиме: виділятиме окремі елементи мишкою, нахилятиме карту двома пальцями чи керуватиме так званою «free floating camera», як у безсмертній грі Descent.
  • Допоміжні інструменти — статистика FPS та пам’яті, візуальний debug. Загалом, вимоги перформансу з одного боку і складнощі дебагу з іншого, роблять роботу над будь-яким нетривіальним додатком сповненою своєрідних викликів.

Розділенню відповідальності між наявними бібліотеками чи фреймворками та власним кодом присвячено наступний розділ. Та варто згадати набір d3-утиліт, а саме d3-scale, d3-scale-chromatic та d3-color. Ясна річ, використовувати їх можна і в контексті DOM, і при роботі з canvas, і для підготовки даних для WebGL. Зручно ж, коли інтерполяція чи вибір кольору з палітри доступні за простим викликом чистої функції.

d3.schemePastel1

d3.interpolatePlasma(t)

Варто окремо зупинитися на d3 та проблемі вибору — брати потужний фреймворк чи йти Unix-шляхом, самостійно організовуючи структуру програми із залученням набору бібліотек для конкретних цілей. Можливо, мені просто щастило завжди і я не підтримував дорослий стабільний проект, що повністю покладається на фреймворк. Тому використання специфічних (у хорошому розумінні) бібліотек було чимось природним, адже ви не зв’язані усталеним підходом до організації коду, маєте уявлення щодо можливостей утиліт, набір певних вимог до них. Отже, залишається «просто» приготувати дані, викликати функцію та відмалювати результат. На минулому проекті якраз працював такий підхід — дані готувались самостійно (набір скриптів, якщо треба регулярно оновлювати — воркерів), окремі d3-утиліти викликалися безпосередньо перед рендерингом, який виконував React, забезпечуючи інтерактивність та звичну структуру компонент. Подібний підхід, поєднання можливостей обох інструментів, Shirley Xueyang Wu продемонструвала у своїй доповіді «D3 and React, Together».

Mike Bostock (автор d3, засновник Observable) — узагалі цікавий персонаж. Код d3 має незвичний стиль, дуже слабо документований. Одразу розібратися в ньому (на відміну від API) досить складно. Але такій продуктивності у поєднанні з архітектурним баченням — для чого який компонент потрібен, як їх зібрати разом — можна лише позаздрити.

Інша справа, коли виникає ситуація, за якої нема ресурсу (часу, бажання, експертизи) на створення структури програми з нуля, а хочеться покластися на популярне перевірене рішення. Цілком справедлива логіка, для якої існує низка інструментів. Наприклад, three.js є взагалі традиційним рішенням, якщо не хочеться писати шейдери, а потрібен набір слабодокументованих, але робочих абстракцій :) Більш сучасні альтернативи — модульні компоненти stackgl та навіть функціональні бібліотеки regl та WebGL for Elm. Перейдіть за останнім посиланням. Там прекрасна схема «Combining Meshes and Shaders», що пояснює такі речі, як меш, фрагмент, уніформи, варіативи та атрибути.

Візуальний debug — улюблений аспект моїх колег, коли справа доходить до шейдерів. Фактично, якщо немає можливості поставити breakpoint чи просто роздрукувати рядок, лишається лише вивести на екран певний набір кольорових пікселів, з яких потім можна витягнути числові дані. Виглядає це прекрасно:

Сторонні модулі, переваги та ризики open-source

Р. Столмен, wiki, CC BY-SA 3.0
Звісно, імплементація усіх перелічених компонентів, навіть для невеликої програми перетворилася б на захопливу історію, сповнену чудових відкриттів. Із сумнівними шансами на завершення задуманої функціональності у притомні терміни.

На допомогу приходить перевірена часом спільнота, не така різнобарвна, як у модних UI-фреймворків, часто з більш консервативними поглядами на документацію та цикл релізів, проте із досвідом роботи над подібними проблемами часів розквіту протистояння OpenGL та Direct3D. Фотопортрет Столмена тут радше заради загального духу свободи і привернення уваги до важливості розповсюдження напрацювань під відкритими ліцензіями. Актуальна проблема, особливо зважаючи на комерційну орієнтованість та очевидну закритість величезного масиву софту, що в тій чи іншій мірі відноситься до візуального чи ігрового.

Є винятки, кожен зі своїм функціональним навантаженням, так само як і з недоліками та ризиками. Тут не буде особливої реклами чи критики, лише хочеться відмітити приклад компанії Mapzen, яка намагалася (зі слів свого СЕО) дещо розворушити картографічну індустрію відкритим підходом, створила низку продуктів, але в січні цього року опублікувала дуже поетичний postmortem в трьох частинах. Вони дотримали слова й не закинули повністю свої репозиторії. Але уявіть, що ваш продукт повністю залежить від, скажімо, стороннього механізму рендерингу, і тут виявляється, що його підтримка сходить практично нанівець, лишаючи вам екзистенційний вибір — застрягнути у застарілій залежності або відчайдушно її форкнути.

Окрема пісня про ліцензування стороннього коду. Хоча ця тема і не є специфічною для візуального світу, але тут до інженерії долучається мистецтво (спрайти, текстури, патентовані ефекти, фільтри), значно розширюючи звичний вибір лізензій.

Неможливо вкотре не згадати тезу (що вже стає неформальним визначенням) про те, чим відрізняється фреймворк від бібліотеки — перший вбудовує ваш код у себе під час циклу програми, тоді як другу викликає ваш власний код. Тобто ви відповідаєте за структуру у випадку використання бібліотеки. Прекрасна доповідь на цю тему — «A Framework Author’s Case Against Frameworks» від Adrian Holovaty — автора Django. Мені подобається ще менш точне визначення: бібліотеку відносно легко виокремити (замінити або скопіювати окрему частину чи повністю), тоді як фреймворк надає ширші можливості, одночасно прив’язуючи до себе. Заміна спрацює лише при відносно сумісному API, як underscore->lodash або React->Preact. Так, перший приклад — це бібліотека, а от другий вже не можна вважати такою, якщо вся програма покладається на нього, а не тільки view layer. Визначення його відповідальності вже теж під питанням, у час, коли MVC на рівні структури стає мало не архаїчною парадигмою і всі говорять про самодостатні компоненти (MV-whatever може й далі собі існувати всередині компонент).

Ну і щоб не перетворювати цей відхід від теми на відверту антитезу високорівневим шаблонам, можна згадати один виняток. Choo позиціонує себе як фреймворк, і це дійсно так, адже він уможливлює конструкцію на зразок “const app = choo(); app.use(...); ...; app.mount(‘body’)”. Але насправді є нічим іншим, як набором утиліт для роутингу та pub-sub шини. Мені особливо подобається цитата з його readme: «We believe frameworks should be disposable, and components recyclable».

API нижчого рівня (WebGL, Shader Language)

З чим же доводиться працювати, обравши найпотужніший з візуальних API — WebGL? Найперше, що трохи відлякує — необхідність або повністю покластися на одну з магічних бібліотек (скажімо, three.js), або неминучість занурення в окрему мову GLSL. Одразу можу рекомендувати ресурс WebGL Fundamentals в поєднанні з порадою створити власний playground-репозиторій — як hardcore підхід. Полегшений (або просто більш зважений) варіант — все ж обрати фреймворк і зосередитися на логіці базових прикладів, звикнути до середовища і переписати все на шейдерах :)

Термінологія, якою майже одразу доведеться оперувати, обертається навколо понять вершин (vertex) та фрагментів (fragment). Відповідно є два типи шейдерів. Перший працює з координатами трикутників як базових елементів складних геометрій (і викликається раз для кожної вершини). Тоді як задача другого — задати колір поточному пікселю (тобто, зазвичай, викликається частіше — на кожен піксель екрану) або взагалі відкинути його. Дані, необхідні шейдерам, діляться на три класи: атрибути (геометрії фігур), уніформи (спільні дані для всіх вершин для поточного проходу) та текстури (тут можуть бути як растри, так і різноманітні кодовані кольором метадані — чудернацька техніка, проте розповсюджена). Спочатку виконується vertex shader, повертаючи позицію вершини, а вже потім — fragment shader, причому сайд-ефектом першого може бути передача додаткових даних у другий — так званих варіативів (varyings). Важливою особливістю варіативів є їхня автоматична інтерполяція для кінцевих пікселів.

Мій улюблений приклад використання текстур для передачі даних, не пов’язаних із геометрією як такою — передача різноманітних параметрів для обчислення та відмалювання пунктирних ліній зі скругленими або кутовими відрізками. На цю тему є прекрасна стаття «Shader-Based Antialiased, Dashed, Stroked Polylines». Красиві демо на тему малювання ліній є в оглядовому туторіалі «Drawing Lines is Hard». Ну і як не згадати про «How I built a wind map with WebGL» від Володимира Агафонкіна. Там теж згадується використання текстур для збереження даних на них — для подальшого зчитування у формі кольорів (бо тільки така форма доступна на зображенні).

Отже, якщо в нас є API для рендерингу, варто розібратися, в якому просторі живуть фігури, координати вершин яких врешті потрапляють на GPU. Традиційно розглядається послідовність із п’яти просторів (local -> world -> view -> clip -> screen), переходи між якими описуються матрицями перетворень (наприклад, з view space до clip space веде матриця проекції). Найкращий опис цих просторів, як на мене, можна побачити в туторіалі «Coordinate Systems» від «Learn OpenGL».

Про саму графічну систему та детально про шейдери можна прочитати у циклах «A trip through the Graphics Pipeline 2011» (11 розділів) та «Introduction to compute shaders» (3 частини).

Об’єктно-орієнтований та функціональний підходи (робота зі станом)

Повертаючись до більш звичних речей, а саме керування станом програми, узагальнюючи — управлінням даними, що крізь програму циркулюють, знову згадаємо про дві найбільш поширені парадигми — об’єктну та функціональну. Сьогодні вже не варто зайвий раз наголошувати на тому, що ці світи не є окремими. Дивитися на самі підходи як на антагоністичні — значить звужувати власну свободу у використанні переваг кожного з них. Краще поєднувати, обираючи для кожної задачі відповідний аспект. Наприклад, якщо мова йде про статичну візуалізацію даних, хай навіть і з певною інтерактивністю (виділення, зміна кольорів, фільтрація) — програму можна організувати у функціональному стилі, як data pipeline на етапі «приготування» даних та як композицію компонентів для відображення даних.

З іншого боку, для складної системи ніхто не заважає використовувати певні об’єктно-орієнтовані підходи, часто саме вони видаються більш природними — наприклад, в іграх із певною кількістю юнітів, що взаємодіють. Правила, звісно, краще описувати декларативно, але спробуйте таким чином повністю задати поведінку персонажа, його реакцію на події, що з ним відбуваються. Щось схоже (добре, досить віддалено) можна спостерігати у світі інтерфейсів, для яких сучасна екосистема надає непоганий вибір з бібліотек для підписки, стрімінгу та обробки подій (Redux з усіма можливими похідними, більш швидкісний MobX, його академічний колега RxJS). Застерігаю, правда, не робити культ з цього шару вашої архітектури, адже часто можна обійтися ізоляцією локальних станів, доповнивши її чимось на зразок простого pub-sub інтерфейсу.

Портрети Б’ярна Страуструпа та Річа Хікі далі просто для привернення уваги.

Попередня обробка даних, шаблон ETL

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

Наприклад, для свого невеликого сайд-проекту (відображення сумарної статистики прослуховувань різних виконавців на карті) я використовую два джерела даних: API Last.fm та MusicBrainz, які потім зливаються у статичний файл. При цьому код кожного зі скриптів (а в майбутньому — воркерів, що мають прокидатися з певною періодичністю), виглядає як «конфігурація + транспорт + кеш + схема» , а все разом поєднується буквально рядком “extract().then(transform).then(load).then(proxyLog).catch(console.error)”.

Підхід очевидний, і в цьому його перевага — у передбачуваності шляхом розділення відповідальності.

Робота зі стилями

Візуальному оформленню документа можна було б присвятити окрему статтю, тільки описуючи різноманіття сучасних підходів. А якщо мова йде про WebGL-програму, то відокремлювати поняття «опису стилю» від «логіки відображення» взагалі доводиться самостійно, вигадуючи своєрідний DSL. Дійсно, у першому випадку, навіть обмежившись світом інтерактивних компонент, маємо тренд CSS-in-JS, зі своїми перевагами. Для мене, основною з них є відсутність необхідності перемикати контекст при розробці і обмежувати себе в гнучкості. Якщо виходити зі згаданого вже концепту про відображення як функцію стану, то стиль — як частина відображення, теж має можливість змінюватися залежно від стану. З’являється можливість використовувати функції всередині стилю — так само, як і всередині розмітки.

Зручно, коли в одному файлі (компоненті) можна описати і структуру, і стиль. Так, синтаксис різний, але це недостатньо вагомий привід відривати їх одне від одного, порушуючи пов’язаність (cohesion, не плутати зі зв’язаністю — coupling). Адже в рамках одного компоненту обидві мови слугують одній меті — описати, як виглядає певний атом функціональності, тобто тримати дві конструкції поруч виявляється природним рішенням.

Спостерігається певна гонитва за популярністю серед інструментів, тому згадаю лише окремі: styled-components, styled-system (набір утиліт, працює разом зі styled-components, наприклад), emotion, styled-tools (допоміжні функції). Приклад зі світу шрифтів — typography.js.

Ще однією вагомою перевагою підходу CSS-in-JS є можливість керувати responsiveness не лише декларативно, як в CSS media queries, але й за допомогою, наприклад, замикань. Уявіть, вам треба сповістити про факт зміни розміру контейнера (або відписатися від десятків неактуальних подій) або відмалювати геть інший компонент за умов зміни розмірів екрану.

Повертаючись до проблеми відсутності DOM і CSS у випадку canvas чи WebGL, завжди постає задача організації власної системи стилів. Тут не може бути єдиного рецепту. Можливо, вам доведеться залучати дизайнерів або взагалі зробити стиль частиною API (якщо ви пишете інтегрований продукт). Я бачив приклади декларативно-імперативних гібридів, коли посеред YAML-файлу трапляється рядок, в якому записано тіло функції-замикання на конфігурацію.

Продуктивність, гонитва за фреймами

Як і в алгоритмічних задачах на обчислювальну складність, у реальному-віртуальному світі є дві основні метрики — час та пам’ять. Про час говорять як про кадри (frames) в секунду, намагаючись тримати цей показник близьким до 60-ти, тобто повністю вкласти ітерацію render loop в 17 мс. Це вікно ділиться на дві зони відповідальності — вашу та системну. Розділення умовне, але варто враховувати, що бувають ситуації, коли навіть лінійна складність ваших обчислень наштовхується на слабку відеокарту та великий екран або перевантажений процесор та купу стороннього коду у спільному потоці. Тоді доводиться балансувати між FPS та якістю (або розміром) картинки, обираючи певні параметри з метою їх контролю та, можливо, навмисного заниження.

Одними з таких параметрів є екранне згладжування (anti-aliasing) та DPR (device pixel ratio). Згладжування може бути реалізованим програмно або підтримуватись GPU. Головне, щоб на рівні коду вашої програми була можливість керувати цією величиною. Проте можливість перемикати його як тумблер сама по собі мало чим корисна: при вимкненому згладжуванні все починає виглядати як в перших двох іграх Doom. DPR, у свою чергу, не обов’язково має бути цілим. Грубо кажучи, це коефіцієнт масштабування зображення, який виливається у ступінь розмиття (update за коментарем Mike Gorchak).

Мій досвід роботи зі three.js на макбуці з вбудованою відеокартою та 5К екраном (дивне поєднання, та все ж) говорить про досить значний вплив DPR при увімкненому згладжуванні. Не думав, що лептоп може видавати такі звуки кулером і так нагріватись «лише» через величезну кількість точок. У тій ситуації профайлинг особливо не допоможе, ви просто дізнаєтесь, що десь всередині бібліотеки або коду, який викликає шейдери, витрачається левова частка дорогоцінних мілісекунд. Рішенням була спроба утримати FPS у заданому діапазоні шляхом керування величиною DPR. Це популярний метод у світі мікроконтролерів, де функції згладжування та плавного росту часто входять у стандартний набір.

Звичний підхід як до зниження вартості обчислень на CPU, так і до передачі менших чисел до GPU — розбиття простору екрану на менші прямокутники (часто — квадрати), відомий як тайлінг. Використовується в картах, іграх, скрізь, де є сенс спочатку виконати завантаження та обчислення в межах кожного з тайлів, а потім пов’язати їх за допомогою певних операцій біля тайлових меж.

Що стосується пам’яті та тривалості певних обчислень, є лише один спосіб розвантажити основний потік — віддати обчислення комусь іншому. Якщо це не сервер чи браузерне розширення, то лишаються Web workers. Важливі аспекти: спілкування обумовлює асинхронний інтерфейс, а сама передача даних відбувається шляхом відправки повідомлень, де задіяне структурне клонування даних, що не допускає функції, об’єкти помилок, DOM-елементи, ігнорує ланцюжок прототипів та деякі інші речі. Важко обійтися без воркерів, адже головний потік і так тримає основне навантаження, з усіма блокуючими операціями (до яких відносяться маніпуляції з DOM).

Зарадити ситуації із великою кількістю обчислень частково можна за допомогою штучних обмежень. Окрім вже згаданих тайлів, часто не потрібно обраховувати точки за межами екрану. Певний обсяг калькуляцій можна зробити заздалегідь (особливо якщо вони стосуються даних, що не так часто оновлюються). До того ж не всі елементи, що лежать в межах екрану, насправді видимі — вони можуть бути перекриті іншими шарами. А серед складних (насичених) видимих геометрій є й ті, які можна значно скоротити (втрачаючи деталізацію, але не змінюючи bounding box) й відмалювати спрощену версію, скорегувавши глибину деталізації в подальшому і за потреби. Підхід чимось схожий на поступове завантаження зображень: спочатку передається лише домінуючий колір, далі — чотири усереднені кольори кожного з квадрантів, потім — ще глибше на кілька рівнів, а в кінці з’являються всі пікселі. Таким чином, у користувача згладжується ефект раптової заміни білого неготового прямокутника на кінцевий рендер.

Говорячи про render loop, у якому відбуваються всі ці речі, варто згадати й спосіб його зупинки — метод «window.cancelAnimationFrame()». Адже буває й так, коли жодних оновлень насправді не виникає, усі елементи не змінюють ні форми, ні кольору, ні положення, а отже нема і сенсу відмальовувати те саме кожні кілька мілісекунд. До того ж «заморожування» екрану економить батарею. Для слідкування за об’єктами, вартими оновлень, часто використовується просте маркування їх як «dirty». Один dirty об’єкт робить весь тайл таким, а отже треба продовжувати render loop, але тільки для відповідних тайлів.

Прикінцеві завваги

Усе розглянути неможливо, особливо в такому, дещо поверхневому огляді. Без уваги лишились такі важливі аспекти, як accessibility та availability, особливо спроби автоматизації таких речей, як «розуміння» структури документа (достойна ініціатива — The Cassius Project). Окремої статті заслоговує формат notebooks — це не стільки про візуальне програмування, як про спосіб зібрати докупи живі приклади, документацію, обчислення, навіть спільне редагування коду та інтеграцію із запуском моделей в «пісочницях» чи на інтеграційних серверах. Вражаючі приклади зібрані на сайті Observable, мій улюблений — хаотичний атрактор. Звісно, не все так солодко в цьому напрямі. За критикою можна звернутися до статті «The First Notebook War» з поправкою на зовсім інший світ — статистики, аналізу даних та формату R Markdown.

Закінчити цей опус хочеться ретро-згадкою про часи Turbo Pascal. Принаймні нам, восьмикласникам у 2000-му році, дісталася версія з кількома екранними буферами. Один з них можна було позначити як наступний для показу чи щось на зразок того — і попередньо відмальовані на ньому пікселі дійсно з’являлися на екрані. У цей час інший фрейм уже готується для показу. Тобто на ньому відмальовується відображення актуального стану — для того, щоб в наступний момент цей буфер замінив собою поточний активний. Якщо ж працювати лише з одним буфером, оновлюючи його в реальному часі, уникнути затримок та характерного блимання було неможливо.

Висновок тут лише один. Варто враховувати всю цю перевантажену машинерію, що задіяна в процесі рендерингу мільйонів пікселів. Так, профайлинг не такий простий, і буває, складається враження, що всі можливі оптимізації на місці, а до омріяних 60 FPS все ще треба пройти певний шлях. Але в цьому і виклик сучасних фронтендів (у широкому сенсі) — зростають вимоги до естетики, швидкості, кількість даних та фрагментація пристроїв. Тоді як на нашому боці адекватність стандартів, увага користувачів та нескінченний потік ще невирішених проблем.

LinkedIn

10 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Натрапив на абсолютно божевільну бібліотеку github.com/enkimute/ganja.js. Це імплементація геометричної алгебри, код читати дійсно важко, але приклади чудові.

Мені сподобалось як Tom MacWright про неї написав:

it’s fiendishly difficult to understand for mortals like me, but i dream of the beautiful, tiny implementations of geometry daily and other visualizations i might be able to produce once i understand it
Згладжування може бути реалізованим програмно або підтримуватись GPU.

Никогда программно — это оверкилл.

Грубо кажучи, це коефіцієнт точності обчислень, який виливається у кількість полігонів.

Нет же, даже рядом не валялось. DPR — это термин вёрстки, к графике он не имеет никакого отношения, тем более к полигонам и их количеству.

. Рішенням була спроба утримати FPS у заданому діапазоні шляхом керування величиною DPR.

На лицо явное непонимание функционирования DPS и MSAA. Например, физический размер экрана 640×480, размер экранной вёрстки 320×240, при вёрстке с DPS = 1 оно будет отрендерено в 320×240 и затем растянуто до 640×480, создавая мыло. При DPS=2 оно будет отрендерено 1:1 в 640×480 и растягиваться не будет.

MSAA работает по другому принципу, часто на уровне GPU, не создавая промежуточных полноценных продуктов рендеринга. 2x MSAA означает, что будет отрендерено в картинку размером 1280×960 и затем сжато до 640×480. 4x MSAA означает 2560×1920 -> 640×480.

В обоих случаях будет антиалиазинг, только в первом с DPR оно будет просто размытием, в случае же с MSAA — уменьшением с интерполяцией по куче соседних точек, что даст очень чёткую, но в то же время сглаженную картинку.

Использовать одновременно DPR и MSAA — это как таблетки от цирроза печени запивать водкой.

Це популярний метод у світі мікроконтролерів, де функції згладжування та плавного росту часто входять у стандартний набір.

Это откуда вообще?

Звичний підхід як до зниження вартості обчислень на CPU, так і до передачі менших чисел до GPU — розбиття простору екрану на менші прямокутники (часто — квадрати), відомий як тайлінг. Використовується в картах, іграх, скрізь, де є сенс спочатку виконати завантаження та обчислення в межах кожного з тайлів, а потім пов’язати їх за допомогою певних операцій біля тайлових меж.

Аппаратный тайлинг используется аппаратно и это не дело приложения разбивать изображение на тайлы, это происходит полу-автоматически на уровне GPU и дисплей контроллера. При искусственном разбиении на тайлы можно ошибиться с размером и получить проседание производительности в разы. Ведь размер и формат тайла даже зависит от ориентации экрана, например, разные форматы с проходом по вертикали или по горизонтали, приложение не знает физический лейаут дисплея, фреймбуффера и формат тайлов, а драйвера знают. Дело приложения использовать как можно больше вертексов с одинаковой программой пиксель шейдера за одну операцию отрисовки, что даст GPU возможность оптимизировать рендеринг в разы лучше и эффективнее.

Дякую за детальні зауваження, та погляньте на ці терміни не з низькорівневої сторони, а з доступних в браузері API. Там де про DPR, на початку абзацу згадується бібліотека three.js. Спробуйте достукатись на рівні WebGLRenderer (чи якоїсь іншої абстрактної обгортки) до всіх цих прекрасних згаданих вами речей. Мультисемплінг там можна або включити, або ні, а DPR є нецілим числом, яке дійсно «милить» краще чи гірше. Про «кількість полігонів» — я дійсно неправий.

Щодо мікроконтролерів та функцій згладжування чи контролю діапазону величини (наприклад, росту до цільового значення), тут не про залежність FPS від DPR, ясна річ. Просто як приклад керування одним параметром задля бажаних значень іншого.

Я міг би бути точнішим в тексті, явно сказавши, що мова не про апаратний тайлінг. На прямокутники розбиваються векторні дані та готові растрові зображення — все це збирається на стороні CPU — така штука теж існує :) На прикладі ігрового простору чи карти — дані завантажуються потайлово з сервера, на CPU виконується логіка формування сцени — розшарування, взаємне розміщення та інше.

Мені особливо подобаються фрази «На лицо явное непонимание», «Это откуда вообще?» та «это не дело приложения». Чия справа брати на себе яку логіку чи оптимізацію — питання саме до конкретної проблеми.

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

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

А это и не было целью :) Кто захочет — тот услышит :)

На лицо явное непонимание функционирования DPS и MSAA. Например, физический размер экрана 640×480, размер экранной вёрстки 320×240, при вёрстке с DPS = 1 оно будет отрендерено в 320×240 и затем растянуто до 640×480, создавая мыло. При DPS=2 оно будет отрендерено 1:1 в 640×480 и растягиваться не будет.

Ну консоли и 4к РС игры так и живут и ничего — никто не умер :)

Ну консоли и 4к РС игры так и живут и ничего — никто не умер :)

Немного не так живут. Вышеописанная схема включает двух проходовый рендеринг, который является серьёзным тормозом для достижения желаемых FPS.

Например, в PS4, есть scaler в дисплей-контроллере, который растягивает или сжимает любое изображение в рилтайме, при этом ещё применят 7/9 tap фильтр at no cost.

Спасибо за статью!

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

С проприетарными платформами точно то же самое, только без последней фразы. Так что приведенный пример открытому коду как раз в плюс — если компания загнулась, а софт хорош, найдутся люди, которые продолжат его развивать. В случае с закрытым кодом если компания загнулась, то уже все, без вариантов.

Ну и если вспоминать Mapzen, то стоило бы упомянуть тогда и Mapbox, как обратный пример — компании, которая тоже сделала абсолютную ставку на open source, и теперь является одной из ведущих в индустрии, продолжая экспоненциальный рост и отвоевывая крупнейших клиентов вроде Facebook и Snapchat у старых игроков.

Справедливо. Я думал над тем, как именно корректно упомянуть Mapbox, учитывая что речь идет о конкурентах :) Мне кажется, радикальный выход в open source — смелое решение, которое для компании может окупиться при наличии многих факторов (достаточный «вес талантов», заряженность на качество, ресурс на поддержку, грамотный маркетинг) и должном умении их вместе соединить. Для сообщества — это однозначный плюс, но вокруг все еще полно людей, которые мыслят категориями «наше/чужое» и боятся потерять контроль над некими сакральными тайнами.

Дякую за статтю. А чи є в когось якісний інструмент для відслідковування скриптів, які переписуюсь ДОМ-модель? Багато часу стикаюсь із тим, що на сайтах навішано 100500+ скриптів та по F12 Хром каже що, DOM 15 разів було перераховано. Але як з’ясувати який саме код це зробив — це проблема.. Може є якесь рішення по знаходженню коду, який переписує ДОМ?

В devtools ви можете зупинити виконання коду на момент модифікації дерева елементу, його атрибуту або видалення елементу. Побачите stack trace, там і видно буде ініціатора відповідної події.

Подписаться на комментарии