
Faster DOM - plurby
https://annevankesteren.nl/2016/05/faster-dom
======
CharlesW
> _That way, you only do the IDL-dance once and the browser then manipulates
> the tree in C++ with the many operations you fed it._

As a beginning web developer, I remember being surprised that there was no way
for me to group DOM manipulations into SQL-like transactions, or even an
equivalent to HyperCard's lockScreen.

I'm finding it a little tough to tell whether the author is talking about
putting the browser or the web developer in control of grouping DOM updates.

~~~
trebor
> [...] I remember being surprised that there was no way for me to group DOM
> manipulations into SQL-like transactions [...]

There is a way to group DOM manipulations: a DocumentFragment[0]. If you have
a wrapper, you can easily use

    
    
        parent.replaceChild(fragment, wrapper); // [1]
    

So long as you start with your fragment having a new copy of that wrapper—the
entire node and its children will be replaced in 1 operation. Then layout and
paint should only happen once apiece.

I've used similar techniques to make applications that render efficiently.

[0]: [https://developer.mozilla.org/en-
US/docs/Web/API/DocumentFra...](https://developer.mozilla.org/en-
US/docs/Web/API/DocumentFragment) [1]: [https://developer.mozilla.org/en-
US/docs/Web/API/Node/replac...](https://developer.mozilla.org/en-
US/docs/Web/API/Node/replaceChild)

~~~
fzzzy
That assumes that all the changes are grouped under the same part of the
tree...

~~~
trebor
Sure. If you have a ton of potentially layout-thrashing changes, just clone
your page's highest wrapper node. Then make your changes and replace the node.
I haven't needed it, or tested it for performance, but it's clearly doable.

    
    
        // Example; untested pseudo-code
        var fragment = document.createDocumentFragment(),
            wrapper  = document.getElementById('wrapper');
        
        fragment.appendChild(wrapper.cloneNode(true));
        // update your fragment
        // update the page
        document.body.replaceChild(fragment, wrapper);
    

That said, because the change is to a page's entire structure I'd think the
layout operation would be much slower. I'm betting React and Ember might do
similar things for their "application" wrapper/scope node(s).

~~~
RussianCow
You also potentially lose things like focus, input text, and selection on
elements that otherwise didn't need to be replaced. I'd wager that re-creating
all of that would add a discernible performance cost.

------
c-smile
We all know that Element.innerHtml = "..." can be significantly faster than
set of individual DOM mutating calls. Element.innerHtml = ... gets executed as
single transaction - relayout happens only once, at the end of it. Yet there
is no overhead on JS-native bridging and function calls in JS in general.

But the need of "transactioned" DOM updates is not just about the speed. There
are other cases when you need this. In particular:

1\. Massive DOM updates (that's your case), can be done with something like:
Element.update( function mutator(ctx) ). While executing the mutator callback
Element.update() locks/delays any screen/layout updates.

2\. That Element.update( function mutator(ctx) ) can be used with enhanced
transitions. Consider something like transition: blend linear 400ms; in CSS.
While executing the update browser makes snapshots of initial and final DOM
states and does blending of these two states captured into bitmaps.

3\. transactioned update of contenteditable. In this case Element.update()
groups all changes made by the mutator into single undoable operation. This is
a big deal actually in WYSIWYG online editors.

There are other cases of course, but these three functionalities are what I've
implemented in my Sciter Engine ( [http://sciter.com](http://sciter.com) )
already - proven to be useful.

~~~
dionidium
_" We all know that Element.innerHtml = "..." can be significantly faster than
set of individual DOM mutating calls"_

Not necessarily:

[http://stackoverflow.com/questions/8461851/what-is-better-
ap...](http://stackoverflow.com/questions/8461851/what-is-better-appending-
new-elements-via-dom-functions-or-appending-strings-w/8461877#8461877)

That's an old post, but even then the conventional wisdom didn't hold up.

~~~
c-smile
"Not necessarily".

Of course there are situations when these two methods of DOM update are on
par.

But in any case I agree with one of answers there:

"If you don't mind the fact that innerHTML is a bit limiting (only total
replacement of DOM sub-tree rooted at target element) and you don't risk a
vulnerability through injecting user-supplied content, use that. Otherwise, go
with DOM."

When you do for example this: element.setAttribute("a","b"); browser

1\. drops all resolved CSS rules on element, its siblings, its parent and all
children. Just in case you have CSS rules like:

    
    
      element[a="b"] > span { display:none }
    

2 Invalidates rendering tree of the element's parent.

And when you do element.appendNode(node):

it does the same as above + plus ensures consistency of HTML DOM (<select>
cannot contain <div>s for example).

If you have multiple calls like these then all steps above need to be done for
each of them. Engines can optimize such cases but not that much in some cases.

While browser do element.innerHtml it does each step strictly once: build
consistent DOM, resolve styles, build rendering tree and mark window [area] as
needed painting - request painting.

~~~
TabAtkins
> drops all resolved CSS rules on element, its siblings, its parent and all
> children.

Browsers optimize this, too - they try hard to keep up metadata structures
that tell them whether they _need_ to invalidate styling or not. (This is why
mutating the stylesheet is so expensive - browsers have to freshly rebuild the
supporting data structures. They _could_ optimize that too, but it's a lot of
effort for a rare case.)

So if there's no rules mentioning an [a] attribute in the page,
el.setAttribute("a", ...) will generally not do anything.

(One of the benefits of shadow DOM is that the shadow trees are scoped away
from the main tree and each other, so you can have more expensive rules in
them without making the _rest_ of the page slow. Yay for componentization!)

~~~
c-smile
"So if there's no rules mentioning an [a] attribute in the page,
el.setAttribute("a", ...) will generally not do anything."

That's true in general but there are exceptions as usual. Changing @href may
trigger not only [href] rules but also :link ones. And :link rules are always
present at least in default style sheet. @value triggers :empty. And so on.

But if someone in some library in galaxy far far away (small library used by
your application) will add that [a] rule then magically you will get a spike
on innocuous el.setAttribute("a", ...) calls in your code.

That's one of points of having transactional yet explicit DOM update
mechanism. We have jQuery, Angulars, React, Ember, Vue, (you name it) that
definitely need such thing. Sites/webapps that do not use one of those are
quite rare these days.

------
tixzdk
I'll leave this module here for those of you how haven't heard of it.
[https://github.com/patrick-steele-idem/morphdom](https://github.com/patrick-
steele-idem/morphdom)

It diff's the DOM instead of a VDOM

~~~
0n34n7
Thus it can be estimated that you can get, on average, a 45% speed
improvement?!

I'm going to try and bench this on a mobile webview and see what happens.

~~~
RussianCow
Where are you getting that number? The benchmarks in the readme show morphdom
being much slower for several operations.

~~~
bobwaycott
5 out of 31? And I'd say only 3 are noticeably slower, and only 1 seems to be
"much" slower (at 0.38ms). But given that just about everything benchmarked
seems to be sub-0.05ms, I'm left thinking that gaining noticeable and much-
increased performance is highly dependent on operations, and a high-precision
timer. Humans aren't going to notice a difference between an operation lasting
0.04ms vs 0.02ms. Unless a ton of operations add up to something noticeable,
of course.

After looking into it further, morphdom appears to be a solid alternative to
virtual DOMs. Seems worth having around for that reason alone, especially if
it were to improve with time, or become used by other tools.

------
m1el
I was playing with the idea of using JS objects as a template engine:
[http://m1el.github.io/jsonht.htm](http://m1el.github.io/jsonht.htm), and I'm
pretty happy with it.

I think DOM is pretty damn fast in modern browsers. `innerHTML` can be slower
than creating elements from a JS object!

Here's the script I'm using for timing:
[https://gist.github.com/m1el/b28625b3b9261f0fab819e866133e49...](https://gist.github.com/m1el/b28625b3b9261f0fab819e866133e49a)

My results in Chrome are: dom: 2663.580ms, innerHTML: 10999.449ms

~~~
coldtea
> _I was playing with the idea of using JS objects as a template
> engine:[http://m1el.github.io/jsonht.htm](http://m1el.github.io/jsonht.htm),
> and I'm pretty happy with it._

Isn't that the same as (among several other things) React's JSX?

~~~
abritinthebay
It's pretty much identical.

I don't think people realize that all JSX is, is a way to elegantly write out
something that transpiles down to a pure object representation of the DOM.

JSX and React have a few quirks where React means it's not _quite_ a _pure_
object, but the React devs are working on removing those.

------
bzbarsky
I wrote up a response to this but haven't found a good place to put it; might
as well post it here.

1) I think the idea about such an API encouraging good practice is very much
correct, in the sense that it makes it harder to interleave DOM modification
and layout flushes. That might make it worth doing on its own.

2) DOM APIs are not necessarily _that_ cheap in and of themselves (though they
are compared to layout flushes). Setting the various dirty flags can actually
take a while, because in practice just marking everything dirty on DOM
mutation is too expensive, so in practice UAs limit the dirty marking to
varying degrees. This trades off time inside the DOM API call (figuring out
what needs to be dirtied) for a faster relayout later. There is some
unpredictability across browser engines in terms of which APIs mark what dirty
and how long it takes them to figure that out.

3) The IDL cost is pretty small in modern browsers, if we mean the fixed
overhead of going from JS to C++ and verifying things like the "this" value
being of the right type. When I say "pretty small", I mean order of 10-40
machine instructions at the most. On a desktop machine, that means the
overhead of a thousand DOM API calls is no more than 13 microseconds. On
mobile, it's going to be somewhat slower (lower clocks, if nothing else,
smaller caches, etc). Measurement would obviously be useful.

There is the non-fixed overhead of dealing with the arguments (e.g. going from
whatever string representation your JS impl uses to whatever representation
your layout engine uses, interning strings, copying strings, ensuring that
provided objects, if any, are the right type, etc). This may not change much
between the two approaches, obviously, since all the strings that cross the
boundary still need to cross it in the end. There will be a bit less work in
terms of object arguments, sort of. But....

4) The cost of dealing with a JS object and getting properties off it is
_huge_. Last I checked in Gecko a JS_GetProperty equivalent will take 3x as
long as a call from JS into C++. And it's much worse than that in Chrome. Most
of the tricks JITs use to optimize stuff go out the window with this sort of
access and you end up taking the slowest slow paths in the JS engine. What
this means in practice is that foo.bar("x", "y", "z") will generally be much
faster than foo.bar({arg1: "x", arg2: "y", arg3: "z"}). This means that the
obvious encoding of the new DOM as some sort of JS object graph that then gets
reified is actually likely to be much slower than a bunch of DOM calls right
now. Now it's possible that we could have a translation layer, written in JS
such that JITs can do their magic, that desugars such an object graph into DOM
API calls. But then that can be done as a library just as well as it can be
done in browsers. Of course if we come up with some other way of communicating
the information then we can possibly make this more efficient at the expense
of it being super-ugly and totally unnatural to JS developers. Or UAs could do
some sort of heroics to make calling from C++ to JS faster or something....

5) If we do provide a single big blob of instructions to the engine, it would
be ideal if processing of that blob were side-effect free. Or at least if it
were guaranteed that processing it cannot mutate the blob. That would simplify
both specification and implementation (in the sense of not having to spell out
a specific processing algorithm, allowing parallelized implementation, etc).
Instructions as object graph obviously fail hard here.

Summary: I think this API could help with the silly cases by preventing layout
interleaving. I don't think this API would make the non-silly cases any faster
and might well make them slower or uglier or both. I think that libraries can
already experiment with offering this sort of API, desugaring it to existing
DOM manipulation, and see what the performance looks like and whether there is
uptake.

------
maxbrunsfeld
I'm curious about the source of the slowness of the IDL operations. Is it
specific to the DOM, or is he referring to overhead that exists for _any_
bindings between JS and C++?

~~~
bzbarsky
IDL operations are not particularly slow. Last I measured, a call from JS into
C++ is between 10 and 40 CPU instructions of fixed overhead depending on the
exact thing being called (method vs getter vs setter; they have slightly
different costs) and the browser involved. At least once you get into the top
JIT tier; baseline jit and interpreter can involve more overhead, obviously.

There's additional cost for dealing with the call arguments, too; this varies
widely by type of argument (e.g. an options object is clearly more expensive
to deal with than a boolean, or even than a list of arguments containing the
same information as the options object) and somewhat less widely by browsers.

~~~
pcwalton
> IDL operations are not particularly slow. Last I measured, a call from JS
> into C++ is between 10 and 40 CPU instructions of fixed overhead

To put that in perspective, that's the same number of instructions as
objc_msgSend, which every Objective-C method call in native iOS or Mac apps
goes through [1]. "The DOM is slow compared to native" is a common meme, but
it's not that simple.

[1]:
[http://sealiesoftware.com/msg/x86-mavericks.html](http://sealiesoftware.com/msg/x86-mavericks.html)

------
EGreg
Yeah, that's why in our framework, we go the FastDOM /greensock route and
encourage writing mutation code explicitly for when the state changes.

However to be fair the point of React, vdom, mithril etc. is to allow the
developers to write a mapping from state to view and let the mutations be
calculated automatically. They claim it's easier to just write the logic of
the mapping instead of all possible transitions. I don't buy it, but it has
grown to a huge community.

------
mmastrac
I think this is a natural evolution of where DOM has been headed. Batching
requests is entirely within the spirit of the original DOM.

------
lucio
Something like suspendLayout(), resumeLayout() I guess this GUI technique is
like... 50 years old?

------
lj3
At what point does fiddling with the DOM get complicated and unpredictable
enough that WebGL and/or Canvas starts to look like the saner alternative?

On that note, I just stumbled on an old framework that's been out of
development since 2012 called Blossom. It looks like it uses Canvas instead of
the DOM. Does anybody have any history with it?

~~~
bzbarsky
At the point when you decide you don't care about accessibility or actually
rendering any interesting text.

~~~
TabAtkins
Or search engines, which are a special case of disabled users.

------
amelius
Is there anything in a browser that ensures that simple updates with local
effect have O(1) layout cost?

Or do developers have to live with the uncertainty that simple updates may
have a higher computational complexity?

~~~
TabAtkins
The CSS 'contain' property helps ensure that local changes don't cause
dirtiness to spread higher up the tree. <[https://drafts.csswg.org/css-
containment/>](https://drafts.csswg.org/css-containment/>)

I know Chrome has an implementation that I think is still moving thru the
shipping cycle, and the other browsers are working on theirs iirc.

------
beders
Build faster browsers. Don't be afraid of dogma.

There were times before there was a DOM (you might not have been born yet) and
there can be times after DOM :)

------
krallja
> backed by C++ (née Rust)

"née" means "originally called" (usually someone's maiden name). I don't think
C++ used to be called Rust.

~~~
bobwaycott
Yeah, that stuck out to me, too. I'm still a bit fuzzy on how to interpret the
author's original intent.

~~~
dceddia
I think he meant "C++ (now Rust)", referring to the Servo browser using Rust -
[https://servo.org/](https://servo.org/)

~~~
sivvy
I think he is actually a she.

~~~
TabAtkins
"Anne" is a male name in the Netherlands. (I don't know if it can _also_ be a
female name there, but this Anne is definitely male.)

