Animate’s HTML5 Canvas Runtime: Display List, Repaint Cost, and Caching
Animate’s HTML5 Canvas Runtime: Display List, Repaint Cost, and Caching
Search
Ask the AI

Animate’s HTML5 Canvas Runtime: Display List, Repaint Cost, and Caching

The same animation plays smoothly on Ctrl+Enter inside Animate and then drops frames once exported to HTML5 Canvas and loaded in a browser. The artwork is unchanged and so is the frame rate. What differs is the rendering model: Animate’s preview draws through a vector renderer, while the exported runtime is CreateJS repainting the entire canvas every frame. Understanding that layer is what tells you which optimisation matters, and why “convert the complex artwork to bitmap” is the single most effective move available in this pipeline.

The export is an executable display tree

When an HTML5 Canvas document is published, the timeline stops being a data table and becomes JavaScript. Each library symbol turns into a constructor inheriting from createjs.MovieClip, and the keyframes, tweens and instance placements on that symbol’s timeline become a series of calls against its timeline. The stage hierarchy becomes a display tree:

Stage
 └─ Container      (a symbol instance)
     ├─ Shape      (vector artwork)
     ├─ Bitmap     (image / sprite sheet region)
     ├─ Text
     └─ MovieClip  (nested symbol with its own timeline)

One consequence connects directly to the timeline and symbol model. A Graphic symbol’s frame can be computed from the parent frame, so it is generally flattened into the parent timeline at export. A Movie Clip owns an independent playhead, so it survives as a real MovieClip object at runtime. That is why projects built heavily on Movie Clips end up with substantially more live objects after export.

Every frame repaints the whole canvas

Canvas 2D is an immediate-mode API: it retains no notion of “the objects in the scene”, only drawing commands. On each stage.update(), CreateJS walks the display tree and draws every object again.

For a Bitmap, that is one drawImage, and its cost tracks pixel area. For a Shape, it means rasterising the vector path afresh: parsing the path, filling, stroking, antialiasing. The more complex the path, the higher the per-frame cost — and that cost is paid every frame, even when the artwork has not changed at all.

Which explains a counter-intuitive observation: a complex vector background sitting perfectly still can cost more than a bitmap character running across it. It does not move, but it is still re-rasterised on every frame.

Caching converts a per-frame cost into a one-time cost

The remedy is caching. Calling cache() renders a display object and its subtree into an offscreen canvas; from then on each frame only needs to drawImage that offscreen bitmap.

shape.cache(x, y, width, height, scale);
// after the internal content changes, refresh explicitly:
shape.updateCache();

Three parameter traps decide whether caching helps or hurts:

  • The rectangle must cover all visible content. The cache region is a rectangle in the object’s own coordinates, and anything outside is clipped. Stroke width and filter spread must be included, or edges come out sliced flat.
  • scale fixes the resolution ceiling. A cache is a bitmap; cache at 1× then scale to 2× and it is blurry. If the object will be enlarged, or the page runs on a high-DPI display, pass a matching scale when caching.
  • Changed content requires updateCache(). This is the most common failure by far: cache a symbol whose interior is still animating, and it freezes on the page at the instant it was cached. A cache is a snapshot.

The resulting rule: cache objects that are internally static but moving as a whole — complex still backgrounds, intricate icons that only translate and rotate, static symbols carrying filters. Conversely, do not cache a symbol whose interior changes every frame, because calling updateCache() per frame costs more than drawing directly, adding an offscreen render plus a copy.

Filters do not work without caching

CreateJS filters operate on bitmap data, so only cached objects display filter effects. Assign filters without calling cache() and nothing changes on screen — the filter has not failed, it simply has no pixels to process.

obj.filters = [new createjs.BlurFilter(8, 8, 1)];
obj.cache(0, 0, w, h);      // without this line the filter is invisible

Blur-type filters also spread pixels outward, so the cache rectangle must be enlarged accordingly, otherwise the blurred edge is clipped into a hard line.

There are two frame rates and they can disagree

The Animate document carries a frame rate setting, and the CreateJS Ticker carries another. The published template normally initialises the Ticker from the document rate, but once the template has been hand-edited, or other code on the page touches the Ticker, the two can diverge — and the animation runs uniformly fast or slow.

createjs.Ticker.timingMode = createjs.Ticker.RAF;
createjs.Ticker.framerate = 24;

In RAF mode the tick follows the browser’s refresh, and the real interval is not guaranteed to equal 1000 / framerate. This matches the conclusion in keyframe interpolation: any logic advancing by frame count drifts from real time as soon as frames drop. Timeline animation advancing by frames is mostly acceptable — slow is simply slow — but any code you write yourself for cursor following, physics or timing must use real elapsed time.

Measure instead of guessing

Do not optimise on instinct. The browser performance panel answers the question directly:

  • Record a profile and read the main thread. Time concentrated in Scripting points at script or timeline logic; time in Rendering and Painting points at rasterisation, which is when caching and bitmaps become the right answer.
  • Bisect by hiding layers. Set a suspect complex layer to visible = false and measure again. A clear drop in frame time identifies the culprit.
  • Compare before and after caching. Add cache() to one object and measure again to confirm frame time actually fell. Caching is not unconditionally faster and is slower for simple shapes.
  • Check canvas size and DPI. On a high-DPI display the canvas backing store can be two to three times the CSS size, multiplying fill cost. This is easy to overlook and applies globally.

When to abandon vectors entirely

If vector complexity cannot be reduced far enough, there is a fallback: export a sprite sheet and play back bitmap frames. The costs are larger files, higher memory use, blurring when scaled up, and no runtime recolouring. The benefit is that each frame reduces to drawImage, performance becomes predictable, and it decouples entirely from vector complexity.

Mixed approaches are common in practice: the character body runs as a bitmap sequence to hold the frame rate, while UI and anything needing runtime colour changes stays vector. The deciding question is whether an element will be scaled or needs properties changed at runtime — when neither applies, going bitmap is nearly always worth it.

This article covers runtime behaviour after export; for how symbol type governs timeline evaluation while authoring, see the Animate timeline and symbol model. The full reading order for the series is on the 2D Animation Principles page.

Leave a Reply

Scroll down