canvaskit-wasm 0.39 build 2026-04-29

Bundler integration

canvaskit-wasm ships a JavaScript loader plus a canvaskit.wasm binary (~6 MB). The loader is straightforward; the WASM binary is where every bundler has its own quirks.

What the npm package gives you

node_modules/canvaskit-wasm/
  bin/
    canvaskit.js        ← the loader (UMD)
    canvaskit.wasm      ← the binary (must reach the browser)
    canvaskit.d.ts      ← TypeScript types
    full/               ← also-built variant with Skottie/Paragraph/etc.

Two things have to happen at runtime:

  1. The browser fetches canvaskit.js.
  2. The loader fetches canvaskit.wasm over the network.

Step 2 is what differs between bundlers. By default the loader looks for the .wasm file at a relative URL next to the loader. Most bundlers don't preserve that relationship.

The locateFile escape hatch

Every CanvasKit init signature lets you override the URL resolver:

import CanvasKitInit from 'canvaskit-wasm';
import wasmUrl from 'canvaskit-wasm/bin/canvaskit.wasm?url'; // bundler-specific

const CK = await CanvasKitInit({
  locateFile: (file) => {
    // file === 'canvaskit.wasm'
    return wasmUrl;
  },
});

If you set locateFile correctly, the rest of the integration just works. Everything below is variations on getting the right URL into that callback.

Vite

Use the ?url import suffix:

import CanvasKitInit from 'canvaskit-wasm';
import wasmUrl from 'canvaskit-wasm/bin/canvaskit.wasm?url';

export const ckit = CanvasKitInit({ locateFile: () => wasmUrl });

Vite copies the file into dist/ and rewrites the import to its hashed asset URL. Works in dev and in production builds.

If you're targeting import.meta.env.BASE_URL (deployed to a sub-path), ?url already handles that.

Webpack 5

Webpack 5 has built-in WASM/asset support, but you usually want to stay on the JS-loader path because the canvaskit .wasm is not a Webpack-friendly module — it's an Emscripten artifact loaded by its own runtime.

import CanvasKitInit from 'canvaskit-wasm';
// `new URL(...)` is the standard way Webpack rewrites asset URLs.
const wasmUrl = new URL(
  'canvaskit-wasm/bin/canvaskit.wasm',
  import.meta.url,
).toString();

export const ckit = CanvasKitInit({ locateFile: () => wasmUrl });

Webpack 5 emits the file into dist/ as a hashed asset and substitutes the URL at build time. Older configs may need asset/resource rules; the new URL() form is the recommended path going forward.

Next.js (App Router)

Next.js refuses to bundle the loader on the server. Wrap the import behind a 'use client' boundary or load it inside a useEffect:

'use client';
import { useEffect } from 'react';

export function CKitMount() {
  useEffect(() => {
    (async () => {
      const CanvasKitInit = (await import('canvaskit-wasm')).default;
      const CK = await CanvasKitInit({
        locateFile: (f) => `/canvaskit/${f}`,
      });
      // …
    })();
  }, []);
  return <canvas id="root" />;
}

For the WASM URL itself, copy the file into public/canvaskit/canvaskit.wasm once (e.g. with a postinstall script) and reference it as a static asset. Importing through the bundler from a 'use client' boundary works too, but the static-copy approach gives you immutable URLs you can long-cache.

esbuild / Bun

Both treat the WASM as a copyable asset:

// esbuild
{
  loader: { '.wasm': 'file' },
}
// Bun
import wasmUrl from 'canvaskit-wasm/bin/canvaskit.wasm' with { type: 'file' };

Then plug wasmUrl into locateFile as in the Vite example.

Plain <script> tag (no bundler)

Straightforward — the loader's default locateFile finds the .wasm next to it.

<script src="https://unpkg.com/canvaskit-wasm@0.39/bin/canvaskit.js"></script>
<script>
  CanvasKitInit({
    // optional — defaults to 'https://unpkg.com/canvaskit-wasm@0.39/bin/' + file
    locateFile: (f) => 'https://unpkg.com/canvaskit-wasm@0.39/bin/' + f,
  }).then((CK) => {
    window.CK = CK;
  });
</script>

In production, host the file yourself rather than hot-linking unpkg.

TypeScript

canvaskit-wasm ships its own .d.ts. You shouldn't need @types/* — the import already resolves to typed code:

import CanvasKitInit, { CanvasKit, Surface, Paint } from 'canvaskit-wasm';

When wrapping CanvasKit in your own helpers, type the singleton as CanvasKit and pass it down — don't keep re-importing the namespace from inside leaf modules.

Async init: do it once, share the promise

// ckit.ts
import CanvasKitInit, { CanvasKit } from 'canvaskit-wasm';
import wasmUrl from 'canvaskit-wasm/bin/canvaskit.wasm?url';

let pending: Promise<CanvasKit> | null = null;
export function getCK(): Promise<CanvasKit> {
  if (!pending) {
    pending = CanvasKitInit({ locateFile: () => wasmUrl });
  }
  return pending;
}

The 6 MB binary downloads and instantiates once per page. Don't re-init per component — multiple CanvasKit instances aren't a feature, they're a leak.

Bundle size

CanvasKit itself is the size driver, not the JS shim. To trim:

  • The default build already ships without Skottie/Paragraph/font-mgr text in the variant your blog uses (the 0.39 build referenced in this docs site). Upstream's full build is ~10 MB.
  • Tree-shaking does not apply — the WASM module is monolithic. If you don't need RuntimeEffect, you still pay for it.
  • HTTP compression matters: serve the .wasm with Content-Encoding: br (Brotli) or gzip. The compressed size is much smaller than the uncompressed disk size.

Troubleshooting

  • 404 for canvaskit.wasm — your locateFile doesn't return a URL the browser can fetch. Check the network tab; it'll show what URL was requested.
  • MIME type mismatch / .wasm served as text/html — your dev server isn't serving .wasm with application/wasm. Vite/Webpack dev servers do this automatically; static hosts may need a config change.
  • CompileError: WebAssembly.instantiateStreaming — usually wrapped MIME issues; the streaming compile fails over to bytes-fetch and you'll see a network warning. Same fix as above.
  • window undefined during SSR — your bundler is trying to evaluate the loader on the server. Wrap the import behind 'use client' (Next.js) or move the call into a client-only effect.

See also