You can do a lot of things in a web-page using JavaScript.
For example, increase its load- or update-time significantly.
I am talking about JS statements that cause the browser to do long-lasting work.
The classic reflow-pitfall is looping a lot of DOM-elements,
reading the element's clientWidth
, and then setting its style.width
.
The browser would have to renew its layout, what he does at least when the next clientWidth
is read.
Which CSS-property change triggers which browser action is documented on csstriggers.com and a lot of other good good articles. Generally a reflow or repaint is forced by
- insert, update, delete or move of a DOM element
- animation of a DOM element
- reading certain element-properties like offsetHeight, clientHeight etc.
- calling getComputedStye() on an element
- changing the style or class attribute of an element
- ....
In this Blog I will present a very simple JS module to get around this problem, written in pure JS (no jQuery).
Problem
The problem is the deferring of CSS-properties writes.
That means, you read some reflow-critical DOM-property like offsetWidth
,
and then you have to defer the resulting CSS-write on element.style.width
to a later time by calling a function, passing all necessary information as parameters.
When that later time has come, the settings must be triggered explicitly.
I call it repaint()
here. That means, a concrete application using this module will have
to call repaint()
at some point in time.
Solution
Here is the "deferred repainter" module.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | var deferredRepainter = function() { "use strict"; var repaintList = []; var that = {}; /** * Aggregates CSS properties for a later repaint() call. * @param element the element to set given CSS properties to on repaint. * @param cssMap the CSS properties to set to given element on repaint. */ that.addForRepaint = function(element, cssMap) { repaintList.push({ element: element, cssMap: cssMap }); }; /** Sets all aggregated CSS properties now. */ that.repaint = function() { for (var i = 0; i < repaintList.length; i++) { var element = repaintList[i].element; var cssMap = repaintList[i].cssMap; for (var css in cssMap) if (cssMap.hasOwnProperty(css)) element.style[css] = cssMap[css]; } repaintList = []; }; return that; }; |
This module factory creates a JS object that can aggregate updates for an element,
and perform them in the order they were added as soon as repaint()
gets called.
The addForRepaint()
function lets add CSS settings for later usage.
That way you can read properties from the DOM or CSS, and defer changes to later.
You need to pass the target DOM-element as parameter, and a map of CSS properties with values.
Calling repaint()
simply will go through the repaintList
and apply all aggregated changes. Finally it clears the list, thus
redundant calls to repaint()
will do nothing.
Example Application
Instead of reading and writing one by one, the application would first do all reads and aggregate
resulting writes, and then call repaint()
to flush all the writes.
var deferredRepaint = deferredRepainter(); var clientWidthToCssWidth = function(element, size) { var style = window.getComputedStyle(element); if (style["box-sizing"] === "border-box") return size + window.parseInt(style["border-left-width"]) + window.parseInt(style["border-right-width"]); return size - window.parseInt(style["padding-left"]) - window.parseInt(style["padding-right"]); }; var setWidth = function(element, clientWidth) { var width = clientWidthToCssWidth(element, clientWidth); var cssWidth = width+"px"; deferredRepaint.addForRepaint(element, { "width": cssWidth, "max-width": cssWidth, "min-width": cssWidth }); }; var elements = ....; for (var i = 0; i < elements.length; i++) { var element = elements[i]; setWidth(element, element.clientWidth + 4); } deferredRepaint.repaint();
This application would increase the width of some elements by 4 pixels.
It uses a sophisticated way to turn a clientWidth
into a CSS width
:
it calculates padding
or border
depending on the box-sizing
property.
Unfortunately calling getComputedStyle()
causes the browser to reflow when some CSS was changed before.
To avoid triggering continuous reflows, the application uses an instance of the deferredRepainter
module to defer the CSS writes via setWidth()
.
As soon as the loop has ended, deferredRepainter.repaint()
is called, which actually sets the values.
This works under the assumption that a browser would not do a reflow when just a CSS property was written
but nothing was read after. Should be true for most browsers.
When a browser does not comply to that, we would have to
write a dynamical stylesheet
with a different CSS-class for each of the elements, add this class to the element's classList
,
and then, on repaint, add that stylesheet to head
. This would do a bulk update with just one statement.