Boost Canvas/WebGL Animation Performance with WebAssembly: Design & Benchmarks

This article examines how WebAssembly can accelerate canvas and WebGL animation engines by redesigning data structures, separating JS and WASM animation objects, optimizing the update cycle, handling mixed animations, and presenting benchmark results for thousands of animated nodes.

Alipay Experience Technology
Alipay Experience Technology
Alipay Experience Technology
Boost Canvas/WebGL Animation Performance with WebAssembly: Design & Benchmarks

Introduction

WebAssembly (WASM) has become stable and mature, and its performance advantage is often used to solve problems. This article focuses on performance optimization in the graphics animation domain: how to use WASM to improve the performance of canvas/WebGL animation engine designs.

Engine Features

The engine should provide complete functionality similar to CSS Animation / Web Animation API, not just a simple frame counter.

Support for pause, resume, speed change, jump, reverse, cancel, complete, carousel, etc.

Simple API that gives a DOM‑like object programmable animation capabilities with parameters.

Optional support for CSS units (rem, vw), stay mode, and events.

These three points increase in completeness; the first is basic, the second is common, the third is more demanding.

Characteristics of WASM

WASM excels at compute‑intensive tasks and can be a few times faster than pure JavaScript, which is valuable for rendering or animation. However, frequent calls and data exchange must be minimized; a frame should exchange as little data as possible.

In practice, the entire per‑frame calculation is performed inside WASM, while initialization actions are rare and can be ignored.

Initial experiments with AssemblyScript showed limited gains, so Rust was chosen ( https://github.com/karasjs/wasm ).

Limitations

The animation engine is tightly coupled with the rendering engine, and many animation types exist. The most common are transform and opacity , followed by visibility, color, border, z‑index, font, etc. Implementing every type in WASM would incur high development and maintenance costs.

Therefore the article uses transform and opacity as the primary example.

Data Structure

Each visual node is represented by a JavaScript object and a corresponding WASM object (WASM Node). Animations can be pure JS, pure WASM, or a mixed combination.

// Pure JS animation (x is like CSS left)
node.animate([
  { x: 0 },
  { x: 100 }
], {
  duration: 1000,
});
// Pure WASM animation (rotateZ is plane rotation)
node.animate([
  { rotateZ: 0 },
  { rotateZ: 90 }
], {
  duration: 1000,
});
// Mixed animation example
node.animate([
  { x: 0, rotateZ: 0 },
  { x: 100, rotateZ: 90 }
], {
  duration: 1000,
});

The design aims to keep the existing JS animation engine largely unchanged while allowing a WASM animation object to be created for transform‑related data.

Process Clock

All data updates must be synchronous, but rendering is asynchronous. After synchronous updates, requestAnimationFrame triggers a single repaint for the next frame.

// Example of batch updates
for(let i = 0; i < 1000; i++) {
  nodes[i].style.translateX = 100;
}
requestAnimationFrame(() => {
  draw(); // only one render per frame
});

The animation engine follows a two‑phase cycle: before (data updates) and after (event callbacks). The after phase should run only after all data updates are finished.

Changes with WASM

The root node holds references to all child nodes and their animations. During the before phase, the WASM root traverses all WASM animations, then the JS engine traverses JS animations, communicating with WASM only once.

During the after phase, the WASM root computes state data and writes it to a SharedBuffer. The JS side reads this buffer to update its animation objects.

// Pseudo‑code to read WASM data via SharedBuffer
let n = wasmRoot.after(); // number of WASM animations
let states = new Uint8Array(wasm.instance.memory.buffer, wasmRoot.states_ptr(), n);

Complex Scenarios

When mixed (green) animations exist, some calculations may be duplicated in both JS and WASM. The performance impact is usually small and acceptable.

If the order of animations (A, B, C, D) is irregular, the JS side must use an offset counter to correctly map data from the SharedBuffer to the corresponding animation objects.

// Pseudo‑code handling offset
let n = wasmRoot.after();
let states = new Uint8Array(wasm.instance.memory.buffer, wasmRoot.states_ptr(), n);
let offset = 0;
for(let i = 0; i < len; i++) {
  let ja = jsAnimations[i];
  if(ja.wasmAnimation) {
    let state = states[i - offset];
    // process state
  } else {
    offset++;
  }
  ja.after();
}

Refresh & Repaint

After the before step, a repaint occurs. Matrix calculations are performed in WASM; the matrix data is retrieved via another SharedBuffer.

let matrix = new Float64Array(wasm.instance.memory.buffer, wasmRoot.matrix_ptr(), len * 16);

Matrix multiplication can be further accelerated with SIMD instructions in newer browsers (e.g., Chrome 91).

Precision

All numeric operations in WASM should use f64 to match JavaScript precision. Using f32 can cause subtle errors. Additionally, Rust’s wasm‑bindgen aligns data differently for f64 (8‑byte) versus f32 (4‑byte), which must be considered.

Benchmark

Performance tests with 10,000 animated nodes (5,000 for Canvas mode) compare FPS across different implementations (Karas‑WASM, Karas‑Canvas, Pixi‑Canvas, Karas‑WebGL, Karas‑WASM‑WebGL, Pixi‑WebGL). Links to the demo pages are provided in the original article.

Other Considerations

Future work includes exploring SIMD for large‑scale animation execution, parallel matrix calculations, and further integration of vertex computations into WASM.

frontend developmentRustCanvasWebAssemblyWebGLAnimation Engine
Alipay Experience Technology
Written by

Alipay Experience Technology

Exploring ultimate user experience and best engineering practices

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.