Что такое DiffHTML.js?


DiffHTML — эта утилита для патчинга (частичного изменения) DOM-дерева. Она умеет находить разницу между существующим DOM-деревом и HTML-строкой, между двумя деревьями. В результате будут произведены только те изменения, которые реально имеют место быть. Те элементы которых не было — вставятся, атрибуты которые были реально изменены — изменятся, и только они. Остальные элементы останутся без изменений.

Зачем это?


Просто чтобы не трогать те элементы в которых не было изменений. Это в теории дешевле, чем ре-рендерить полностью все дерево.

Как же React.JS?


React делает почти тоже самое, но у DiffHTML есть существенный козырь — эта библиотека по-умолчанию не требует практически никакой инфраструктуры вокруг себя. Ни сборки, ни специальных трансформаций в реакт-объекты. Вы просто можете сделать следующее:

diff.innerHTML(document.body, '<h1>Hello world!</h1>');

И объект появится в DOM-дереве. Далее…

diff.innerHTML(document.body, '<h1 class=”title”>Hello world!</h1>');

И только атрибут class будет добавлен. Просто добавим параграф:

diff.innerHTML(document.body, '<h1 class=”title”>Hello world!</h1><p>Dear, World!</p>');

В общем идея очень простая и в то же время достаточно мощная.

Минусы (готовность для продакшена)


  • Проект молодой, поэтому здесь баги — это нормально
  • Проблемы с навешиванием событий (старые события автоматически не удаляются)
  • Нет большого количества информации вокруг этой библиотеки
  • Нет бенчмарков

Плюсы


  • В теории, быстрей простой перерисовки всех элементов внутри контейнера
  • Есть middleware, можно контролировать проецес патчинга
  • Есть подобие Virtual DOM, можно писать React-like шаблоны
  • Меньше проводить времени с точечными модификациями DOM-дерева вручную
  • Отзывчивый разработчик

Где применять?


Лично я вижу сферы применения:

  • В старом коде, работающем на базе традиционных шаблонов и el.innerHTML вставки, можно добиться ускорения производительности
  • Для сложных SaaS виджетов, где важен размер подключаемых библиотек
  • Для pet-проектов, где React избыточен, но на Vanilla.JS уже не хочется

А ToDo?


Есть ToDo, но как мне показалось — код сильно избыточен, поэтому я сделал свое:

Мой ToDo (DiffHTML, Babel DiffHTML tag transformer, Redux)

Вывод


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

Будет очень кстати, если кто то возьмется померять производительность.

» Github: github.com/tbranyen/diffhtml
» Issues: github.com/tbranyen/diffhtml/issues

Спасибо за чтение!

Update: в комментариях подсказали, что еще есть аналог — morphdomфорк).

Update 2: в одном из комментариев продолжил идею об уходе от работы напрямую с DOM к работе исключительно с шаблонами. Пересобрать HTML-строку всего приложения из шаблонов не так дорого, и пусть DiffHTML посчитает разницу и внесет изменения в DOM.
Поделиться с друзьями
-->

Комментарии (24)


  1. profesor08
    30.08.2016 21:41

    Значит эта утилита годится только для замены текущего dom дерева на новое, пришедшее из ajax ответа? Хотелось бы увидеть результаты тестов производительности с и без утилиты.


    1. jMas
      30.08.2016 21:49

      Можно еще UI компоненты делать, на подобие React / Riot (https://github.com/jmas/htmldiff-todo/blob/master/src/ui.js). Бенчмарки самому интересны.


    1. jMas
      30.08.2016 23:58
      +1

      Померял на реальном сложном куске HTML — DiffHTML показал такие результаты:

      el.innerHTML ... 742ms
      diffhtml.innerHTML() ... 136ms
      morphdom() ... 2023ms
      

      На однородном глубоком дереве из DIV лучше всего работает стандартный el.innerHTML, morphdom() — средний результат, а diffhtml.innerHTML — хуже всего.

      Листинг бенчмарка
      import * as diffhtml from 'diffhtml';
      // import morphdom from 'morphdom';
      
      var rootEl = document.getElementById('root');
      
      var tpl = `...`;
      
      // console.profile("diffhtml.innerHTML()");
      
      var fn = function () { return Math.ceil(Math.random()*1000); };
      var start = window.performance.now();
      
      for (var i=0,ln=1000; i<ln; i++) {
        // str += tpl;
        // V1 = innerHTML
        // rootEl.innerHTML = tpl//tpl.replace(/\{rnd\}/ig, fn);
      
        // V2 = diffhtml.innerHTML
        diffhtml.innerHTML(rootEl, tpl);
      
        // V3
        // morphdom(rootEl, tpl);
      }
      
      // console.profileEnd();
      console.log(Math.ceil(window.performance.now() - start));
      


      1. profesor08
        31.08.2016 04:28

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


        1. jMas
          31.08.2016 10:54

          Как раз на большом реальном дереве получилось очень быстро, быстрей чем обычный innerHTML. Попробую разобраться почему. За ссылку на бенч отдельное спасибо.


        1. jMas
          31.08.2016 11:02

          Нужно принять во внимание, что холодная вставка (пустой контейнер) будет скорей всего всегда хуже. В своем бенче у меня было 1000 операций вставки подряд одного и того же дерева.


      1. nuit
        31.08.2016 04:39
        +1

        >Как оказалось, не так просто подготовить объективный бенчмарк. Если есть идеи как оценить объективно — пишите.

        Если не лень, можете спортировать этот бэнчмарк https://github.com/localvoid/uibench-react/
        Вот пример результатов этого бэнча: https://cdn.rawgit.com/localvoid/6715c4b23eadc460112e671b4add3710/raw/907901966dd0473f1026d1ff25e244a022eb5ab1/uibench_results.html

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


  1. Riim
    30.08.2016 21:48
    +1

    Аналог: morphdom и мой форк с поддержкой встроенного svg, сохранением позиции фокуса и гарантированным переиспользованием существующих уникальных (с id, key — в моём форке) элементов (оригинальный morphdom всё ещё имеет несколько кейсов типа этого).


    1. jMas
      30.08.2016 21:50

      Спасибо за наводку. Мне очень интересна эта тема.


  1. spmbt
    30.08.2016 23:29

    Получается, что идея — в экономии некоторых if() в jQuery, лишь выбраны некоторые пороги узнавания диффов? Например, третий пример:

    var $temp = $('h1.title', document.body);
    if($temp.length && $temp.html() =='Hello world!')
        $temp.after('<p>Dear, World!</p>');
    
    Если так, то, действительно, наличие условных инструментов модификации DOM постоянно требуется. Но как в DiffHTML напишется аппенд к любому содержимому (innerHTML) блока $temp? Наверное, надо вводить метасимволы типа "*" и дальнейшие правила разбора шаблона (второго параметра).

    Не проще ли написать сразу плагин к jQuery для условной манипуляции DOM? Преимущество в том, что не создаётся новый инструмент, а расширяется давно известный, дополняются лишь правила. Не будет, например, перестановки параметров. Если смущает увесистость jQuery, то у меня есть функция на 1.5K, которая делает элементы условной манипуляции и при этом заменяет основные функции jQuery (аналогичные микролибы — $dom.js, balalaika и другие).

    И объём DiffHTML — 80К, что подсказывает, что туда вошли много правил, парсер шаблонов, частично исполняющие роли jQuery. Плагин был бы легче. Другими словами, хотел бы сказать, что оформленные идеи, работающие в коде — конечно, хорошо, но описанное в Гитхабе API наводит на мысли, что к частичным целям можно идти другими и более спрямлёнными путями.


    1. jMas
      31.08.2016 00:12

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


  1. TimsTims
    31.08.2016 00:16

    А что насчет исполнения/добавления javascript-кода/функций в пришедших данных?


    1. jMas
      31.08.2016 00:19

      Автор предлагает делать это инлайновыми onclick=${onButtonClick}, пример тут. Но с событиями навешенными другими способами могут возникнуть проблемы.


  1. Jabher
    31.08.2016 10:53

    А почему было не сделать обертку над incremental-dom?


    1. jMas
      31.08.2016 11:11

      Демается, что это было бы хорошо, но я прошел мимо лишь потому, что нет встроенного транслятора в incremental-dom объекты. А может быть есть?


  1. vird
    31.08.2016 10:54
    +2

    tl;dr не используйте ни diffhtml, ни morphdom. Они даже на синтетике не могут показать хорошие результаты.

    1. Открываем http://mathieuancelin.github.io/js-repaint-perfs/ в режиме инкогнито дабы исключить влияние плагинов браузера. (Прим. есть и более сложные способы получить более аккуратные результаты)
    2. Chrome devtools. Sources. Press Escape. Rendering. FPS meter. Включаем.
    3. Открываем http://mathieuancelin.github.io/js-repaint-perfs/diffhtml/index.html на разрешении 1920*1080. (Дабы потом результаты не расходились с таблицей ниже)
    4. Ждем 10 сек устаканивания fps.
    ВНИМАНИЕ. На каждый новый тест закрываем все вкладки инкогнито и открываем снова.

    В статье:
    diffhtml          14-17
    morphdom          11-17
    
    mainstream:
    (тут должен быть jquery)
    vanilla-optimized 31-33
    React             31-34
    Backbone          25-27
    Angular           28-31
    Angular 2.0 Alpha 38-40
    vuejs             35-36
    ember              0-1 (нужен тест с последней версией, не верю)
    Aurelia           33-36 ( http://jdanyow.github.io/aurelia-dbmonster/ )
    
    hipsters:
    riot              21-23
    Elm               32-36
    vidom             41-44
    Monkberry         43-45
    Inferno           40-43
    frzr              40-42
    моя поделка       40-43
    
    вне конкурса:
    canvas            60
    


    Software:
    Chrome 52.0.2743.116 m.
    Windows 7 64bit
    
    Hardware:
    CPU : AMD A10-7850K (не самый плохой процессор  https://www.cpubenchmark.net/high_end_cpus.html      5548 где-то сзади в high end)
    GPU : AMD 7970      (не самая плохая видеокарта http://www.videocardbenchmark.net/high_end_gpus.html 5246 где-то top 42)
    А теперь представьте как всё это будет тормозить на чём-то по-слабее
    


    Не удовлетворяемся результатами т.к. библиотека развивается. Может что-то пофиксили.
    1. Скачиваем содержимое http://mathieuancelin.github.io/js-repaint-perfs/diffhtml/index.html (chrome сохранить страницу целиком)
    2. Забрасываем в папку dbmon (diffHTML)_files https://raw.githubusercontent.com/tbranyen/diffhtml/master/dist/diffhtml.js
    3. Патчим до https://gist.github.com/vird/ff236a38b6b72e23a9e87c88e38760cd
    4. Запускаем по инструкции выше.
    Получаем тот же fps.

    morphdom, честно, было лень патчить и проверять.

    Замечания к методике тестирования и результатам принимаются.
    P.s. Есть еще todomvc benchmark. Кто бросит ссылку на большую сборную солянку 20+, тому отдельное спасибо. Ато на http://todomvc.com/ только примеры.


    1. vird
      31.08.2016 10:58

      BTW. На хабре еще упоминались

      matreshka.js  26-28
      Angular light 35-38
      


    1. Starche
      31.08.2016 11:25

      Как жаль, что у riot такой низкий показатель. Мне прям очень нравится эта библиотечка. Буду знать, что для перегруженных UI придётся от неё отказываться


      1. nuit
        31.08.2016 11:34
        +1

        dbmon для riot'а — это бэст кэйс, на других кэйсах там всё гораздо печальнее. API у риота вроде нормальное, впаривать они тоже умеют (всякая ложь типа минимальное кол-во дом операций итп), но вот если взглянуть на исходники, то реализация полное говно :)


    1. jMas
      31.08.2016 11:31

      У меня наоборот

      diffhtml ... 19-20
      Angular ... 12-14
      

      MacBook Pro i5 / Chrome

      Да, оказывается, на деле у DiffHTML показатели средние по больнице. Спасибо за наводку на бенчмарк.


      1. vird
        31.08.2016 12:40
        +1

        Вы тестировали неоптимизированный angular. Попробуйте
        http://mathieuancelin.github.io/js-repaint-perfs/angular/opt.html
        http://mathieuancelin.github.io/js-repaint-perfs/angular-track-by/


        1. vird
          31.08.2016 12:43

          Вторая ссылка по недосмотру битая
          http://mathieuancelin.github.io/js-repaint-perfs/angular-track-by/


  1. CodeViking
    31.08.2016 14:44
    +1

    Какие планы у автора касательно своего проекта? Планирует ли автор предлагать библиотеку как инструмент равный любому популярному фреймворку или это будет фича из разряда пониже (к примеру underscore.js)?


    1. jMas
      31.08.2016 14:47

      Хороший вопрос, пока ничего по этому поводу от автора не слышал. Но судя по инструментарию (транформер html в подобие Virtual DOM), автор предлагает DiffHTML как возможную альтернативу Rect.JS, например. Ну это лично у меня сложилось такое мнение.