Никогда не зависающие страницы

На 80% браузеров, по статистике caniuse.com


Здравствуйте. Я занимаюсь программированием пользовательских интерфейсов 8 лет. На пути у меня были веб-сайты, промышленные приложения, игры, мобильные приложения. Для их создания применялись различные технологии, но одна проблема оставалась и остается неизменной — зависание пользовательского интерфейса.


Это связано с более общей проблемой разработки ПО, которая заключается в банальной невозможности оптимизировать все подряд. Мы думаем, что в этом конкретном месте нужно обработать сто сущностей, а выясняется, что в реальности надо будет обрабатывать сто тысяч сущностей. Счастливые обладатели инженерной интуиции, от этого страдают не так сильно, но к сожалению рынок труда состоит не только из счастливчиков, так что выходя в реальную эксплуатацию, наше приложение начинает тормозить. Команда расчехляет профилировщики, отправляясь на поиск “бутылочных горлышек”, а босс подсчитывает убытки.

Преждевременная оптимизация — корень всех зол.

На разных платформах мы решаем “тормоза” различными способами. В приложениях, написанных на языках, поддерживающих многопоточность, мы запускаем новый поток выполнения, добавляем в программу средства управления конкурентного доступа к памяти, внося дополнительные побочные эффекты, увличивая вероятность появления ошибок. Находясь в рамках асинхронной парадигмы, мы разбиваем вычислительную задачу на несколько подзадач, которые будут выполняться за время, незаметное для восприятия пользователя, в ущерб производительности и поддерживаемости. И в первом и во втором приведенных случаях, придется переписать тесты. Переписывание тестов в отличии от переписывания целевого кода (рефакторинг) не идут на пользу продукту.

Особо остро, на мой взгляд, проблема стоит в программировании современных веб-страниц, где программист вынужден бороться с наследием времен прежнего веба, находясь в среде, где инструменты, такие как jQuery, поощряют бесконтрольное использование ресурсоемких операций.

Около полугода назад я вернулся к веб-программированию, с которого когда-то начинал. За время моего отстутствия изменилось многое. Бразуеры стали работать относительно одинаково. Появились, такие библиотеки, как AngularJS, React, Knockout. Они сделали разработку прозрачнее, позволили создавать более сложные приложения, перенеся логику пользовательского интерфейса на сторону клиента. Javascript-программирование выделилось в отдельную профессию, перестав быть побочной обязанностью разработчика сервера. Вместе с этим, классическая веб-страница, превратилась в javascript-приложение, получив в наследство DOM когда-то созданный для отображения статических страниц и однопоточную виртуальную машину работающую поверх процессора с двумя или даже четырьмя физическими ядрами.

Любому разработчику javascript-приложений известно, что самым главным “тормозом” является DOM. Как уже было отмечено выше, очевидные решения вроде jQuery приводят примерно к такому коду

Счастливые обладатели инженерной интуиции из Google и Facebook понимают, что это работает невероятно медленно, по этому они разрабатывают средства, которые мягко ограждают программиста от возможности писать такой код.

Подходы разные. Разработчики Angular считают, что рядовому программисту вообще не следует работать с DOM. Он должен писать декларативную view на “как-бы” HTML, и неявно управлять ей из “контроллера”. Для продвинутых ребят, существует механизм директив — элементов view, где вы пользуясь диалектом jQuery делаете тоже что обычно.

Разработчики React исходят из предпосылки, что тормозит исключительно DOM и больше ничего, по этому они создали псевдо-дом, псевдо-события и предложили программистам пользоваться ими.

Итак, я решил пойти дальше и считаю, что в вебе тормозит все, а разработчикам вообще не следует доверять (мы все знаем нашу маленькую тайну). DOM управляется десятком простых команд. Создать элемент, добавить элемент, удалить элемент, прочитать свойство, обновить атрибут, а все остальное это — целевая логика. Так почему бы не разделить поток выполнения, вынеся целевую логику в веб-воркер? Целевая логика будет выполняться, не блокируясь тяжелыми DOM-операциями, а тяжелые DOM-операции будут выполняться, не блокируясь вычислениями целевой логики. Выполнение будет происходить параллельно (на разных ядрах, если возможно). Это позволит создавать отзывчивые приложения: прокрутка колесом мыши или пальцем, CSS-эффекты работают независимо от того, что в данный момент выполняется в целевой логике. Таким образом, подход решает проблему: мы можем заблокировать поток выполнения на значительное время, не опасаясь, что пользователь посчитает страницу зависшей.

Дорогие любители “чистого javascript”, я искренне прошу прощения (на самом деле нет), что ввел вас в заблуждение и не указал в начале, что моя библиотека, написана на Scala, и вы не сможете ничего на ней сделать, потому что “Scala это сложно”. Если бы я сказал это сразу, то вы бы не стали читать, не правда ли? Однако я надеюсь, что описанный подход может вдохновить кого-нибудь на создание более популярной версии моего решения без всяких там монад и функторов. Желательно с сохранением обратной совместимости (об этом ниже).


Кошка Мурка просто в восторге от библиотеки

Библиотека, реализующая подход описанный выше, называется “Мурка”. Она написана на языке Scala, и компилируется в javascript с помощью Scala.js. На Мурке можно описывать DOM и работать с данными в стиле функционального реактивного программирования. В частности, на данный момент Мурка поддерживает связывание данных, реактивные коллекции и первоклассные эмиторы. Не могу сказать, что Мурка годится для промышленного применения. Пока, то что написано, является скорее доказательством концепции, нежели настоящей библиотекой. Вот простое приложение на Мурке.

Есть поле ввода и строка рядышком. Что будет введено в поле ввода, то и отобразится в строке (не забываем нажать “Enter”, что бы сработало событие “change”). Посмотрите как это работает. Усложним пример.

Так это выглядит в браузере. Теперь у нас получилось два поля ввода, значения в которых будут суммированы и выведены справа, после знака равно. При этом, если мы введем не число в одно из полей, то вместо суммы будет надпись “Enter a number”.

Давайте разберемся, что происходит “под капотом” у усложненного примера. Так как воркеры не умеют работать с DOM на прямую, приложение разделено на две части. Первая часть называется Render Backend (буду называть просто рендер). Это простая программа, написаная на javascript и умеющая принимать команды для работы с DOM, отдавать значения свойств и события DOM. Вторая часть это само приложение, которое полностью отвязано от “физического” DOM, имеет его псевдо-копию (я буду называть ее “псевдо-дом”) и систему синтетических событий “псевдо-событий”. Именно вторую часть мы программируем.

Диаграма взаимодействия компонентов Мурки
  1. Первым стартует рендер. Скрипт очень маленький, поэтому страница инициализируется мгновенно и пользователь сразу может взаимодействовать со статикой.
  2. Далее стартует приложение в воркере. Псевдо-дом генерирует команды для создания “физических” DOM-элементов и формирования DOM-дерева. Рендер принимает и выполняет их.
  3. С этого момента пользователь может взаимодействовать с приложением. Когда пользователь начинает вводить значение, рендер отлавливает событие “change” и отправляет копию данных события в приложение.
  4. На их основе создается псевдо-событие, которое работает так же, как обычное событие, с тем отличием, что оно выполняется для псевдо-дома. Оператор “=:=” ждет “change” для своего элемента, и в тот момент когда событие происходит, отсылает команду рендеру на извлечение свойства “value”.
  5. Рендер исполняет ее и отправляет полученное значение назад в приложение.
  6. Приложение изменяет соответствующее реактивное значение. Далее с помощью реактивного связывания генерируются команды на изменение DOM и отправляются на рендер.
  7. Рендер исполняет их, а пользователь видит результат.

Схема может показаться громоздкой, однако на практике Мурка работает очень быстро, за счет выполнения тяжелых DOM-операций в рендере, а само приложение не делает никаких лишних движений за счет FRP-парадигмы. Подобная программа на React будет формировать новый псево-дом на каждый “change”, потом сравнивать его с предыдущим и генерировать список команд на изменение “физического” DOM. AngularJS будет сравнивать значения из scope, ведь он не знает какие именно изменения произошли.

“Примеры не впечатляют!” — Скажете вы. Посмотрите как работает TodoMVC — классический пример относительно сложного приложения, написанный с помощью Мурки. Исходный код примера можно посмотреть здесь.

В следствии того, что приложение работает без доступа к DOM, существует два ограничения, известных на данный момент. Первое ограничение несущественное — невозможно использовать условного preventDefault() у псевдо-событий, так как preventDefault() реального события необходимо выполнить в том же колл-стэке, что и dispatchEvent(). Второе ограничение существенное — нельзя работать с внешними библиотеками, которые обращаются к DOM. Например, сюда относятся все библиотеки для работы с географическими картами.

Для того, чтобы обойти второе ограничение, в Мурке предусмотрена (но пока не реализована) система плагинов, которые выполняются на стороне рендера. Вы можете написать небольшую обертку для библиотеки на чистом javascript, которая будет предоставлять нужную функциональность через рендер, и ее программный интерфейс на Scala.

При разработке я вдохновлялся проектами Li Haoyi. Спасибо за Scalatags и ScalaRx!

Это все. Исходный код библиотеки расположен на гитхабе. Надеюсь вам понравилась Мурка, и если это так, то возможно у вас будет время поработать над ней вместе со мной, потому что работы предстоит много.


Небольшой постскриптум для любителей “чистого javascript”. Если подход “псевдо-дом внутри воркера” вдохновил вас и вы решили написать что-то свое, то давайте сделаем бибилотеки совместимыми, оставив общий рендер. Тогда плагины для Мурки будут подходить к вашей библиотеке, а плагины для вашей библиотеки будут подходить к Мурке. Если у вас есть вопросы или предложения по Render Backend, то свяжитесь со мной по адресу aleksey.fomkin@gmail.com.