I’ve been working on an HTML5 game and trying to make it run smoothly on older phones and low-end tablets. A few things that helped so far: reduce draw calls, use sprite sheets instead of many small images, and lower the canvas resolution when possible.

Culling off-screen objects and simplifying effects made a big difference too. I also use requestAnimationFrame for smoother timing and object pooling to reduce garbage collection.

Still, performance varies a lot between browsers and devices. What optimization tricks have worked best for you when targeting weaker hardware?

Recommended Answers

All 4 Replies

Fewer DOM nodes means less to parse. Don't overly nest DOM nodes as well. Avoid inefficient CSS selectors. Use simple selectors as much as possible like .class or #id and avoid things like the * wildcard. Eliminate as much unused CSS as possible. Keep in mind that any changes to the DOM will require re-parsing CSS. Since it's a game, use image sprites (multiple tiny images combined together into a single image). A lot of the selector stuff applies to javascript as well. Don't attach too many event handlers ... only when essential!

Good luck!!

Do you use group calls?

Much 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's much better to create a single path with all the lines and draw it in a single call. In other words, rather than drawing individual lines:

Slow-working solution

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();
}

A faster-running solution

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 at once rather than drawing segments separately.

But keep in mind that there's an important exception to this rule with canvas: if the primitives of the object being drawn have small bounding boxes, it may be more efficient to draw them separately.

Use multi-layer canvases for complex scenes.

Rendering large images is slow and should be avoided. In addition to using the off-screen buffer (pre-render section), we can use layered canvases. By using transparency in the top layer, we can rely on the GPU to apply the alpha channel during rendering. You can use this with two absolutely positioned canvases, one on top of the 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>

Note: On mobile devices, the situation is completely the opposite - multi-layer canvases slow down significantly, forcing you to use 1 + pre-render the scene into the background (and a backing div the size of the canvas and located underneath it, since, for some inexplicable reason, the canvas background also slows down significantly).

Avoid Non-Integer Coordinates

HTML5 canvas supports sub-pixel rendering, and there's no way to disable it. If you draw with non-integer coordinates, it automatically uses anti-aliasing to smooth out lines. Here's a visualization of sub-pixel performance from Seb Lee-Delisle's article:
bunny

If an anti-aliased sprite isn't what you need, it's much faster to convert your coordinates using Math.floor or Math.round.

To convert non-integer coordinates to integers, there are several clever techniques, most of which rely on adding half the number and using bitwise operations to remove the mantissa.

// 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;

The relatively new requestAnimationFrame API is recommended for implementing interactive applications in the browser. Instead of telling the browser to draw at a specific rate, you politely ask it to trigger a draw and let you know when it's finished. As a nice bonus, if the page is inactive, the browser is smart enough to avoid drawing.

The requestAnimationFrame call targets 60 FPS, but doesn't guarantee it, so you should keep track of how much time has passed since the last draw. 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();

Note that requestAnimationFrame applies to both canvas and other techniques such as WebGL.

RequestAnimationFrame is tested on Chrome, Safari, and Firefox, so you should use it with caution.

Note: Most solutions are provided by my colleagues who work with web-based applications.

You’ve already nailed the big ones — sprite sheets, culling, object pooling, and requestAnimationFrame() make a huge difference.

A few more tips that help on low-end devices:

Use WebGL or OffscreenCanvas for GPU-accelerated rendering.

Compress textures (WebP/AVIF) and load smaller versions based on device pixel ratio.

Reduce JS overhead — avoid frequent object creation or DOM access inside the loop.

Lazy-load assets to cut initial memory load.

Cap frame rate to 30fps on weaker phones — smoother overall and less heat.

And definitely profile on real hardware, not emulators — that’s where the real bottlenecks show.

On older devices, the biggest wins for me came from drastically reducing overdraw, limiting physics calculations, and avoiding expensive operations inside the main loop. Pre-computing values, using lightweight collision checks (AABB instead of per-pixel), and keeping all assets as compressed and small as possible also helped.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.