canvaskit-wasm 0.39 build 2026-04-29

Memory management

CanvasKit is C++ compiled to WebAssembly. Its objects live in WASM linear memory, not the JavaScript heap. JS garbage collection cannot reach them. Every CanvasKit object you create must be deleted explicitly — and forgetting to do so leaks memory until the page reloads.

This sounds like a chore. In practice it boils down to four rules.

1. If it has delete(), you delete it

Anything you got from new CK.X(), any CK.Make* factory, or any something.copy() returns an object with a delete() method. You own it.

const paint = new CK.Paint();
const path  = new CK.Path();

// ...use them...

path.delete();
paint.delete();

The error pattern that bites everyone: allocating inside an animation loop and forgetting to free.

loop(() => {
  // BAD — leaks one Paint per frame, 60 per second.
  const paint = new CK.Paint();
  paint.setColor(CK.Color(255, 0, 0, 1));
  canvas.drawCircle(100, 100, 50, paint);
  surface.flush();
});

Allocate once outside the loop, reuse, mutate as needed:

const paint = new CK.Paint();
paint.setColor(CK.Color(255, 0, 0, 1));

loop(() => {
  canvas.drawCircle(100, 100, 50, paint);
  surface.flush();
});

2. Things you pass into draw calls aren't retained

canvas.drawPath(path, paint) reads from path and paint synchronously, copies what it needs into the surface's command buffer, and returns. After the call you can mutate the path, mutate the paint, or even delete() them — Skia doesn't keep references.

That's why the same Paint works fine across many draws of different shapes, why mutating paint.setColor between draws gives you different colors, and why you can build a transient Path for one draw and immediately reset it for the next.

3. Filters and shaders set on a paint are referenced

paint.setImageFilter(filter), setColorFilter, setMaskFilter, setPathEffect, setShader all read from the filter object on every subsequent draw with that paint. If you delete the filter while the paint still references it, the next draw crashes.

Two safe patterns:

  • Outlive the paint. Create the filter once, set it on the paint, delete it only after you delete the paint (or after paint.setImageFilter(null)).
  • Replace before delete. paint.setImageFilter(newFilter); oldFilter.delete(). The paint now reads newFilter; oldFilter is no longer referenced and is safe to free.

The same applies to factories that take an input?: ImageFilter | null — the resulting filter holds a reference to the input. Don't delete the input while it's still inside a chain.

4. Surface and Canvas are special

Surface you create once per <canvas> and keep alive for the demo's lifetime. Don't allocate per-frame. Delete only when you're done with the whole demo (e.g. on page unload).

Canvas you don't delete at all — it's not a top-level WASM object you own. You get it via surface.getCanvas(), and its lifetime is tied to the parent surface. When the surface goes away, the canvas does too.

Things that are not WASM objects

  • CK.Color, CK.Color4f, CK.LTRBRect, CK.XYWHRect, CK.RRectXY, CK.LTRBiRect return plain Float32Arrays. JS-managed; no .delete().
  • Enum values (CK.BlendMode.Multiply, CK.PathOp.Union, etc.) are numbers/objects sitting on the CK namespace. No allocation.
  • CK itself is the singleton you got from CanvasKitInit(...). Don't delete.
  • The runtime helpers on this site (canvas.drawAnchors, canvas.drawTangents, loadImage) are JS-only — no WASM allocations of their own (though loadImage produces an Image you'd otherwise own, which is why the doc runtime caches it on window and never deletes it).

How edit-mode helps you

The doc-page bundle wraps the user's CK instance with a tracking proxy in edit mode. Anything you new or Make* is registered. When the demo re-runs (because you typed in the editor or flipped a control), the runtime calls delete() on every tracked resource before evaluating the new code.

This does not absolve you from writing real-world correct code. Outside this docs site there's no proxy. The patterns the examples teach (allocate outside the loop, delete what you own, mind references on filters) are what real production code needs.

Quick checklist

  • Did I new or Make it? → I delete it.
  • Did I get it from *.copy()? → I delete it.
  • Am I inside a loop? → No allocation. Reuse the outside instance.
  • Did I set a filter/shader on a paint? → Don't delete the filter while the paint is alive (or call paint.setX(null) first).
  • Surface, Canvas, Float32Array rects/colors, enums, CK itself? → Leave them alone.