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:
- The browser fetches
canvaskit.js. - The loader fetches
canvaskit.wasmover 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.39build 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
.wasmwithContent-Encoding: br(Brotli) orgzip. The compressed size is much smaller than the uncompressed disk size.
Troubleshooting
- 404 for
canvaskit.wasm— yourlocateFiledoesn't return a URL the browser can fetch. Check the network tab; it'll show what URL was requested. - MIME type mismatch /
.wasmserved astext/html— your dev server isn't serving.wasmwithapplication/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.windowundefined 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
- Memory management — for what to do once the module is loaded.
Surface— the first thing you'll create after init.