Эта статья продолжает серию статей Принесите свой собственный фреймворк, в которой исследуется выбор средства визуализации. Здесь вы можете найти предыдущие статьи: Часть 1, Часть 2, Часть 3.

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

Каждый день появляется новый фреймворк, особенно с появлением веб-компонентов. Они во многом похожи, и все, что нужно, - это одно или два мнения, чтобы разделить другое. Я приложила все усилия, чтобы, так сказать, не выплеснуть Младенца вместе с водой из ванны, но что не менее важно, если я не могу подтвердить свои слова реальными числами, я просто бросаю в беспорядок. Быть быстрым было недостаточно. Я должен был стремиться к тому, чтобы быть, пожалуй, самым быстрым, не нарушая границ, которые я для себя установил. Но, честно говоря, это было не так сложно, поскольку в этом пространстве был явный пробел (Modern Fine Grained KVO), и мне просто нужно было применить все, что я узнал за эти годы. Что ж, давайте приступим к делу ...

Методы рендеринга

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

1. Мелкозернистый (KVO)

Насколько я знаю, это самый старый в списке. Идея заключалась в том, чтобы «привязать данные» определенные изменения dom к конкретным изменениям элемента DOM. Классически это предполагало использование специальных атрибутов DOM в HTML и построение этого графа зависимостей. Оттуда различались разные реализации. Такие библиотеки, как Angular, придерживались подхода «сверху вниз», когда наблюдатели выполняли цикл дайджеста. Knockout справился с этим с помощью явных событий, которые при каждом выполнении сбрасывают все подписки и перестраивают их.

Плюсы:

  • Потрясающая производительность при частичном обновлении / анимации
  • Произвольные границы обновления данных. (Насколько детально подходит для применения обнаружения изменений)

Минусы:

  • Накладные расходы на первоначальный рендеринг и разборку
  • На производительность сильно влияет пакетная обработка / синхронизация механизма обнаружения изменений.
  • Более крутая кривая начального обучения.
  • Упрощенное согласование делает непригодным для моментальных снимков больших данных

2. Виртуальный DOM

Популярный React, этот подход включает в себя реконструкцию дерева, которое представляет структуру DOM, не затрагивая фактическую DOM при каждом выполнении. Затем эту «виртуальную» DOM можно различать, и только изменения применяются к фактической DOM. Существуют разные методы выполнения этого сравнения и исправления, но эти библиотеки всегда работают сверху вниз (цикл обновления включает переоценку потомков) и включает повторный запуск методов «рендеринга».

Плюсы:

  • Легко объяснить, поскольку вся сложность абстрагируется от рендеринга.
  • Худшие случаи никогда не бывают такими плохими, поскольку обновления всегда различаются, и только потом применяются изменения.

Минусы:

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

3. Скомпилированный DOM

Это более новая техника, которая немного позаимствована из обеих других техник. Код, описывающий представление, скомпилирован в 2 метода, первый из которых запускается изначально, а второй - при обновлении. Этот подход также выполняется сверху вниз, но метод «рендеринга» запускается только один раз, так как после этого запускается скомпилированный метод обновления.

Плюсы:

  • Несомненно, самый быстрый способ примирения.

Минусы:

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

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

Разработка средства визуализации

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

В библиотеках Fine Grained классически использовались строковые шаблоны и вложенные контексты, поэтому я начал с этого, в основном моделируя их после KnockoutJS. Вычисление в библиотеках Fine Grained - это метод, который перезапускается каждый раз при обновлении одной из его зависимостей. Я вставлял строковые шаблоны, обходя дерево DOM, чтобы извлечь выражения привязки, а затем связывать их с этими узлами DOM, строящими вычисления, а затем при удалении узлов я отслеживал всех их потомков, чтобы избавиться от этих вычислений. Связываемые данные начинаются с корневого объекта Component, но по мере того, как вы попадаете в цикл в шаблоне, вы клонируете текущий контекст и смешиваете повторяющийся элемент и переменные индекса. Хотя я создавал что-то быстрее, чем Knockout, этот метод был невероятно медленным.

Следующее, что я попробовал, - это разобрать строковый шаблон напрямую, не вставляя его в DOM. Это сгенерировало большую строку инструкций Javascript, преобразованных в код с помощью конструктора Function. Он вызвал методы времени выполнения, которые создавали элементы вручную с помощью document.createElement, и подключили все привязки. В этом подходе все еще есть объекты контекста, которые будут клонированы на каждом уровне. Каждый контекст будет владеть массивом одноразового использования, к которому будут добавляться дочерние элементы (независимо от элементов DOM), чтобы гарантировать, что там, где будут выпущены, дочерние вычисления также будут. Я был очень доволен этой версией. Он был намного быстрее и вытеснил React 15 (текущая версия на тот момент). Но, по общему признанию, в то время я все еще понятия не имел, что делаю.

Некоторое время спустя я наткнулся на Surplus Адама Хейла, использующий JSX. Идея была довольно простой. Используйте предварительную компиляцию, чтобы обернуть все выражения JSX в функции, которые будут использоваться для точных вычислений. Сразу стало ясно, что Surplus был оптимизирован специально для S.js, это библиотека Fine Grained и несовместима с некоторыми методами прокси, которые я использовал в этой области. Тем не менее, я не особо задумывался об этом, поскольку считал, что делаю почти то же самое без JSX. Я пробовал разные вещи, но со временем так и не смог сократить разрыв. Однажды я наконец решил провести эксперимент, в котором вместо клонирования контекстов я обернул состояние замыканиями и не мог поверить, насколько это было быстрее.

Так что JSX или вариант, подобный Hyperscript, внезапно показался привлекательным. Однако с точки зрения предварительной компиляции JSX намного проще внедрить. Поэтому я написал плагин Babel, чтобы добиться цели. Это позволило настроить вашу собственную среду выполнения так, чтобы ее можно было использовать с любой библиотекой Fine Grained. Babel Plugin JSX DOM Expressions теперь имеет плагины, расширяющие его до Knockout (https://github.com/ryansolid/ko-jsx), MobX (https://github.com/ryansolid/ mobx-jsx ) и Solid . Ключевое различие между этой работой и более ранней работой заключается в закрытии, устраняющем необходимость во вложенных контекстах, что значительно упрощает вещи, и вместо того, чтобы создавать элементы один за другим, шаблоны JSX фактически компилируются обратно в строковые шаблоны без динамических выражений и устанавливаются в узел шаблона, поэтому что то их можно клонировать по запросу. Как оказалось, клонирование узлов происходит намного быстрее, чем document.createElement и добавление. Кроме того, если вы будете получать доступ только к определенным свойствам элемента DOM, на самом деле это значительно снижает стоимость. Например, node.childNodes работает исключительно медленно, поскольку это не настоящий массив, и при обращении к нему вы заставляете браузер выполнять кучу дополнительной работы.

Бенчмаркинг

В целом результат был впечатляющим. Но мне нужно было увидеть, где на самом деле находится решение. Я должен сказать, что не был готов к миру JS Benchmarking. Я разрабатывал в основном с помощью моего удобного теста окружностей для оптимизации и наткнулся на несколько приемов, но я понял одну вещь: все не всегда так, как кажется. Борис Каул, автор JS Framework ivi, написал замечательную статью Как добиться успеха в тестах Web Framework », в которой действительно подчеркиваются оптимизации, которые как бы обманывают эти тесты для получения лучших результатов. Их известность привела к изменению Контрольных показателей или, по крайней мере, сглаживанию игрового поля. С тех пор также были достигнуты успехи в инкрементальном рендеринге, который использует Принципы анимации, чтобы предложить улучшенные впечатления от просмотра. Это просто код для повышения производительности за счет намеренного отбрасывания кадров. Это делает многие исторические ориентиры менее полезными сегодня. Поэтому вместо того, чтобы сосредоточиться на всех читах, я попытаюсь извлечь смысл из результатов, чтобы подтвердить принятый подход.

Прочная «структура», которую я собрал из всех частей (контейнер, управление изменениями, средство визуализации), как правило, является наиболее производительной из библиотек, использующих плагин Babel, благодаря его ядру S.js и тому, которое я буду сравнивать здесь. У него есть некоторые накладные расходы на эргономику. Там, где он принимает попадание, его объект состояния является прокси-сервером ES6, а его установщик состояния предлагает мощный синтаксис, который имеет немного больше накладных расходов, чем если бы вы вручную писали свои обновления. Несмотря на то, что Fine Grained, я намерен сделать код легко понятным для аудитории, знакомой с React там, где это имеет смысл.

Наконец, все, что я использую, установлено в последней версии Chrome. Таким образом, результаты могут сильно отличаться в других браузерах, особенно с плохой поддержкой ES Proxy.

4. Тест "Круги"

Это был первый тест, с которым я когда-либо сталкивался, сравнивая веб-фреймворки. Джереми Ашкенас, автор Backbone, сделал этот JSFiddle еще в 2012 году, чтобы проиллюстрировать разницу в производительности между Backbone и Ember. Это было сделано путем анимации 100 кругов в цикле setTimeout 0. Этот тест был невероятно грубым для измерения времени основного цикла, и со временем появилось несколько форков, сравнивающих разные фреймворки и оптимизированных VanillaJS. Я перестроил этот тест, сделав 300 кругов и измерив полное время между итерациями цикла. Вы можете проверить это здесь: Тест кругов. В общем, это не так полезно для теста, как когда-то, поскольку современные компьютеры намного более производительны, а масштабирование узлов еще больше отходит от любой практической полезности сценария.

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

Как прошла ярмарка в моей библиотеке. По общему признанию, этот тест находится в стадии разработки, и необходимо добавить больше библиотек. Но в целом довольно прилично. Для этого есть две версии реализации Solid. Тот, который больше похож на React with State, и еще один, немного более производительный, который использует такие сигналы, как Knockout. Учитывая цель базовой линии, более справедливо будет показать более простые примитивы для сравнения яблок с яблоками. Только другие наблюдения действительно показывают, что Inferno здесь действительно довольно быстр, а улучшенная производительность React 16 опережает Preact. Попробуйте сами в своем браузере, и я рекомендую обновляться между библиотеками.

3. DBMon

Этот тест имеет некоторую историю. Впервые его показал на React conf в 2015 году Райан Флоренс. Мне кажется, что именно Benchmark подтвердил первоначальные заявления React о том, что Virtual DOM является наиболее производительным подходом. Реализации Райана Angular и Ember были справедливыми с учетом инструментов, предоставляемых фреймворком, но этот сценарий определенно был адаптирован для демонстрации React. Матье Анслен создал сайт с большинством основных фреймворков (хотя для большинства из них он устарел на несколько лет), где вы можете их попробовать. Совершенно очевидно, что некоторые библиотеки очень много играли в эту, в то время как другие реализации, особенно некоторые из ранних мелкозернистых, настолько наивны (как Knockout), по сути, перерисовывая весь экран при каждом обновлении.

DBMon имитирует статистику мониторинга базы данных, по сути, быстро рассылает спам снимок двумерного массива поддельной информации мониторинга базы данных. В отличие от теста Circles Benchmark, который состоит из известных целевых обновлений, это полный дамп совершенно случайных новых данных, как если бы они были предоставлены сервером. Вы можете контролировать скорость мутаций, что помогает увидеть, как библиотеки работают в разных сценариях. Как я уже сказал, библиотеки Fine Grained были полностью вне сферы их применения, когда это было введено. Посмотрите, как библиотека Virtual DOM, например, React, всегда может в худшем случае просто повторно отрендерить весь Virtual DOM и применить только изменения. Для Fine Grained вы должны в основном сопоставить все точки данных с наблюдаемыми фикстурами данных и вручную проанализировать и применить обновления, прежде чем вы даже визуализируете. Для ранних библиотек Fine Grained, которые не выполняли пакетные операции или имели неудобные циклы выполнения, вероятно, было проще просто отказаться и перерисовать все целиком. Также существует оптимизированный режим, который изначально был добавлен, чтобы позволить Angular конкурировать, но другие фреймворки использовали его. Вместо того, чтобы давать новые капли данных, он видоизменяется на месте. Для библиотек, которые работают таким образом, это огромный импульс, но с учетом подхода Solid, заключающегося в сравнении предыдущего состояния с новыми изменениями, это было невозможно использовать.

Я скажу, насколько «реалистичен» этот сценарий, по моему опыту, это не очень распространенный случай для клиентских приложений. Я настоятельно рекомендую переместить ползунок мутации и заметить разницу в работе библиотек. Некоторые библиотеки относительно плоские, а другие - очень экстремальные. Мелкозернистые библиотеки оптимизированы для более низких уровней мутаций, как в большинстве приложений. Количество передач сервера и отсутствие имитируемой интерактивности делают это просто хорошим способом дисквалифицировать мелкомасштабную производительность. Если бы этот сценарий был вообще реальным, какова была бы реальная частота мутаций 5%, 10%. У меня ничего из этого не было.

Так как я поступил с Solid? Вы можете посмотреть и запустить мою реализацию здесь. Как всегда, пробег будет очень, но я очень доволен результатами. По производительности он на высоте с Inferno и самыми быстрыми библиотеками Virtual DOM. Да, есть несколько абсурдно быстрых реализаций, таких как Aurelia и Ripple, которые здесь используют некоторые очень специфические методы, но в целом производительность согласователя в Solid более чем достаточна для того, что я считаю наихудшим сценарием.

2. UIBench

Теперь мы переходим к более простому тесту. В отличие от двух предыдущих тестов, этот по-прежнему актуален и является одним из самых полезных инструментов для тех, кто создает библиотеку. UIBench был создан Борисом Каулем, автором фреймворка ivi. Этот тест запускает довольно исчерпывающий набор тестов по 3 сценариям, таблице, дереву и анимации списка. Он также классифицирует реализации и имеет множество различных режимов работы. Это то, что вам нужно для тех, кто хочет запустить свою библиотеку через батарею тестов рендеринга.

Одна из ключевых конфигураций заключается в том, следует ли измерять полную занятость или только время, потраченное на JS. По умолчанию используется JS, но я рекомендую немедленно установить этот флажок, чтобы использовать полное время рендеринга, поскольку в противном случае не учитываются методы, используемые для манипуляций с DOM. Далее идет оптимизация shouldComponentUpdate (sCU). Нажмите, чтобы отключить его. По умолчанию снова используется DBMon, но если вы находитесь в более реалистичных настройках, таких как Redux или Apollo, вы собираетесь настроить именно так. Неизменяемые справочные сопоставимые данные. Почему это не по умолчанию, ускользает от меня. Могу только предположить, что другие варианты лучше выделяют определенные библиотеки. Если вы хотите протестировать яблоки на яблоки, я предлагаю пропустить любую библиотеку, которая не сохраняет состояние или не перерабатывает узлы DOM. Оба этих подхода могут испортить реальные жизненные сценарии, и хотя новая точка интереса для демонстрации оптимизации производительности не имеет реальной общей ценности.

Так как же поступил Solid? Что ж, UIBench очень умно спроектирован, где реализации на самом деле не живут в репо, а вместо этого управляются URL-адресом. Просто вставьте это, и он запустит тест в новом окне (все оценки указаны внизу начальной страницы):

Https://ryansolid.github.io/solid-uibench

Полученные результаты. В среднем неплохо. В приведенной выше конфигурации итоги сопоставимы с Inferno и ivi, намного превосходя другие реализации Virtual DOM. Борис рекомендует не рассматривать итоги как результат, который я могу отстать в том смысле, что количество тестов непропорционально важности этих тестов. Например, анимационных тестов всего 4. Но, с другой стороны, счет - это счет, и каждое очко, которое вы отдаете одному месту, которое возмещается в другом месте, - это честная игра. Solid определенно демонстрирует слабые места для деревьев, особенно при вставке и сортировке по краям списка, но действительно хорош для большинства операций с таблицами. Table Activate и Anim Benchmark Solid - самые быстрые из всех, что имеет смысл, поскольку они полагаются на частичные обновления. Но для сравнения, даже там, где Solid слаб, более популярные библиотеки, такие как React, намного слабее в этом тесте.

Здесь стоит на минутку позвонить Stage0. Поскольку я много говорил о Fine Grained и Virtual DOM, эта библиотека относится к третьему типу. Он не может реализовать оптимизацию sCU из-за того, как работает согласование DOM, но это не повлияет на его результаты в этом тесте. Stage0 - это скорее служебный пояс (подумайте о jQuery), чем библиотека представления, поэтому в некоторых аспектах это не совсем справедливое сравнение. Но он разделяет тот же базовый подход, что и DomC, который работает и быстро развивается.

1. Тест JS Frameworks Benchmark

JS Frameworks Benchmark действительно №1. Это единственное, что я представляю здесь, которое вы не сможете легко запустить в собственном браузере, чтобы самому увидеть результаты. Но компромисс - это тест, который очень точно измеряет гораздо более реалистичные сценарии, чем другие тесты. Он не полагается на многократную рассылку больших наборов данных и не полностью подписывается на недавний поток тестов, касающихся времени до начальной отрисовки, осмысленной отрисовки, интерактивности и т. Д. Они рассказывают только историю первых (очень важных) секунд загрузка страницы. Этот тест выходит далеко за рамки простого рассмотрения общих операций, которые будут выполняться над таблицей (или на самом деле с любой загруженной сеткой / списком), включая пропускную способность передачи и потребление памяти этими операциями. В нем действительно есть всего понемногу: от полных рендеров, замены узлов, сортировки, выбора, частичных обновлений, одиночного удаления, полного удаления, добавления. Кроме того, он эффективно использует регулирование ЦП для имитации устройств с низким энергопотреблением, чтобы действительно выявить узкие места в производительности. В общем, это бесценный инструмент для тестирования производительности в реальных условиях.

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

Прежде всего, на этой странице есть 2 набора по 3 таблицы. Вы можете просто игнорировать последние 3 таблицы. Они предназначены для так называемых результатов без ключа, когда повторно используются узлы DOM. Это интересная оптимизация производительности в некоторых сценариях, но побочные эффекты в целом более пагубны, чем даже эта статья (которая является отличным введением в идею). Каждая реальная библиотека также должна иметь реализацию с ключом, поэтому мы можем смело игнорировать это.

Во-вторых, хотя это может измениться к тому моменту, когда вы это прочтете, Solid довольно сильно уступает (или, в зависимости от того, как вы смотрите на) DomC, как наиболее производительный фреймворк. Технически две ванильные реализации JS и Stage0 возглавляют пакет, но они по сути являются эталонными сборками идеализированного вручную созданного решения и не предписывают ту же модель, что и другие библиотеки в тесте. В десятку лучших входят еще несколько эталонных сборок, включая Web Component и Web Asm. Если вы пропустите их, если бы вы сегодня перечислили наиболее производительные фреймворки в пределах 20% от наиболее оптимизированного ванильного JS, это выглядело бы так:

  1. DomC (скомпилированный DOM)
  2. Твердый (мелкозернистый)
  3. Излишек (мелкозернистый)
  4. ivi (виртуальный DOM)
  5. Knockout JSX (мелкозернистый)
  6. MobX JSX (мелкозернистый)
  7. PetitDOM (виртуальный DOM)
  8. Inferno (виртуальный DOM)

Здесь хорошее сочетание различных технологий, и все они очень близки. Но у каждого подхода есть свои сильные и слабые стороны. Виртуальная модель DOM обычно использует больше памяти (хотя некоторые библиотеки Fine Grained довольно раздуты, но это не ошибка модуля рендеринга), тогда как у скомпилированной DOM меньше всего. Fine Grained имеет лучшую производительность при частичном обновлении и выборе строки.

В-третьих, было некоторое обсуждение того, что эталонный тест несправедливо настроен для Fine Grained, поскольку он имеет меньше привязок, чем реальное приложение на узел DOM. Но я оспариваю это. По моему опыту, количество динамических привязок относительно невелико на узел DOM, и именно обработчики событий и статические свойства составляют большинство привязок. Это фактически повлияло на дизайн привязок в плагине Babel. За исключением, возможно, эффекта наведения (наведения указателя мыши), эта таблица довольно реалистична и, вероятно, будет обработана в CSS. Добавление еще одной ячейки таблицы может означать еще одну привязку, но это также означает и для узлов DOM.

Наконец, по умолчанию список отсортирован по геометрическому замедлению, которое можно рассматривать как общую оценку производительности. Но нажмите «Частичное обновление» и посмотрите, насколько по-другому выглядит список. Angular внезапно резко поднялся в списке. Нокаут, который находится в противоположном конце списка, можно увидеть, не прокручивая страницу по горизонтали. И наоборот, Vue вернулся. Я бы сказал, что влияние этого теста во всяком случае недостаточно представлено, так как при рендеринге страницы обычное дело, взаимодействие с ним сводится к частичному обновлению. Хотя одно это еще не конец истории, но тайно, почему Angular немного быстрее, чем обычно кажется, а Vue - наоборот. Вам нужно как минимум полюбить согласованность React.

Заключение

Если вы зашли так далеко, то престижно. В этой статье есть много ссылок и вещей, с которыми можно поиграть, но она не такая наглядная, а тема немного сухая с большим количеством деталей. Но, возможно, вы чему-то научились или лучше разбираетесь в аспектах проектирования, создания и проверки средства визуализации JS. Что еще более важно, это признание всех отличных возможностей в средствах визуализации и понимание того, как они могут быть объединены в «ваш» фреймворк, не зацикливаясь на большой тройке.

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