Speeding up charts using OffscreenCanvas

Chart rendering can seriously impact the browser. Especially when it comes to outputting a multitude of elements representing diagrams in the interface of a complex application. You can try to improve the situation using the interface OffscreenCanvas, the level of support of which browsers are gradually growing. It allows, using a web-worker, to shift to it the tasks of forming an image displayed in a visible element <canvas>. The article, the translation of which we are publishing today, is devoted to the use of the interface . Here we will talk about why this interface might be needed, about what really can be expected from its use, and about what difficulties may arise when working with it.



OffscreenCanvas

Why pay attention to OffscreenCanvas?


The code involved in the rendering of the diagram may itself have sufficiently high computational complexity. In many places, you can find detailed stories about how to output smooth animation and to ensure convenient user interaction with the page, it is necessary that the rendering of one frame fits within about 10 ms, which allows you to reach 60 frames per second. If the calculations required to output one frame take more than 10 ms, this will result in noticeable "slowdowns" of the page.

When displaying diagrams, however, the situation is aggravated by the following:

  • โ€” - (SVG, canvas, WebGL) , , .
  • , , , , 10 . ( โ€” ) .

These problems are ideal candidates for a classic optimization approach called divide and conquer. In this case, we are talking about the distribution of computing load across multiple threads. However, before the appearance of the interface, OffscreenCanvasall the rendering code needed to be executed in the main thread. The only way this code could use the APIs it needed.

From a technical point of view, โ€œheavyโ€ calculations could be output to a web worker thread earlier. But, since the calls responsible for rendering had to be made from the main stream, this required the use of complex message exchange schemes between the worker stream and the main stream in the chart output code. Moreover, this approach often gave only a slight performance gain.

SpecificationOffscreenCanvasgives us a mechanism for transferring surface control to display element graphics <canvas>in a web worker. This specification is currently supported by Chrome and Edge browsers (after Edge has been migrated to Chromium). It is expected that in Firefox support OffscreenCanvaswill appear within six months. If we talk about Safari, it is still unknown whether support for this technology is planned in this browser.

Chart output using OffscreenCanvas



An example of a chart that displays 100,000 multi-colored dots

In order to better assess the potential advantages and disadvantagesOffscreenCanvas, let's look at an example of the output of the โ€œheavyโ€ diagram shown in the previous figure.

const offscreenCanvas = canvasContainer
  .querySelector('canvas')
  .transferControlToOffscreen();
const worker = new Worker('worker.js');
worker.postMessage({ offscreenCanvas }, [offscreenCanvas]);

First, we query OffscreenCanvasthe element canvasusing the new method transferControlToOffscreen(). Then we call the method worker.postMessage({ canvas: offscreenCanvas }, [offscreenCanvas]), doing this to send a message to the worker containing a link to offscreenCanvas. It is very important that here, as the second argument, you need to use [offscreenCanvas]. This allows you to make the owner of this object worker, which will allow him to manage alone OffscreenCanvas.

canvasContainer.addEventListener('measure', ({ detail }) => {
  const { width, height } = detail;
  worker.postMessage({ width, height });
});
canvasContainer.requestRedraw();

Considering that the sizes were OffscreenCanvasoriginally inherited from the attributes widthand the heightelement canvas, it is our responsibility to keep these values โ€‹โ€‹up to date when the element is resized canvas. Here we use the event measurefrom d3fc-canvas, which will give us the opportunity, in agreement with requestAnimationFrame, to convey to the worker information about the new dimensions of the element canvas.

In order to simplify the example, we will use components from the d3fc library . This is a set of auxiliary components that either aggregate d3 components or complement their functionality. You OffscreenCanvascan work with without d3fc-components. All that will be discussed can be done using exclusively standard JavaScript features.

Now go to the code from the file worker.js. In this example, for the sake of a real increase in rendering performance, we are going to use WebGL.

addEventListener('message', ({ data: { offscreenCanvas, width, height } }) => {
    if (offscreenCanvas != null) {
        const gl = offscreenCanvas.getContext('webgl');
        series.context(gl);
        series(data);
    }

    if (width != null && height != null) {
        const gl = series.context();
        gl.canvas.width = width;
        gl.canvas.height = height;
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    }
});

When we receive a message containing a property canvas, we assume that the message came from the main thread. Next, we get the canvascontext from the element webgland pass it to the component series. Then we call the component series, passing it the data ( data), which we want to render using it (below we will talk about where the corresponding variables came from ).

In addition, we check the properties widthand height, which came in the message, and use them to set the dimensions offscreenCanvasand viewing area of WebGL. The link offscreenCanvasis not used directly. The fact is that the message contains either a property offscreenCanvasor properties widthand height.

The remaining worker code is responsible for setting up the rendering of what we want to output. There is nothing special here, so if all of this is familiar to you, you can immediately proceed to the next section in which we discuss performance.

const randomNormal = d3.randomNormal(0, 1);
const randomLogNormal = d3.randomLogNormal();

const data = Array.from({ length: 1e5 }, () => ({
    x: randomNormal(),
    y: randomNormal(),
    size: randomLogNormal() * 10
}));

const xScale = d3.scaleLinear().domain([-5, 5]);

const yScale = d3.scaleLinear().domain([-5, 5]);

First, we create a dataset containing the coordinates of points randomly distributed around the origin x / y, and adjust the scaling. We do not set the range of x and y values โ€‹โ€‹using the method range, since the WebGL series are displayed in full size of the element canvas (-1 -> +1in the normalized coordinates of the device).

const series = fc
    .seriesWebglPoint()
    .xScale(xScale)
    .yScale(yScale)
    .crossValue(d => d.x)
    .mainValue(d => d.y)
    .size(d => d.size)
    .equals((previousData, data) => previousData.length > 0);

Next, we set up a series of points using xScaleand yScale. Then we configure the appropriate means of access that allow you to read data.

In addition, we define our own equality checking function, which is designed to ensure that the component does not transmit dataGPUs at each rendering. We must express this explicitly, because even if we know that we will not modify this data, the component cannot know about it without performing a resource-intensive โ€œdirtyโ€ check.

const colorScale = d3.scaleOrdinal(d3.schemeAccent);

const webglColor = color => {
    const { r, g, b, opacity } = d3.color(color).rgb();
    return [r / 255, g / 255, b / 255, opacity];
};

const fillColor = fc
    .webglFillColor()
    .value((d, i) => webglColor(colorScale(i)))
    .data(data);

series.decorate(program => {
    fillColor(program);
});

This code allows you to colorize the chart. We use the index of the point in the data set to select a suitable color from colorScale, then convert it to the required format and use it to decorate the output point.

function render() {
    const ease = 5 * (0.51 + 0.49 * Math.sin(Date.now() / 1e3));
    xScale.domain([-ease, ease]);
    yScale.domain([-ease, ease]);
    series(data);
    requestAnimationFrame(render);
}

Now that the points are colored, all that remains is to animate the chart. Due to this, we will need a constant method call render(). This, in addition, will create some load on the system. We simulate the increase and decrease chart scale using requestAnimationFrameto modify the properties xScale.domainand yScale.domainevery frame. Here, time-dependent values โ€‹โ€‹are applied and calculated so that the scale of the chart changes smoothly. In addition, we modify the message header so that a method is called to start the rendering cycle render(), and so that we do not have to directly call series(data).

importScripts(
    './node_modules/d3-array/dist/d3-array.js',
    './node_modules/d3-collection/dist/d3-collection.js',
    './node_modules/d3-color/dist/d3-color.js',
    './node_modules/d3-interpolate/dist/d3-interpolate.js',
    './node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.js',
    './node_modules/d3-random/dist/d3-random.js',
    './node_modules/d3-scale/dist/d3-scale.js',
    './node_modules/d3-shape/dist/d3-shape.js',
    './node_modules/d3fc-extent/build/d3fc-extent.js',
    './node_modules/d3fc-random-data/build/d3fc-random-data.js',
    './node_modules/d3fc-rebind/build/d3fc-rebind.js',
    './node_modules/d3fc-series/build/d3fc-series.js',
    './node_modules/d3fc-webgl/build/d3fc-webgl.js'
);

In order for this example to work, it remains only to import the necessary libraries into the worker. We use for this importScripts, and also, so as not to use the tools for building projects, we use a list of dependencies compiled manually. Unfortunately, we canโ€™t just download the complete d3 / d3fc builds, since they are DOM dependent, and what they need in the worker is not available.

โ†’ The full code for this example can be found on GitHub

OffscreenCanvas and performance


Animation in our project works thanks to the use requestAnimationFrameof a worker. This allows the worker to render pages even when the main thread is busy with other things. If you look at the project page described in the previous section and click on the button Stop main thread, you can pay attention to the fact that when the message box is displayed, the update of the timestamp information stops. The main thread is blocked, but the chart animation does not stop.


Project Window

We could organize a rendering initiated by receiving messages from the main stream. For example, such events could be sent when a user interacts with a chart or when receiving fresh data over the network. But with this approach, if the main thread is busy with something and no messages are received in the worker, the rendering will stop.

Continuous rendering of the chart in conditions when nothing happens on it is a great way to annoy the user with a buzzing fan and low battery. As a result, it turns out that in the real world, the decision on how to divide responsibilities between the main thread and the worker thread depends on whether the worker can render something useful when he does not receive messages about a change in the situation from the main thread.

If we talk about the transfer of messages between threads, it should be noted that here we applied a very simple approach. Honestly, we need to transmit very few messages between threads, so I would rather call our approach a consequence of pragmatism, not laziness. But if we talk about real applications, it can be noted that the message transfer scheme between the flows will become more complicated if the userโ€™s interaction with the diagram and updating the visualized data will depend on them.

You can use the standard MessageChannel interface to create separate message channels between the main thread and the worker thread.. Each of these channels can be used for a specific category of messages, which makes it easier to process messages. As an alternative to standard mechanisms, third-party libraries like Comlink can act . This library hides low-level details behind a high-level interface using proxy objects .

Another interesting feature of our example, which becomes clearly visible in retrospect, is the fact that it takes into account the fact that GPU resources, like other system resources, are far from endless. Translation of rendering into a worker allows the main thread to solve other problems. But the browser, anyway, needs to use the GPU to render the DOM and what was generated by the means OffscreenCanvas.

If the worker consumes all the GPU resources, then the main application window will experience performance problems, regardless of where the rendering is performed. Pay attention to how the speed of updating the timestamp in the example decreases as the number of displayed points increases. If you have a powerful graphics card, then you may have to increase the number of points transmitted to the page in the query string. To do this, you can use a value that exceeds the maximum value of 100000, specified using one of the links on the example page.

It should be noted that here we did not explore the secret world of context attributes. Such a study could help us to increase the productivity of the solution. However, I did not do this by doing so because of the low level of support for these attributes.

If we talk about rendering using OffscreenCanvas, then the attribute looks most interesting here desynchronized. It, where it is supported, and taking into account some restrictions, allows you to get rid of synchronization between the event loop and the rendering loop that is executed in the worker. This minimizes the delay in updating the image. Details about this can be found here .

Summary


The interface OffscreenCanvasgives developers the opportunity to improve chart rendering performance, but using it requires a thoughtful approach. Using it, you need to consider the following:

  • (, , ) - .

    • , . , , , postMessage.
  • , (, SVG/HTML- <canvas>), , , .

    • , , , , , , , , .
  • , GPU, OffscreenCanvas .

    • OffscreenCanvas, GPU-, , . , , .

Here is an example code, and here is its working version.

Dear readers! Have you used OffscreenCanvas to speed up the display of graphics on web pages?


All Articles