📜 ⬆️ ⬇️

Improving HTML5 canvas performance

Recently I have been lucky to stumble upon interesting articles for translation. This time is an HTML5Rocks article on HTML5 canvas performance. The author writes about some kind of wall, which developers come up against when creating applications. Some time ago, I rested in it when porting the good old game to canvas.

Unfortunately, the graphics in the original inserted through the iframe. I could take pictures and place their images, but the author himself is positioning the graphics relevant and updated, so I simply placed links to them. Enjoy reading!

image
  1. Introduction
  2. Performance testing
  3. Pre-draw on virtual canvas
  4. Group calls
  5. Avoid unnecessary state changes.
  6. Draw only the difference, not the entire canvas.
  7. Use layered canvas for complex scenes.
  8. Avoid shadowBlur
  9. Various ways to clear the screen
  10. Avoid non-integer coordinates
  11. Optimize animations with 'requestAnimationFrame'
  12. Most canvas mobile implementations are slow.
  13. Conclusion
  14. Links

')

Introduction


HTML5 canvas, which began as an experiment by Apple, is the most widely accepted standard for 2D direct graphics on the Internet. Many developers use it in a wide range of multimedia projects, visualizations and games. Be that as it may, with the increasing complexity of applications, developers inadvertently stumble upon a performance wall.

There are many scattered canvas scans everywhere. This article aims to bring them together to create a more readable resource for developers. The article includes both fundamental optimizations that apply to all areas of computer graphics, as well as specific techniques for canvas that change as canvas implementations evolve. In particular, as GPU acceleration is used, some of the techniques described will become less relevant. This will be indicated if necessary.

Keep in mind this article is not an HTML5 canvas tutorial. But you can study the relevant articles on HTML5Rocks, here is this chapter on Dive into HTML5 and lessons on MDN .


Performance testing


In the fast-paced world of HTML5 canvas, JSPerf ( jsperf.com ) helps you verify that all of the proposed optimizations are still working. JSPerf is a web application that allows developers to write JavaScript performance tests. Each test focuses on the result that you are trying to get (for example, cleaning the canvas) and includes different approaches. JSPerf runs each option as much as possible in a short period of time and displays a statistically meaningful number of iterations per second. More is always better!

Visitors to the JSPerf page can run tests in their browser and allow JSPerf to store normalized results on Browserscope ( browserscope.org ). Since the optimization techniques in this article are stored on JSPerf, you can always go back and see actual information about whether this technique is still applicable. I wrote a small helper application that displays these results as graphs used in the article.

All performance test results in this article are tied to the browser version. This seems to be the limit, since we don’t know under which OS the browser was launched, or, more importantly, whether the HTML5 canvas hardware acceleration was enabled when testing was in progress. You can determine whether hardware acceleration is enabled in Chrome by typing about:gpu in the address bar.


Pre-draw on virtual canvas


If you draw similar primitives on the screen over many frames (as is often the case when writing a game), you can gain significant performance gains by drawing large pieces outside the scene. Preliminary drawing involves the use of virtual (or virtual) canvases, on which temporary images are drawn, and then copying virtual canvases onto the visible. For those familiar with computer graphics, this technique is also known as the display list .

For example, suppose you redraw Mario at 60 frames per second. You can draw his hat, mustache and “M” in each frame or pre-draw it before running the animation.

without pre-rendering:
 // canvas, context are defined function render() { drawMario(context); requestAnimationFrame(render); } 

pre-rendering:
 var m_canvas = document.createElement('canvas'); m_canvas.width = 64; m_canvas.height = 64; var m_context = m_canvas.getContext('2d'); drawMario(m_context); function render() { context.drawImage(m_canvas, 0, 0); requestAnimationFrame(render); } 


Pay attention to requestAnimationFrame, the use of which will be described in more detail a little later. The following graph demonstrates the benefits of using pre-rendering ( jsperf ): a graph .

This technique is especially effective when drawing is complex (drawMario). A good example is drawing a text, which is a very expensive operation. Here's how dramatic performance increases with pre-rendering text ( jsperf ):

Anyway, you can see in the example above the poor performance of the “pre-rendered loose” test. When previewing, it is important to make sure that your temporary canvas has a “tight” size for your image, otherwise the performance gain will meet with a loss of performance when copying one large canvas to another (which looks like a function of the size of the target canvas). A suitable canvas for the example above is smaller:
 can2.width = 100; can2.height = 40; 

In comparison with the large:
 can3.width = 300; can3.height = 100; 



Group calls


Since rendering is an expensive operation, it is much more efficient to load the drawing state machine with long lists of commands, and then unload them into the video buffer.

For example, when drawing multiple lines, it is much better to do one path with all the lines and draw it in one call. In other words, how to draw separate lines:
 for (var i = 0; i < points.length - 1; i++) { var p1 = points[i]; var p2 = points[i+1]; context.beginPath(); context.moveTo(p1.x, p1.y); context.lineTo(p2.x, p2.y); context.stroke(); } 


It is better to draw one broken line:
 context.beginPath(); for (var i = 0; i < points.length - 1; i++) { var p1 = points[i]; var p2 = points[i+1]; context.moveTo(p1.x, p1.y); context.lineTo(p2.x, p2.y); } context.stroke(); 


This also applies to canvas. When drawing a complex path, for example, it’s better to place all the points on it right away than to draw the segments separately ( jsperf ): a graph .

But keep in mind that with canvas there is an important exception to this rule: if the primitives of the object being drawn have small surrounding rectangles (bounding box), it may turn out to be more efficient to draw them separately ( jsperf ): a graph .


Avoid unnecessary state changes.


Canvas is implemented on the basis of a finite state machine, which tracks things like fill and stroke styles, like previous points that make up the current path. When trying to optimize, there is a temptation to focus on drawing. But manipulations with the state machine also lead to costs.

If you use multiple fill colors in a scene, it is cheaper to paint by color than by positioning it on the canvas. To draw a small stripe texture, you can draw a line, change the color, draw the following, and so on:
 for (var i = 0; i < STRIPES; i++) { context.fillStyle = (i % 2 ? COLOR1 : COLOR2); context.fillRect(i * GAP, 0, GAP, 480); } 


Or draw all the even and odd stripes:
 context.fillStyle = COLOR1; for (var i = 0; i < STRIPES/2; i++) { context.fillRect((i*2) * GAP, 0, GAP, 480); } context.fillStyle = COLOR2; for (var i = 0; i < STRIPES/2; i++) { context.fillRect((i*2+1) * GAP, 0, GAP, 480); } 


A comparison of these methods is presented in the following test ( jsperf ): a graph .

As expected, the first option is slower, as well as manipulating the state of the road.


Draw only the difference, not the entire canvas.


As you can imagine, the smaller part of the screen we draw, the cheaper it is. If you have only minor differences between redraws, you can get significant performance gains by drawing only the difference. In other words, how to clear the entire screen before drawing:
 context.fillRect(0, 0, canvas.width, canvas.height); 


Keep track of the bounding box being rendered and clean only it.
 context.fillRect(last.x, last.y, last.width, last.height); 


This is shown in the following test, which includes a white dot crossing the screen ( jsperf ): a graph .

If you understand computer graphics, you should be aware of this technique called “redraw regions”, in which the previous bounding box is saved and then cleared with each drawing.

This technique applies to pixel-by-pixel rendering, as in this discussion of the Nintendo JavaScript emulator .


Use layered canvas for complex scenes.


As mentioned earlier, drawing large images is expensive and should be avoided. In addition to using an offscreen buffer (pre-rendering section), we can use canvases overlaid on each other. Using the transparency of the upper layer, we can rely on the GPU to apply the alpha channel during rendering. You use this with two absolutely positioned canvases on top of each other, like this:
 <canvas id="bg" width="640" height="480" style="position: absolute; z-index: 0"> </canvas> <canvas id="fg" width="640" height="480" style="position: absolute; z-index: 1"> </canvas> 


The advantage over a single canvas is that when drawing or clearing the top, we do not affect the background. If a game or multimedia application can be split into 2 layers, it is better to draw them in different canvases to get a significant performance boost. The following graph compares the naive version with one canvas and the one where you redraw or clear the top layer ( jsperf ) as needed: the graph .

Often, one can benefit from flawed human perception and draw the background only once or less than the top layer (which attracts most of the user's attention). For example, you can N draws the top layer to draw the background only 1 time.

This method also applies to any other number of layers, if your application works better with this structure.


Avoid shadowBlur


Like other graphical environments, HTML5 canvas allows developers to blur primitives, but this operation is very expensive:
 context.shadowOffsetX = 5; context.shadowOffsetY = 5; context.shadowBlur = 4; context.shadowColor = 'rgba(255, 0, 0, 0.5)'; context.fillRect(20, 20, 150, 100); 


The test demonstrates the same scene, rendered with shadow and without shadow, and a drastic difference in performance ( jsperf ): the graph .


Various ways to clear the screen


Since canvas is a paradigm of direct graphics mode , the scene must be redrawn every frame. Because of this, clearing the canvas is an operation of fundamental importance in applications and games.

As stated in the section “Avoid unnecessary state changes,” clearing the entire canvas is often undesirable, but if you are required to do this, there are two ways to do this: call context.clearRect (0, 0, width, height) or use the hack: canvas.width = canvas .width ;.

At the time of writing, clearRect overtakes width reset, but, in some cases, using width reset is much faster in Chrome 14 ( jsperf ): graph .

Be careful with these tricks because it depends heavily on the implementation of canvas. For more information, see the Simon Sarris article on cleaning the canvas .


Avoid non-integer coordinates


HTML5 canvas supports sub-pixel rendering and there is no way to turn it off. If you draw with non-integer coordinates, it automatically uses anti-aliasing to smooth the lines. Here is the visual effect of sub-pixel performance from the Seb Lee-Delisle article :
bunny

If a smoothed sprite is not what you need, it will be much faster to translate your coordinates using Math.floor or Math.round ( jsperf ): a graph .

To translate non-integer coordinates into integers, there are several ingenious techniques, most of which are based on adding half to the number and applying bitwise operations to remove the mantis.
 // With a bitwise or. rounded = (0.5 + somenum) | 0; // A double bitwise not. rounded = ~~ (0.5 + somenum); // Finally, a left bitwise shift. rounded = (0.5 + somenum) << 0; 


Full performance breakdown here ( jsperf ): graph .

This optimization method will no longer make sense from the moment canvas implementations become GPU-accelerated, which allows you to quickly draw non-integer coordinates.


Optimize animations with `requestAnimationFrame`


A relatively new requestAnimationFrame API is recommended for implementing interactive applications in the browser. Instead of ordering the browser to render at a specific frequency, you politely ask it to call rendering and let you know when it is finished. As a nice addition, if the page is inactive, the browser is smart enough not to draw.

The call to requestAnimationFrame is aimed at 60 FPS, but does not guarantee it, so you should keep track of how much time has passed since the last rendering. It might look like this:
  var x = 100; var y = 100; var lastRender = new Date(); function render() { var delta = new Date() - lastRender; x += delta; y += delta; context.fillRect(x, y, W, H); requestAnimationFrame(render); } render(); 


Keep in mind that using requestAnimationFrame applies to both canvas and other techniques like WebGL.

At the time of writing, the API was only available for Chrome, Safari and Firefox, so you should use it carefully .


Most canvas mobile implementations are slow.


Let's talk about the mobile platform. Unfortunately, at the time of writing, only iOS 5.0 beta with Safari 5.1 used canvas hardware acceleration. Without it, mobile browsers simply do not have a sufficiently powerful CPU for modern canvas applications. Several of the tests above demonstrate the order of decline in the performance of the mobile platform compared to the desktop, significantly limiting the types of cross-platform applications that can be expected to work successfully.


Conclusion


This article has covered an extensive set of useful optimizations that will help you develop high-performance HTML5 canvas applications. Now that you have learned something new here, dare and optimize your incredible creations. Or, if you do not yet have a game or application for optimization, see Chrome Experiments or Creative JS for inspiration.


Links




Selected Comments


mikenerevarin #
On mobile devices, the situation is completely reversed - multilayered canvases greatly slow down, you have to use a 1 + scene prerender in the background (moreover, the div’s background is the size of a canvas and located below it, because inexplicably the background of the canvas also slows down much).
In many more clever ways, it is possible to achieve quite a normal speed of work on a mobile phone.

Theshock #
...
Judging by the code, it means that instead of each frame drawing graphic primitives, it is better to draw them once to the buffer and then draw from the buffer, and it’s true to draw a circle according to mathematical calculations, which are made in .arc or a curve, according to mathematical The calculations that are made in bezierCurveTo are much slower than just copying a picture of the appropriate size.

But the real "double buffering" does not make much sense - the browser has its own back buffer, and the back buffer is drawn onto the main layer:
1. Takes too much time.
2. Prevents search bottlenecks
...

Source: https://habr.com/ru/post/127014/


All Articles