JS Browser Reflow and Repaint


Published: 2016-05-29
Updated: 2016-09-12
Web: https://fritzthecat-blog.blogspot.com/2016/05/js-browser-reflow-and-repaint.html


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

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.





ɔ⃝ Fritz Ritzberger, 2016-05-29