TakumiTakumi

Cloudflare Workers

Edge-render images with the WASM renderer.

Workers are a special target. No filesystem, no Node APIs, a strict bundle-size budget, and every cold start pays to instantiate the WebAssembly module. Takumi handles this by shipping a WASM build through takumi-js/wasm — same API as the Node renderer, different binary.

index.tsx
wrangler.jsonc
package.json

Install

See Installation. On Workers you only need takumi-js — the WASM binding ships with it.

Bundle the WASM and any fonts as data modules

Wrangler needs to know that .wasm and font files are byte payloads, not source. Add a rules entry to wrangler.jsonc:

wrangler.jsonc
{
  "name": "my-og-worker",
  "main": "src/index.tsx",
  "compatibility_date": "2025-10-30",
  "rules": [
    {
      "type": "Data",
      "globs": ["**/*.ttf", "**/*.woff", "**/*.woff2"],
      "fallthrough": true
    }
  ]
}

Reuse the fetch cache across requests

A Map declared at module scope persists for the lifetime of the isolate. Pass it as resourcesOptions.cache and Takumi will short-circuit repeated remote fetches.

src/index.tsx
import { ImageResponse } from "takumi-js/response";

interface Env {}

const fetchCache = new Map();

export default {
  async fetch(request) {
    const { searchParams } = new URL(request.url);
    const name = searchParams.get("name") ?? "Wizard";

    return new ImageResponse(
      <div
        style={{
          width: "100%",
          height: "100%",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          fontSize: 80,
          fontWeight: 700,
          background: "#F48120",
          color: "#fff",
        }}
      >
        Hello, {name}
      </div>,
      {
        width: 1200,
        height: 630,
        resourcesOptions: { cache: fetchCache },
      },
    );
  },
} satisfies ExportedHandler<Env>;

Workers have a 128 MB memory ceiling and pay for every cold start. Keep the persistent cache bounded (cap entries or use an LRU), preload fonts and frequently used images once, and avoid rendering at sizes larger than the consumer actually needs.

Asset preloading

Cold starts on Workers are where remote fetch hurts most. If your design pulls in the same logo, font, or background image on every render, load them once with putPersistentImage outside the fetch handler:

src/index.tsx
import { Renderer } from "takumi-js/wasm";

interface Env {}

const renderer = new Renderer();
const ready = Promise.all([
  renderer.loadFonts([
    {
      name: "Geist",
      data: () => fetch("https://example.com/Geist.woff2").then((r) => r.arrayBuffer()),
    },
  ]),
  renderer.putPersistentImage({
    src: "https://example.com/logo.png",
    data: () => fetch("https://example.com/logo.png").then((r) => r.arrayBuffer()),
  }),
]);

export default {
  async fetch() {
    await ready;
    // ...renderer.render(...) and wrap in a Response
  },
} satisfies ExportedHandler<Env>;

The first request still pays for the load, but every subsequent request inside that isolate skips it entirely.

Edit on GitHub

Last updated on

On this page