TakumiTakumi

Performance & Optimization

Best practices for building high-performance rendering pipelines with Takumi.

The renderer is fast. Most performance problems come from rebuilding it on every request, fetching the same image twice, or stacking filters that allocate full-viewport buffers. Avoid those three things and you'll be on the happy path.

Reuse the Renderer

The Renderer owns parsed fonts, the persistent image store, and the WASM/native module handle. Build it once per process; pass it into every render.

server.ts
import { Renderer } from "takumi-js/node";

const renderer = new Renderer({
  fonts: [
    /* ... */
  ],
});

export function GET() {
  return new ImageResponse(<OgImage />, { renderer, width: 1200, height: 630 });
}

ImageResponse manages an internal renderer if you don't pass one — fine for getting started, wasteful in production.

Cloudflare Workers: init outside the handler

On Workers the WASM module must be instantiated, and the renderer constructed, in module scope. Doing it inside fetch pays the init cost on every cold request.

index.tsx
import { ImageResponse } from "takumi-js/response";
import { initSync, Renderer } from "takumi-js/wasm";
import module from "takumi-js/wasm/takumi_wasm_bg.wasm";
import archivo from "path-to-font.ttf";

initSync(module); 
const renderer = new Renderer({ fonts: [archivo] }); 

export default {
  fetch(request: Request) {
    return new ImageResponse(<OgImage />, { renderer, width: 1200, height: 630 });
  },
};

Image preloading

Frequently used assets — logos, backgrounds, avatars — belong in the persistent image store. See Persistent image store for the full pattern and the Cloudflare memory caveat.

Filter cost

Every filter, backdrop-filter, and box-shadow with non-zero blur allocates a composition buffer the size of the affected node. Stacking multiple filters on a single node is cheaper than splitting them across nested nodes, because one buffer holds the whole chain.

// Cheaper — one composition layer
<div style={{ filter: "blur(8px) brightness(1.1) saturate(1.2)" }}>{content}</div>

// More expensive — three nested layers
<div style={{ filter: "blur(8px)" }}>
  <div style={{ filter: "brightness(1.1)" }}>
    <div style={{ filter: "saturate(1.2)" }}>{content}</div>
  </div>
</div>

Fonts

See Typography & Fonts. The short version: prefer TTF over WOFF2 if file size isn't the binding constraint — decompression isn't free, and you pay it every time the renderer initializes.

Parallel rendering

@takumi-rs/core (native) renders on a thread pool and scales with cores. @takumi-rs/wasm is single-threaded. If you need throughput on a multi-core host, use the native binding.

Measure cost

For ground truth, run the benchmark suite in takumi/benches/. The numbers there reflect the engine without framework overhead — a useful baseline when you're trying to figure out whether a regression is yours or ours.

Edit on GitHub

Last updated on

On this page