Stoyan (@stoyanstefanov) is a frontend engineer, writer ("JavaScript Patterns", "Object-Oriented JavaScript"), speaker (JSConf, Velocity, Fronteers), toolmaker (Smush.it, YSlow 2.0) and a guitar hero wannabe.
Intrigued by Luke Smith‘s comment and also Alois Reitbauer‘s comment on the previous post about rendering I did some more testing with dynaTrace and SpeedTracer. Also prompted by this tweet, I wanted to provide an example of avoiding reflows by using document fragments as well as hiding elements with display: none.
(btw, sorry that I’m slow to respond to tweets and blog comments, just too much writing lately with the crazy schedule, but I do appreciate every tweet and comment!)
So welcome to the new game show: “Will it reflow?” where we’ll look into a few cases where it’s not so clear if the browser will do a reflow or just a repaint. The test page is here.
Changing classnames
The first test is fairly straightforward – we only want to check what happens when you change the class name of an element. So using “on” and “off” class names and changing them on mouse over.
.on {background: yellow; border: 1px solid red;} .off {background: white; border: 1px dashed green;}
Those CSS rules shouldn’t trigger a reflow, because no geometry is being changed. Although the test is pushing it a bit by changing borders, which may affect geometry, but not in this case.
The test code:
// test #1 - class name change - will it reflow? var onoff = document.getElementById('on-off'); onoff.onmouseover = function(){ onoff.className = 'on' ; }; onoff.onmouseout = function(){ onoff.className = 'off'; };
Sooo.. will it reflow?
In Chrome – no! In IE – yes.
In IE, even changing the class name declarations to only change color, which is sure not to cause reflow, still caused a reflow. Looks like in IE, any type of className
change causes a reflow.
cssText updates
The recommended way to update multiple styles in one shot (less DOM access, less reflows) is to update the element’s style.cssText
property. But.. will it reflow when the style changes do not affect geometry?
So let’s have an element with a style attribute:
...style="border: 1px solid red; background: white"...
The JavaScript to update the cssText
:
// test #2 - cssText change - will it reflow? var csstext = document.getElementById('css-text'); csstext.onmouseover = function(){ csstext.style.cssText += '; background: yellow; border: 1px dashed green;'; }; csstext.onmouseout = function(){ csstext.style.cssText += '; background: white; border: 1px solid red;'; };
Will it reflow?
In Chrome – no! In IE – yes.
Even having cssText
(and the initial style) only play with color, there’s still a reflow. Even trying to just write the cssText
property (as opposed to read/write with += ) still causes a reflow. The fact that cssText
property is being updated causes IE to reflow. So there might be cases where setting individual properties separately (like style.color
, style.backgroundColor
and so on) which don’t affect geometry, might be preferable to touching the cssText
.
Next contestant in the game show is…
addRule
Will the browser reflow when you update stylesheet collections programatically? Here’s the test case using addRule
and removeRule
(which in Firefox are insertRule
/deleteRule
):
// test #3 - addRule - will it reflow? var ss = document.styleSheets[0]; var ruletext = document.getElementById('ruletext'); ruletext.onmouseover = function(){ ss.addRule('.bbody', 'color: red'); }; ruletext.onmouseout = function(){ ss.removeRule(ss.rules.length - 1); };
Will it? Will it?
In Chrome – yes. The fact that style rules in the already loaded stylesheet have been touched, causes Chrome to reflow and repaint. Even though class .bbody
is never used. Same when creating a new rule with selector body {...}
– reflow, repaint.
In IE there’s a repaint definitely, and there’s also a kind of reflow. Looks like dynaTrace shows two kinds of rendering calculation indicators: “Calculating generic layout” and “Calculating flow layout”. Not sure what is the difference (web searches disappointingly find nada/zero/rien for the first string and my previous blog post for the second). Hopefully “generic” would be less expensive than “flow”.
display: none
In my previous post I boldly claimed that elements with display: none
will not have anything to do with the render tree. IE begs to differ (thanks to dynaTrace folks for pointing that out).
A good way to minimize reflows is to update the DOM tree “offline” out of the live document. One way to do so is to hide the element while updates are taking place and then show it again.
Here’s a test case where rendering and geometry are affected by simply adding more text content to an element by creating new text nodes.
// test #4 - display: none - will it reflow var computed, tmp; var dnonehref = document.getElementById('display-none'); var dnone = document.getElementById('bye'); if (document.body.currentStyle) { computed = dnone.currentStyle; } else { computed = document.defaultView.getComputedStyle(dnone, ''); } dnonehref.onmouseover = function() { dnone.style.display = 'none'; tmp = computed.backgroundColor; dnone.appendChild(document.createTextNode('No mo tests. ')); tmp = computed.backgroundColor; dnone.appendChild(document.createTextNode('No mo tests. ')); tmp = computed.backgroundColor; dnone.appendChild(document.createTextNode('No mo tests. ')); tmp = computed.backgroundColor; } dnonehref.onmouseout = function() { dnone.style.display = 'inline'; }
Will it reflow?
In Chrome – no. Although it does do “restyle” (calculating non-geometric styles) every time a text node is added. Not sure why this restyling is needed.
In IE – yes. Unfortunatelly display: none
seems to have no effect on rendering in IE, it still does reflows. I tried with removing the show/hide code and having the element hidden from the very beginning (with an inline style attribute). Same thing – reflow.
document fragment
Another way to preform updates off-DOM is to create a document fragment and once ready, shove the fragment into the DOM. The beauty is that the children of the fragment get copied, not the fragment itself, which makes this method pretty convenient.
Here’s the test/example. And will it reflow?
// test #5 - fragment - will it reflow var fraghref = document.getElementById('fragment'); var fragment = document.createDocumentFragment(); fraghref.onmouseover = function() { fragment.appendChild(document.createTextNode('No mo tests. ')); tmp = computed.backgroundColor; fragment.appendChild(document.createTextNode('No mo tests. ')); tmp = computed.backgroundColor; fragment.appendChild(document.createTextNode('No mo tests. ')); tmp = computed.backgroundColor; } fraghref.onmouseout = function() { dnone.appendChild(fragment); }
In Chrome – no! And no rendering activities take place until the fragment is added to the live DOM. Then, just like with display: none
a restyle is being performed for every new text node inserted. And even though the behavior is the same for fragments as for updating hidden elements, fragments are still preferable, because you don’t need to hide the element (which will cause another reflow) initially.
In IE – no reflow! Only when you add the final result to the live DOM.
Thanks!
Thank you for reading. Tomorrow if all goes well there should be a final post related to JavaScript and then moving on to … other topics 😉