Пока я писал на Solid у меня была возможность оценить количество попыток оптимизировать производительность на графиках (benchmarks).

DOM является бутылочным горлышком фронтенд разработки. Разные решения могут приводить к схожим результатам. Новые библиотеки появляются каждую неделю, вбирая в себя лучшее из предыдущих, чтобы составить идеальную комбинацию. Через некоторое время глаза начинаю разбегаться от такого разнообразия решений.

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

Какой способ работы с DOM самый быстрый:

  • Virtual DOM
  • Tagged Template Literals
  • Fine-Grained Observables

Сравнение


JS Frameworks Benchmark — лучший опенсорс проект для сравнения производительности JavaScript UI фреймворков. Лучше прогонять тесты локально вместо использования официальных результатов. Результаты могут меняться в зависимости от машины. Поскольку я тестирую на слабой машине производительность заметно просядет.

Я возьму самые лучше подходы в отрисовке (rendering) DOM-дерева, чтобы проиллюстрировать избыточность определенных решений. Я сделаю это, используя способность Solid поддерживать разные варианты рендеринга, чтобы наложить издержки по мере изменения переменных и сравнить это со схожими результатами других фреймворков. Давайте на них взглянем.

Разновидности Solid:

  • solid — версия фреймворка с ES2015 прокси сеттером поверх отслеживающих изменения функций, встроенных в клонированые шаблоны DOM нод. Это достигается предварительным компилированием шаблонов JSX (Code)
  • solid-signals — эта версия такая же, как и предыдущая, но вместо проксей используются raw Signals. Это усложняет применение библиотеки, но на выходе мы получаем меньший бандл и лучшую производительность (Code)
  • solid-lit — эта версия не использует JSX прекомпиляцию в рантайме (Code)
  • solid-h — в этой версии используется HyperScript для создания `document.createElement` на лету. В остальном использует ту же реализацию, что и Solid (Code)

Другие библиотеки:

  • domc — микро-библиотека, которая была разработана для максимизации отрисовки DOM нод с помощью собственного DSL (domain specific language) для формирования HTML, сгенерированного на основе index.html (Code)
  • surplus — эта библиотека отслеживает малейшие изменения прекомпилированных JSX шаблонов для создания серии `document.createElement`. Solid использует такую же библиотеку изменений (Code)
  • ivi — возможно вы слышали об Inferno, но эта библиотека вершина виртуального DOM. Она использует HyperScript Helpers-esque с хуками (Code)
  • lit-html — не первая библиотека в своем роде, но c хорошей производительностью. Она вставляет в рантайме Tagged Template Literals в ноды DOM-шаблонизатора (Code)
  • inferno — быстрейший из клонов Реакта и один из самых быстрых Virtual DOM библиотек. Использует специальные JSX директивы для наилучшей производительности (Code)

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

Для сравнения я бы хотел добавить Web Assembly. К сожалению, на момент написания статьи записи WASM представляли собой ванильную реализацию без высокоуровневых абстракций. (Позже во фреймворк добавили wasm-bindgen — прим. переводчика)

Результаты


HyperScript (inferno, ivi, solid-h)


HyperScript отображает разметку в виде композиции функций (как h или React.createElement). Например:

h('div', {id: 'my-element'}, [
  h('span', 'Hello'),
  h('span', 'John')
])

Этим свойством обладают фреймворки с виртуальным DOM. Даже если они используют JSX или другие DSL шаблонизаторы — под капотом они все равно конвертируются в поэлементные рендер методы. Это используется для конструирования виртуального DOM дерева при каждом цикле рендеринга. Но, как показано тут, рендер функции могут быть использованы для создания реактивного графа зависимостей, как в случае Solid.



Как видите, библиотеки с виртуальным DOM гораздо быстрее. Solid теряет в производительности из-за избыточного создания реактивного графа. Заметьте разницу в бенчмарках #1, #2, #7, #8, #9.



Память менее убедительна. Inferno и эта версия Solid показывают примерно одинаковый результат. В то время, как ivi использует больше памяти.
Ключевое отличие таких фреймворков, как Solid, в том, что там нет VDOM. Solid не пробрасывает данные по дереву вверх и вниз при отложенных циклах обновления, чтобы отрисовать представление виртуального DOM, а затем пропатчить реальный DOM. Solid компилирует JSX в оптимизированные реальные DOM инструкции. Затем через обработчик событий он запускает обновление определенных нод только при обновлении зависимостей от данных, которые изменились. Solid реализует fine grained evaluation благодаря синхронным программируемым графам. Он по-другому отслеживает изменения.
Источник
Фреймворки с реактивным отслеживанием обновлений показывают лучшие результаты при обновлении строк. Этот график объяснил бы популярность VDOM в последние годы. Достаточно отметить, что если вы используете HyperScript с таким обновлением, то вам лучше переключиться на Virtual DOM.

Строковые шаблоны (domc, lit-html, solid-lit)


Каждая библиотека здесь имеет нечто схожее. Они рендерятся на основе клонирования шаблонов элементов, исполняются в рантайме и не используют VDOM. Но все же имеют различия. DomC и lit-html используют top-down diffing схожий с Virtual DOM в то время, как Solid использует реактивный граф. Lit-html разделяет шаблоны на части. DomC и Solid компилирует шаблон в отдельные пути в рантайме и обновляет их.



Эта категория имеет широчайший разброс производительности. DomC работает быстрее всего, а lit-html самая медленная. Solid Lit находится в середине. DomC демонстрирует, что простота кода ведет к наилучшей производительности.

DomC проседает только в секции #4 потому что вычисляет разницу нод, которая усложняется по мере роста глубины. Она довольна быстрая, но необходимо провалидировать результаты на больших данных.

Solid Lit более производительная, чем Solid HyperScript. Моментальная компиляция в рантайме убирает недостатки создания реактивного графа, позволяя фреймворку догнать ivi — быстрейшую VDOM библиотеку (смотрите полную таблицу в конце статьи).



DomC показал хорошие результаты в потреблении памяти. Это произошло из-за клонирования элементов шаблона. Примечательно, что кодогенерация в рантайме может иметь минимальные затраты производительности по сравнению с компиляцией на стадии билда. Возможно, это несправедливое сравнение для lit-html поскольку фреймворк не использует эту технику. Но справедливо отметить, что lit-html или подобные библиотеки, как hyperHTML или lighterHTML, являются не наилучшим способом реализации Tagged Template Literals. И можно достичь хороших результатов даже в рантайме без VDOM.

Прекомпилированный JSX (solid, solid-signals, surplus)


Эти библиотеки используют JSX, который компилируется в DOM или реактивный граф на стадии билда. Шаблоны могут быть чем угодно, но JSX предоставляет чистое синтаксическое дерево, которое улучшает опыт разработчика.



Эта группа имеет схожие результаты, но разница здесь очень важна. Все три используют одну и ту же библиотеку для управления состоянием S.js. На примере Solid Signals видно, что отслеживающие функции с клонированием элементов шаблона дают большую производительность. Стандартная реализация Solid перегружена использованием ES2015 Proxies, что ухудшает результат на всех графиках. Surplus использует `document.createElement`, который ухудшает показатели на тестах, где создаются строки #1, #2, #7, #8.



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

Вывод здесь заключается в том, что прокси ухудшают производительность и больше библиотек должны клонировать шаблоны. С другой стороны, можете рассматривать небольшую потерю в производительности из-за прокси, как инвестицию. Пример с Solid имеет самое маленькое количество кода среди других примеров — всего 66 строчек, в ней на 13% меньше не пробельных символов, чем в Svelte — библиотеке, которая гордится своей минималистичностью.

Лучшие в классе (domc, ivi, solid-signals, vanillajs)


Теперь возьмем победителей в каждой категории и сравним их с брутальным, эффективным, написанным вручную примером на ванильном JavaScript. Каждая реализация представляет одно из популярных решений для отслеживания состояний. Вы можете даже провести аналогию между этими библиотеками и большой тройкой: Solid > Vue, DomC > Angular, ivi > React. Этот результат вы получите, если уберете все лишнее, кроме рендера, и избавитесь от 60–200kb кода.



DomC и Solid близки в показателях, ivi значительно отстает, но DomC, в целом, быстрее. Его сложность по сравнению с vanillaJS заметно меньше, но он менее эффективен при частичных обновлениях. Только этот критерий не является показательным. Любой, кто считает, что VDOM медленный или имеет ненужные усложнения, должен проверить это самостоятельно.

Большинство библиотек никогда не будут обладать такой производительностью.



На графике с памятью тоже лидирует DomC. Fine-Grained Solid обгоняет VDOM ivi в плане потребляемой памяти.

Интересно, что эти библиотеки ненамного хуже vanillaJS независимо от метода. Они все очень быстрые.

Bundle Size


Напоследок я бы хотел затронуть размер бандла. Многие реальные тесты уделяют внимание только этим метрикам. Да, размер бандла важен и имеет прямую корреляцию с производительностью, но какая разница? Я подозреваю, что сложность работы кода имеет большую важность, чем размер.



Conclusion


Как обычно результаты в таких графиках никогда не бывают до конца убедительными. Важен сам процесс и выводы, которые мы делаем. В данном случае, мы видим, что DOM сам по себе является большим бутылочным горлышком, когда речь заходит о производительности. Настолько, что не существует однозначной техники для его обхода.


Christoper Lambert as The Highlander

Нет, все не так просто. Ни DOM, ни VDOM не является медленным. Но я считаю, что они стоят друг друга. Признаюсь, что риторика Реакта о производительности VDOM привела меня к этим размышлениям. Невежество мнений вокруг этой темы приводит в ярость.

Утверждение, что VDOM медленный происходит из-за плохой информированности. Рендерить VDOM и высчитывать разницу состояний — это переусложнение по сравнению с тем, чтобы этого не делать. Но масштабируемо ли его отсутствие? И как производить изменения данных?

Я вижу, что в каждом правиле есть исключение. В целом, прекомпиляция соединенная с Fine-Grained в реактивных фреймворках является самым быстрым решением. Но DomC показывает высокую производительность и без этого. Нативные JS методы, например, клонирование элементов шаблона с Tagged Template Literals могут быть наилучшим решением в реализации lit-html от больших корпораций (Google). Но это один из самых медленных фреймворков в данной таблице и даже не наилучшая реализация этих технологий. Svelte считается в комьюнити самой быстрой библиотекой, но она даже близко не смогла конкурировать с представленными решениями.

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

Результаты тестирования всех библиотек в одной таблице: