There is a function that draws triangles on the canvas area in each lines:
function DivotTemplate(divot, canvas) {
if (divot === void 0) { divot = defaultConfig; }
var config = __assign(__assign({}, defaultConfig), divot);
var ctx = canvas.getContext('2d');
if (!ctx)
throw Error('Canvas context error');
var width = canvas.width;
var height = canvas.height;
var step = config.step, radius = config.radius, strokeStyle = config.strokeStyle, triangleWidth = config.triangleWidth;
var x = [1, 1, triangleWidth + 1];
var y = [1, triangleWidth, triangleWidth / 2];
var context = canvas.getContext('2d');
if (!context)
throw Error('Canvas context error');
/* Рисуем три точки для первого треугольника */
for (var c = 0; c < height; c++) {
if (c % 2 === 0) {
x[0] = triangleWidth + 1;
x[1] = triangleWidth + 1;
x[2] = 1;
}
else {
var s = step / 2;
x = [1 + s, 1 + s, triangleWidth + 1 + s];
}
for (var i = 0; i < width; i = i + step) {
context.beginPath();
context.arc(x[0], y[0], radius, 0, 2 * Math.PI, false);
context.strokeStyle = strokeStyle;
context.stroke();
context.beginPath();
context.arc(x[1], y[1], radius, 0, 2 * Math.PI, false);
context.strokeStyle = strokeStyle;
context.stroke();
context.beginPath();
context.arc(x[2], y[2], radius, 0, 2 * Math.PI, false);
context.strokeStyle = strokeStyle;
context.stroke();
/* Соединяем две точки из трех */
context.beginPath();
context.moveTo(x[2], y[2]);
context.lineTo(x[0], y[0]);
context.strokeStyle = strokeStyle;
context.stroke();
context.beginPath();
context.moveTo(x[2], y[2]);
context.lineTo(x[1], y[1]);
context.strokeStyle = strokeStyle;
context.stroke();
x[0] = x[0] + step;
x[1] = x[1] + step;
x[2] = x[2] + step;
}
x = [1, 1, triangleWidth + 1];
y[0] = y[0] + step;
y[1] = y[1] + step;
y[2] = y[2] + step;
}
return canvas;
}
I measured the performance of this function and get bad indicators (pic. 1)
How it can be improved?
Why context.strokeStyle = strokeStyle;
takes over 4 seconds?
Maybe pull out context.beginPath();
outside of loop? Or combine some operators? How to optimize canvas drawing?
1 Answer 1
2D canvas performance
You are running on a VERY slow system. Make sure you are not running any extension when measuring performance.
Avoid GPU state changes
The biggest performance penalty when using the 2D canvas API are state changes.
State changes are when you change rendering settings (state) such as the color. The state needs to be moved from the CPU to the GPU which means shutting down many systems so that the main Address and Data Bus can communicate with the GPU Address and Data Bus. Not only does this slow your code, but all processes are effected.
You can see the penalty on lines 44, 48, 52, 58, and 63, where you set context.strokeStyle = strokeStyle;
(approx 400ms) This penalty is forced as your styles are all identical (The 2D API does not check if the style needs to be changed)
The most basic fix is to batch all render calls using the same style as one path operation. ctx.beginPath(); /* ... build path ... */; ctx.stroke();
Note that building a path does not require GPU communication until the render function is called. ctx.stroke
or ctx.fill
Avoid exceptions.
Using try
, catch
, or throw
in JS will mark the code (and any code called, or in the same scope) as Do not optimise
. Meaning that the optimiser does not get to improve the performance.
Avoid arrays
Indexing into arrays is slower than using variables directly.
Creating an array is much slower than assigning variables directly. Up to a point, for 3 items its best to use variables, for 12 items then an array is better, but still avoid creating a new array, reuse the existing array.
General points
Avoid repeated calculations. Especially true if you have the code marked Do not optimise
Examples...
x = [1, 1, triangleWidth + 1];
at bottom of outer loop is not needed.context.arc(x[2], y[2], radius, 0, 2 * Math.PI, false);
Pre-calculate 2.PI. Use the defaults, eg the last argumentfalse
is the default and thus not needed.You get the 2D context twice. Only needed once.
For
ctx.arc
ifradius
is less than 2px usectx.rect(x - radius. y- radius, diameter, diameter)
wherediameter
is calculated outside the loops asconst diameter = radius * 2
ctx.rect
is much quicker thanctx.arc
and at <2px radius they a visually very similar. Note when usingctx.rect
reducing the radius by0.8
will more closely approximate the visual density of the arc.You are rendering 2 connected line segments using two
moveTo
calls. They are connected and as such only need onemoveTo
(see rewrite)
Rewrite
The following rewrite will be a huge improvement in performance. My estimate is at least twice as fast.
function DivotTemplate(divot = {}, canvas) {
const TAU = 2 * Math.PI;
const config = {
...defaultConfig,
... divot
};
const ctx = canvas.getContext('2d');
const width = canvas.width, height = canvas.height;
const step = config.step;
const radius = config.radius;
const strokeStyle = config.strokeStyle;
const triangleWidth = config.triangleWidth;
var x0 = 1, x1 = 1, x2 = triangleWidth + 1;
var y0 = 1, y1 = triangleWidth, y2 = triangleWidth / 2;
var c, s, i;
/* One state change */
ctx.strokeStyle = strokeStyle;
/* Build one path */
ctx.beginPath();
for (c = 0; c < height; c++) {
if (c % 2 === 0) {
x0 = triangleWidth + 1;
x1 = triangleWidth + 1;
x2 = 1;
} else {
s = step / 2;
x0 = 1 + s;
x1 = 1 + s;
x2 = triangleWidth + 1 + s;
}
for (i = 0; i < width; i += step) {
ctx.moveTo(x0 + radius, y0);
ctx.arc(x0, y0, radius, 0, TAU);
ctx.moveTo(x1 + radius, y1);
ctx.arc(x1, y1, radius, 0, TAU);
ctx.moveTo(x2 + radius, y2);
ctx.arc(x2, y2, radius, 0, TAU);
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
x0 += step;
x1 += step;
x2 += step;
}
y0 + =step;
y1 += step;
y2 += step;
}
/* One render call */
ctx.stroke();
return canvas;
}
Multiple calls
If the function DivotTemplate
is called many times (with the same settings) move setup, beginPath
, and stroke
out of the function and build one path for the many DivotTemplate
calling stroke when all done.
Render pixels directly
It is unclear what the various values of divot
are but if step
is near 1 px then the whole function should write pixels via a buffer and avoid the 2D context path calls altogether.