Canvas Path Performance

Posted in JavaScript by ebenpack
Tags: ,

If you've done much work with the HTML5 canvas API, and especially if you've ever looked into performance tuning your canvas apps, you've likely come across the advice to batch your canvas calls together. For example, you may have read that when drawing multiple lines or shapes, it's better to create a single path and only call your draw method once, drawing all lines and shapes in one go, than it is to draw each line or shape individually. In other words, this:

1
2
3
4
5
6
7
8
ctx.beginPath();
lineArray.forEach(function(line){
    ctx.moveTo(line.startx, line.starty);
    ctx.lineTo(line.endx, line.endy);
});
// Draw all lines at once.
ctx.stroke();
ctx.closePath();

is preferable to this:

1
2
3
4
5
6
7
8
 lineArray.forEach(function(line){
    ctx.beginPath();
    ctx.moveTo(line.startx, line.starty);
    ctx.lineTo(line.endx, line.endy);
    // Draw each line individually.
    ctx.stroke();
    ctx.closePath();
});

As I recently discovered, however, this does not always hold true. Performance in certain browsers actually degrades very quickly as the number of subpaths increases above a certain threshold. More information about how different browsers perform can be found at this jsperf.

The following test method was used in order to obtain quantitative data to investigate this issue:

 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
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Long Path</title>
</head>
<body>
    <canvas id="canvas" width="600" height="600"></canvas>
    <script>
        (function(){
            var canvas = document.getElementById('canvas');
            var ctx = canvas.getContext('2d');
            ctx.strokeStyle = "red";

            var results = [];

            // Draw increasingly long paths
            for (var i=0; i<2000; i+=20){
                ctx.clearRect(0,0,600,600);
                var start = performance.now();
                ctx.beginPath();
                for (var j=0; j<i; j++){
                    ctx.moveTo(0, j);
                    ctx.lineTo(j, 0);
                }
                ctx.stroke();
                ctx.closePath();
                var end = performance.now();
                results.push(end-start);
            }
        })();
    </script>
</body>
</html>

What this code is doing is drawing paths to the canvas with increasingly many subpaths. performance.now() was used to measure execution time, as it provides higher resolution timestamps than Date.now(). Results were stored in an array, which was used to produce the chart below.

0 200 400 600 800 1,000 1,200 1,400 1,600 1,800 2,000 0 200 400 600 800 execution time (milliseconds) number of subpaths Chrome Firefox

The takeaway, it would seem, is that you may see performance drop off precipitously in some browsers when the number of subpaths in your path reaches or exceeds ~600. If you encounter this issue, in order to work around it, paths should be periodically drawn and closed. In other words, paths should not be fully batched together, but should be batched into chunks. Experimentation has shown that keeping subpaths to <200 provides relatively good performance.

Addendum: Further testing suggests this issue is not very widespread at all, and does not affect all versions of Firefox. Currently, these results are reproducible in Firefox 31.0 running on Arch Linux. Firefox 31.0 running on OS X 10.7 does not produce similar results. After further investigation, this sounds like it may be an issue with cairo.