Rendering in AUX
This document describes how the AUX library renders widgets. The different techniques used are important to understand when using AUX widgets within other applications. This is especially true when widgets are dynamically resized or their style sheets are changed dynamically. This document also details some optimizations to increase DOM performance, which might be of interest to others.
DOM manipulations and scheduling
The appearance of a widget is determined by the values of its options and
CSS declarations. By design it does not matter in which order options
in a widget are set. This allows us to delay and reorder DOM manipulations
arbitrarily. In particular, DOM manipulations are usually not performed when
an option is modified using the set()
method. Instead, widgets will simply
record which options have changed and (in case they are visible) register
themselves to be rendered in the next animation frame.
The rendering is done by calling a method called redraw()
.
This method checks which options have been modified since the last time a
widget was rendered and perform all necessary modifications to the DOM.
Delaying the actual DOM modifications has several advantages from a performance
perspective. It also allows us to apply other optimizations, which we will go
into later.
The main advantage of delaying DOM modifications is that it makes performance much more reliable. This is particularly important on slow devices in applications which have a very high update frequency. One example is an application displaying audio levels, where update intervals of 20 ms are not uncommon. When using several level meters in parallel, the cost of DOM manipulations might prevent a slow device from keeping up with all the modifications. This would lead to high delays and possibly make the application completely unresponsive. On the other hand, when DOM manipulations are delayed to a rendering frame, the application is guaranteed to display the latest data at the highest frame rate it can achieve.
There are several other optimizations which can be used when DOM manipulations are not performed immediately.
- DOM manipulations can be completely disabled for widgets, which are not currently visible.
- In modern browsers (especially on tablets and phones) rendering can be completely turned off when the application window is in the background. Note that this can not be easily done by completely disabling the application, since the application logic might need to continue running.
AUX furthermore employs a technique which we will explain below. We call this technique DOM scheduling.
The run time performance of code operating on the DOM tree can be substantially
influenced by the amount of reflows and style recalculations which it
triggers. Style recalculations might be triggered by asking for the value of
a computed style of an element. Likewise reflow operations might be necessary
when asking for pixel positions or dimensions of an element, e.g. by reading
element.offsetWidth
. Ideally, these kinds of operations are avoided entirely,
however in some situations that might not be possible. Inside of AUX there
are several widgets which need to measure dimensions when rendering, for instance
the AUX.Fader
element, which needs to measure the size of the fader handle,
in order to position the scale and background correctly.
On the other hand, the operations which might trigger reflows and style recalculations do so only if the DOM tree has been previously modified. If the DOM tree is clean, these operations are relatively cheap. Therefore the obvious way to improve the performance of code which triggers many reflows is to reorder operations on the DOM in such a way as to reduce reflows to a minimum. It is rather straightforward to apply this technique to individual pieces of code, however that might not be sufficient when different parts of an application modify the DOM in parallel. If each of these pieces of code is optimized to trigger just one reflow, the whole application will still trigger as many reflows as the number of individual pieces of code. In other words, reducing the number of reflows locally does not scale. This performance problem might not be a serious one in simple application or web sites. However, in complex applications which might consist of 1000s of widgets it can seriously degrade performance.
Consider this rather artificial example:
function autosize(node) {
// might trigger reflow
var size = Math.min(node.parentNode.innerWidth,
node.parentNode.innerHeight);
// invalidates the DOM
node.style.width = size + "px";
node.style.height = size + "px";
}
function autosize_all(nodes) {
for (var i = i; i < nodes.length; i++)
autosize(nodes[i]);
}
In the above example autosize()
will measure the size of the parent of node
and
resize node
such that it fills its parent while staying square. When called on
several nodes it will force a reflow once for each node. The number of reflows can be
reduced to one by doing all the measurements before resizing all the nodes.
function measure(node) {
return Math.min(node.parentNode.innerWidth,
node.parentNode.innerHeight);
}
function resize(node, size) {
node.style.width = size + "px";
node.style.height = size + "px";
}
function autosize_all(nodes) {
var sizes = [];
for (var i = i; i < nodes.length; i++)
sizes[i] = measure(nodes[i]);
for (var i = i; i < nodes.length; i++)
resize(nodes[i], sizes[i]);
}
Unfortunately most real world situations are considerably more complex than this example and therefore this problem can not be solved by reordering DOM manipulations manually.
DOM scheduling is a technique which addresses this problem in a way which can be practically implemented inside complex applications. A requirement for it to work is that the individual modifications are independent of each other. As explained in the beginning of this document, this is the case for AUX widgets. The central component of DOM scheduling is the scheduler. It is a simple global object which executes a series of functions in a certain order. The order of execution is determined by a priority. Without going into the details of the implementation we can illustrate this by our example:
var S = new DOMScheduler();
function autosize(node) {
// might trigger reflow
var size = Math.min(node.parentNode.innerWidth,
node.parentNode.innerHeight);
// run the following with priority 1
S.add(function() {
// invalidates the DOM
node.style.width = size + "px";
node.style.height = size + "px";
}, 1);
}
function autosize_all(nodes) {
for (var i = i; i < nodes.length; i++)
S.add(autosize.bind(window, nodes[i]), 0);
S.run();
}
A similar thing happens inside of AUX. Different redraw()
methods
in widgets are added for the next scheduler run if their options have
been modified. The DOMScheduler eventually runs during the next animation
frame. The individual redraw()
methods will schedule certain parts of the
DOM manipulation to be run at different priorities. This guarantees that
operations of different widgets which would trigger a reflow are run at the
same time.
Note that the above code can be more elegantly implemented using ECMAScript 6 Generators, which makes the necessary code modifications even less invasive.