Optimize HTML5 canvas rendering with layering

Most graphical elements on platforms need some form of optimization. In this article, learn about the optimization technique of layering canvas elements. Walk through a simple example to learn how to identify layers, and explore unique rendering methodologies for optimizing layers. Layering canvases is an optimization strategy that you can apply to any interactive real-time scene.

Adam Ranfelt, Software Developer, The Nerdery

Adam RanfeltAdam Ranfelt is a living, breathing example of The Nerdery's commitment to being technologically agnostic. A 2009 graduate from the University of Minnesota with a degree in computer science, Adam enjoys working on the forefront of technology. He is an expert in HTML, ActionScript, Java, and Objective-C. Before joining The Nerdery in 2010, Adam worked at Curb-Crowser Design, where he built and maintained numerous digital media projects. A firm believer in using the right technology for the job, Adam is well-versed in over ten programming languages and counting. In 2012, he was promoted to principal software engineer in recognition of his technical and leadership skills.



30 October 2012

Also available in Chinese Russian Japanese

Introduction

Often, in 2D games or when rendering HTML5 canvas, optimization is performed so multiple layers are used to build a composite scene. In low-level rendering such as OpenGL or WebGL, rendering is performed by clearing and drawing to a scene every frame. After implementation, games require optimization to cut down on the amount of rendering, at various costs. Because the canvas is a DOM element, it gives you the capability to layer multiple canvases as a method of optimization.

Frequently used abbreviations

  • CSS: Cascading Style Sheets
  • DOM: Document Object Model
  • HTML: HyperText Markup Language

In this article, explore the rationale for layering a canvas. Learn about the DOM setup to accomplish the layered canvases. Using layering for optimization requires an assortment of practices. This article also explores the conceptual and technical points of a handful of optimization strategies to augment the layered approach.

You can download the source code for the examples used in this article.


Choosing an optimization strategy

It can be difficult to select the best possible optimization strategy. When choosing a layered scene, consider how the scene is composed. Large screen real-estate renderings that often reuse several components are good candidates to investigate. Effects such as parallax or animated entities often require a lot of changing screen space. It's good to be aware of these situations when exploring your best optimization strategy. Though canvas-layered optimization requires several different techniques, it offers a significant boost when applied properly.


Setting up layers

When using a layered approach, the first step is to set up the canvas on the DOM. Typically, this would be as simple as defining a canvas element and placing it into the DOM, but layers require some extra styling. Using CSS, there are two requirements for the layering of canvases to be successful:

  • Canvas elements must co-exist on the same location on the viewport.
  • Each canvas must be visible underneath another canvas.

Figure 1 shows the general overlapping concept behind the layer setup.

Figure 1. Layer example
layer example diagram

The steps for setting up the layer are:

  1. Add the canvas elements into the DOM.
  2. Add canvas positioning style to allow for layering.
  3. Style canvas elements to have a transparent background.

Set up canvas overlay stack

Creating an overlay stack in CSS can require a small amount of style. There are many ways to perform overlays using HTML and CSS. The example in this article uses a single <div> tag to contain the canvases. The <div> tag will specify a unique ID, which will apply styles to its child HTML5 canvas elements, as shown in Listing 1.

Listing 1. Canvas positioning style
#viewport {
    /**
     * Position relative so that canvas elements
     * inside of it will be relative to the parent
     */
    position: relative;
}
 
#viewport canvas {
    /**
     * Position absolute provides canvases to be able
     * to be layered on top of each other
     * Be sure to remember a z-index!
     */
    position: absolute;
}

The container <div> accomplishes the overlay requirement by styling all child canvas elements to use absolute positioning. By choosing to have the #viewport use relative positioning, you future-proof so that any absolute layout styles applied to child styles will be relative to the #viewport container.

The order of these HTML5 canvas elements does matter. The ordering can be managed by either the order of appearance of the elements on the DOM or by styling a z-index style in the order the canvases should display. While not always the case, other styles may impact the rendering; be careful when introducing additional styles, such as any of the CSS transforms.

Transparent backgrounds

Use overlapping visibility to accomplish the second style requirement for the layer technique. The example uses the option to set a background color to a DOM element, as shown in Listing 2.

Listing 2. Stylesheet rule for setting a transparent background
canvas {
    /**
     * Set transparent to let any other canvases render through
     */
    background-color: transparent;
}

Styling canvases to have a transparent background achieves the second requirement for having visible overlapping canvases. Now that you have the markup and styles constructed to satisfy the layering needs, you can set up a layered scene.


Layer considerations

When selecting an optimization strategy, be aware of any trade-offs of using that strategy. Layering an HTML5 canvas scene is a strategy that places weight on the runtime memory for the benefit of runtime speed. You add more weight to the page's in-browser weight to get a faster framerate. A canvas should, in general, be considered a graphics surface on the browser that includes a graphics API.

By performing tests in Google Chrome 19 and documenting the browser's tab memory usage, you can see an obvious trend in memory usage. The test uses a <div> that is already styled, as discussed in the previous section, and generates single-color filled canvas elements that are placed onto the <div>. Canvases are sized at 1600 x 900 pixels, and the data is collected from the task manager utility in Chrome1. Table 1 shows an example.

In Google Chrome's Task Manager, you can see the amount of memory, or RAM, usage for a page. Chrome also provides GPU memory, or the memory that the GPU is using. This is generally information such as geometry, textures, or any sort of buffered data that the computer might need for pushing your canvas data to screen. The lower the memory, the less weight is put on the computer. While there aren't any hard numbers to live by, always test to make sure that your programs don't go overboard and use too much memory. If too much is used, the browser or page will crash due to lack of memory resources. GPU processing is an immense programming pursuit that is outside the scope of this article. You can get started by learning OpenGL or reviewing Chrome's documentation (see Resources).

Table 1. Canvas layer memory overhead
Layer countMemoryGPU memory
030.011.9
137.628.9
137.628.9
249.046.6
352.259.6
858.498.0
1665.0130
32107187

In Table 1, as more HTML5 canvas elements are introduced and used on the page, more memory is used. The general memory also has a linear correlation, though the growth per additional layer is significantly less. While this test does not elaborate on the implications these layers have on performance, it does show that the canvases will heavily impact GPU memory. Always remember to perform stress tests on your target platforms to make sure the platforms don't have constraints that make your application unable to perform.

When choosing to change a single canvas rendering cycle to a layered solution, consider the performance gain with respect to the memory overhead. Despite the memory cost, this technique works by decreasing the number of pixels modified on each frame.

The next section explains how to use layering to organize a scene.


Layering the scene: the game

In this section, learn about a multi-layer solution by refactoring a single-canvas implementation of a parallax effect on a scrolling platform runner-style game. Figure 2 shows how the game view is made up of clouds, hills, a ground, a background, and some interactive entities.

Figure 2. Composite game view
composite game view

In the game, the clouds, hills, ground, and background will all move at different rates. Essentially, they will achieve a parallax effect with elements that are further in the background moving slower than elements in the front. To make the situation more complicated, the background will be moving slow enough that it will only re-render every half of a second.

Normally, a good solution would clear and re-render the screen every frame because the background is an image and constantly changing. Because the background in the example is changing only twice a second, you don't need to re-render it every frame.

Now that the workspace is defined, you can determine which parts of the scene should be on a layer. Once the layers are organized, you'll investigate the various rendering strategies to use for the layering. To begin, consider implementing the solution using a single canvas, as shown in Listing 3.

Listing 3. Single canvas render loop pseudo code
/**
 * Render call
 *
 * @param {CanvasRenderingContext2D} context Canvas context
 */
function renderLoop(context)
{
    context.clearRect(0, 0, width, height);
    background.render(context);
    ground.render(context);
    hills.render(context);
    cloud.render(context);
    player.render(context);
}

The solution would have a render function, like the code in Listing 3, being called every game loop call or every update interval. In this case, the render is abstracted out of the main loop call and the update call that would update the positions of each element.

Following the greedy clear-to-render mentality, the render call clears the context and follows it up by calling the render functions of each respective entity on screen. Listing 3 follows a very procedural path for placing the elements onto the canvas. While this solution is effective in rendering the entities on the screen, it neither describes any of the render methods used nor allows for any sort of rendering optimization.

To better elaborate on the entity's render methods, use two types of entity objects. Listing 4 shows the two entities you'll be using and refining.

Listing 4. Renderable Entity pseudo code
var Entity = function() {
    /**
     Initialization and other methods
     **/
     
    /**
      * Render call to draw the entity
      *
      * @param {CanvasRenderingContext2D} context
      */
    this.render = function(context) {
        context.drawImage(this.image, this.x, this.y);
    }
};
 
var PanningEntity = function() {
    /**
     Initialization and other methods
     **/
     
    /**
      * Render call to draw the panned entity
      *
      * @param {CanvasRenderingContext2D} context
     */
    this.render = function(context) {
        context.drawImage(
            this.image,
            this.x - this.width,
            this.y - this.height);
        context.drawImage(
            this.image,
            this.x,
            this.y);
        context.drawImage(
            this.image,
            this.x + this.width,
            this.y + this.height);
    }
};

The objects in Listing 4 store instance variables for the entity's image, x, y, width, and height. The objects follow JavaScript syntax, but for the sake of brevity are incomplete pseudo code of the intended object. At the moment, the render algorithms very greedily render out their image to the canvas without any respect to any other needs for the game loop.

For performance, it's important to note that the panning render call outputs a larger amount of its image than necessary. This particular optimization will be overlooked for this article, but be sure to render out only the necessary patches when using less image space than your image provides.


Determining the layers

Now that you know how to implement the example using a single canvas, let's find ways to refine this type of scene and speed up the render loop. To use the layer technique, you have to identify the HTML5 canvas elements the layering needs by finding the entities' rendering overlap.

Redraw regions

To determine overlap, consider some invisible areas called redraw regions. Redraw regions are areas where entities' images that require canvas clearing are drawn. Redraw regions are important for rendering analysis because they allow you to find optimization techniques to better render the scene, as shown in Figure 3.

Figure 3. Composite game view with redraw regions
Composite game view with redraw regions

To visualize the effect in Figure 3, each entity in the scene has an overlay representing the redraw region spanning the viewport width and the entity's image height. The scene can be divided into three groups: background, foreground, and interactive. Redraw regions in the scene have a colored overlay to differentiate the different regions:

  • Background - Black
  • Clouds - Red
  • Hills - Green
  • Ground - Blue
  • Red ball - Blue
  • Yellow obstacle - Blue

For all overlays except the ball and obstacle, the redraw region spans the width of the viewport. These entities' images fill nearly the entire screen. Due to their panning requirement, they will render over the entire width of the viewport, as illustrated in Figure 4. The ball and obstacle are expected to be moving across the viewport and can have the respective region defined by an entities' position. If you remove the images rendered to the scene to leave just the redraw regions, you can easily see the separate layers.

Figure 4. Redraw regions
Redraw regions

Initial layers are obvious because you can note regions that overlay one another. Since the ball and obstacle regions overlay the hills and the ground, these entities can be grouped into one layer called the interactive layer. The interactive layer will be the top layer because of the render order of the game entities.

Another method to find additional layers is to collect all of the regions that do not overlap. The red, green, and blue regions that span the viewport do not overlay, and they make up the second layer—the foreground. The clouds and the interactive entities' regions do not overlap, but because the ball could jump into the red region, you should consider that entity to be on a separate layer.

For the black region, you can easily infer that the background entity makes up the final layer. While not applicable to this scene, any regions that fill the entire viewport, such as the background entity, should be considered for encompassing an entire layer. Having defined our three layers, we can begin assigning these layers to canvases, as shown in Figure 5.

Figure 5. Layered game view
layered game view

Now that the layers for each of the grouped entities are defined, you can begin optimizing the canvas clearing. The goal with this optimization is to save processing time by reducing the amount of screen real estate rendered at each step. It is important to note that each image might be better optimized using a different strategy to render. The next section explores optimization methods for various entities or layers.


Rendering optimization

Optimizing the entities is the core of the layering strategy. Layering the entities allows for a render strategy to be employed. In general, an optimization technique tries to remove overhead. As noted in Table 1, by introducing layers, you've added memory overhead. The optimization techniques discussed here will decrease the amount of work the processor has to do in order to speed up the game. The goals are to find a way to reduce the amount of space to render and to remove as many render and clear calls as possible that occur for each step.

Single entity clearing

The first optimization method targets the clear space and speeds up processing by only clearing a subset of the screen that makes up the entity. Start by decreasing the amount of the redraw region that overlaps transparent pixels around the region's respective entity. Obvious targets to use this technique include relatively small entities that fill a small area of the viewport.

The first target will be the ball and obstacle entities. The single entity clearing technique involves clearing the region the entity rendered to on the previous frame before rendering the entity to its new location. You introduce a clear step into the render of each entity and store a bounding box of the entity's image. Adding this step modifies the entity object to include the clear step, as shown in Listing 5.

Listing 5. Entity with single box clearing
var Entity = function() {
    /**
     Initialization and other methods
     **/
     
    /**
     * Render call to draw the entity
     *
     * @param {CanvasRenderingContext2D} context
     */
    this.render = function(context) {
        context.clearRect(
            this.prevX,
            this.prevY,
            this.width,
            this.height);
        context.drawImage(this.image, this.x, this.y);
        this.prevX = this.x;
        this.prevY = this.y;
    }
};

The updates to the render function introduce a clearRect call that occurs before the normal drawImage. For this step, the object needs to store the previous position. Figure 6 shows the steps the object takes with the previous position.

Figure 6. Clear Rect
Clear Rect

You can implement this render solution by creating a clear method for each entity that is called before the update step (though this article will not use the clear method). You can also introduce this clear strategy into the PanningEntity to add clearing onto the ground and clouds entities, as shown in Listing 6.

Listing 6. PanningEntity with single box clearing
var PanningEntity = function() {
    /**
     Initialization and other methods
     **/
     
    /**
     * Render call to draw the panned entity
     *
     * @param {CanvasRenderingContext2D} context
     */
    this.render = function(context) {
        context.clearRect(
            this.x,
            this.y,
            context.canvas.width,
            this.height);
        context.drawImage(
            this.image,
            this.x - this.width,
            this.y - this.height);
        context.drawImage(
            this.image,
            this.x,
            this.y);
        context.drawImage(
            this.image,
            this.x + this.width,
            this.y + this.height);
    }
};

Because the PanningEntity spans the entire viewport, you can use the canvas width as the clear rectangle size. Using this clear strategy provides the redraw regions you've defined for the clouds, hills, and ground entities.

To further optimize the cloud entity, you can separate the clouds into individual entities with their own redraw region. Doing so will cut down significantly on the amount of cleared screen space within the cloud redraw region. Figure 7 shows the new redraw regions.

Figure 7. Clouds with singular redraw regions
Clouds with singular redraw regions

The single entity clearing strategy produces a solution that solves most issues on a layered canvas game like the example, but it still can be optimized. To find an edge case to this render strategy, let's assume that the ball will collide with the triangle. If the two entities collide, the entities' redraw regions could overlap and create an undesirable render artifact. Another clearing optimization, better suited for entities that could collide, would also be useful for layering.

Dirty rectangle clearing

In the absence of a singular clearing strategy, a dirty rectangle clearing strategy can be a powerful substitute. You would use this strategy of clearing for situations with large amounts of entities with a redraw region, such as dense particle systems or a space game with asteroids.

Conceptually, the algorithm collects the redraw regions of all entities managed by the algorithm and clears the entire area in a single clear call. For added optimization, this clearing strategy also removes the repeated clear calls that each individual entity would produce, as shown in Listing 7.

Listing 7. DirtyRectManager
var DirtyRectManager = function() {
    // Set the left and top edge to the max possible
    // (the canvas width) amd right and bottom to least-most
    
    // Left and top will shrink as more entities are added
    this.left   = canvas.width;
    this.top    = canvas.height;
    
    // Right and bottom will grow as more entities are added
    this.right  = 0;
    this.bottom = 0;
    
    // Dirty check to avoid clearing if no entities were added
    this.isDirty = false;
    
    // Other Initialization Code
    
    /**
     * Other utility methods
     */
    
    /**
     * Adds the dirty rect parameters and marks the area as dirty
     * 
     * @param {number} x
     * @param {number} y
     * @param {number} width
     * @param {number} height
     */
    this.addDirtyRect = function(x, y, width, height) {
        // Calculate out the rectangle edges
        var left   = x;
        var right  = x + width;
        var top    = y;
        var bottom = y + height;
        
        // Min of left and entity left
        this.left   = left < this.left     ? left   : this.left;
        // Max of right and entity right
        this.right  = right > this.right   ? right  : this.right;
        // Min of top and entity top
        this.top    = top < this.top       ? top    : this.top;
        // Max of bottom and entity bottom
        this.bottom = bottom > this.bottom ? bottom : this.bottom;
        
        this.isDirty = true;
    };
    
    /**
     * Clears the rectangle area if the manager is dirty
     *
     * @param {CanvasRenderingContext2D} context
     */
    this.clearRect = function(context) {
        if (!this.isDirty) {
            return;
        }
        
        // Clear the calculated rectangle
        context.clearRect(
            this.left,
            this.top,
            this.right - this.left,
            this.bottom - this.top);
        
        // Reset base values
        this.left   = canvas.width;
        this.top    = canvas.height;
        this.right  = 0;
        this.bottom = 0;
        this.isDirty = false;
    }
};

Integrating the dirty rectangle algorithm into the render loop requires that the manager in Listing 7 is called before the render call is made. Adding the entities into the manager allows the manager to calculate the clear rectangle dimensions at clear time. While the manager will produce the desired optimization, depending on the game loop, the manager is capable of being optimized towards that game loop, as illustrated in Figure 8.

Figure 8. Redraw region for interactive layer
Redraw region for interactive layer
  • Frame 1 - Entities are colliding, almost overlapping.
  • Frame 2 - Entities redraw regions are overlapping.
  • Frame 3 - Redraw regions are overlapping and collected into one dirty rect.
  • Frame 4 - Dirty Rect is cleared.

Figure 8 shows the redraw region calculated by the algorithm for the entities in the interactive layer. Because the game includes interaction on this layer, the dirty rectangle strategy is sufficient to solve the interactive and overlapping redraw region issue.


Overwrite as a clear

For completely opaque entities that animate within a constant redraw region, you can overwrite as an optimization technique. Rendering an opaque bitmap to a region—a default composite operation—will place the pixels in that place regardless of the original rendering that was in the region. This optimization removes the need for a clear call before the render call since the render will cover the original region.

Overwrite speeds up the ground entity by re-rendering the image over the top of the previous render. The largest layer, the background, also speeds up in the same manner.

By reducing the redraw regions for each layer, you've effectively found optimization strategies for each of the layers and their contained entities.


Conclusion

Layering canvases is an optimization strategy that you can apply to any interactive real-time scene. Layering for optimization requires that you consider how the scene overlays itself by analyzing the scene's redraw regions. Scenes that have collections of redraw regions overlapped to define the layer are good candidates for rendering layered canvases. Whenever you have particle systems or a large set of physics objects colliding together, layering canvases could be a good optimization choice.


Download

DescriptionNameSize
Article source codeHTML5CanvasRenderingSource.zip3KB

Resources

Learn

Get products and technologies

  • OpenGL: Get the latest drivers.
  • jQuery: Download the popular JavaScript Library that simplifies HTML document traversing, event handling, animating, and Ajax interactions for rapid web development.
  • Modernizr: Get this open-source JavaScript library that helps you build the next generation of HTML5 and CSS3-powered Web sites.
  • Kibo: Download the popular library specifically written for speedy cross-browser keyboard event handling.
  • IBM product evaluation versions: Download or explore the online trials in the IBM SOA Sandbox and get your hands on application development tools and middleware products from DB2, Lotus, Rational, Tivoli, and WebSphere.

Discuss

  • developerWorks community: Connect with other developerWorks users while exploring the developer-driven blogs, forums, groups, and wikis.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Select information in your profile (name, country/region, and company) is displayed to the public and will accompany any content you post. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Web development on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development
ArticleID=842907
ArticleTitle=Optimize HTML5 canvas rendering with layering
publish-date=10302012