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 readsnewFilter;oldFilteris 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.LTRBiRectreturn plainFloat32Arrays. JS-managed; no.delete().- Enum values (
CK.BlendMode.Multiply,CK.PathOp.Union, etc.) are numbers/objects sitting on theCKnamespace. No allocation. CKitself is the singleton you got fromCanvasKitInit(...). Don't delete.- The runtime helpers on this site (
canvas.drawAnchors,canvas.drawTangents,loadImage) are JS-only — no WASM allocations of their own (thoughloadImageproduces anImageyou'd otherwise own, which is why the doc runtime caches it onwindowand 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
neworMakeit? → 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,
Float32Arrayrects/colors, enums,CKitself? → Leave them alone.