Performance & Optimization
Reuse Takumi renderers, tune image and glyph caches, and measure cold and repeated renders.
Reuse the renderer
import { } from "takumi-js";
const = await (< ="p-8 text-4xl">Hello Takumi</>, {
: 1200,
: 630,
});render and ImageResponse reuse one managed renderer, so they need no cache setup. If you create a Renderer yourself, create it outside the request handler. Its registered fonts and decoded resources then survive between renders.
Know what is cached
| Work | Lifetime | Configuration |
|---|---|---|
| Decoded images, SVG rasters, parsed stylesheets | Renderer | cacheMaxBytes, 64 MiB by default |
| Glyph outlines and rasterized masks | Loaded backend module | setGlyphCacheMaxBytes, 8 MiB by default |
| Parsed Tailwind classes | Loaded backend module | Automatic, bounded internally |
| Resolved Tailwind declarations | Render | Automatic, separated by viewport width, font size, and pixel ratio |
| Google Fonts CSS | Fetch implementation | Automatic, bounded internally |
| Downloaded image bytes | Caller-provided cache | Optional images.fetchCache |
Cache budgets count retained entries, not total process memory. Fonts, active renders, output buffers, and allocator overhead use memory too. See image caching and Google Fonts for resource-specific behavior.
Measure cold and repeated renders
Measure the first render separately from repeated renders. Font downloads and decoding slow the first one. Layout, painting, and encoding still run on a warm renderer.
Use representative text, images, output formats, and concurrent requests. Compare latency and memory before you change a budget. A larger cache helps only when later renders reuse what it keeps.
Resource cache
import { } from "takumi-js/node";
const = new ({ : 64 * 1024 * 1024 }); Raise the budget when reused images or stylesheets get evicted. For an image used once, cache: "none" keeps it from pushing out reusable entries. This budget does not cover images.fetchCache, so bound that cache yourself.
Glyph cache
import { } from "takumi-js";
(64 * 1024 * 1024); Call it before the first render. A later call does not resize a cache already in use. Every renderer in the same backend module shares this budget. Large glyph sets, such as CJK text, can need more space. Benchmark your content first.
Keep memory flat on Linux servers
Each render allocates a few megabytes for its canvas and encoder buffers. On Linux, glibc keeps freed memory in its arenas, so a long-running process holds more memory than the renderer retains.
GLIBC_TUNABLES=glibc.malloc.mmap_threshold=262144 node server.jsglibc raises its mmap threshold after a large block is freed, so later canvases land in the arenas. A fixed threshold keeps buffers over 256 KiB in their own mappings, and glibc unmaps each one when it is freed.
| Setting | RSS over 1,275 renders | Render time |
|---|---|---|
| Default | 189–235 MB | Baseline |
GLIBC_TUNABLES=glibc.malloc.mmap_threshold=262144 | 114–130 MB | 5–7% slower |
MALLOC_ARENA_MAX=2 | 165–211 MB | 7% slower with 4 renders at once |
These figures come from one test setup, described in PR #1862: the node:24-bookworm-slim image on Linux arm64, rendering 65 OG templates four at a time. Both settings apply to the whole process, so measure your own server first. Alpine and other musl images use a different allocator.
Keep network requests out of repeated work
Bundle fonts for a predictable startup without a font service. WOFF2 files are smaller but need decompression. TTF files skip that step. A reused renderer decodes a registered file once. See Fonts in CI and offline renders.
For repeated remote images, use a bounded fetch cache. The renderer's decode cache does not replace a download cache.
Reduce painting work
Each filtered node needs an offscreen layer. Put filters on one node when they should apply to the same composed content. Moving filters from children to a parent can change the output, so compare the result.
Render large canvases
A canvas holds at most 64 megapixels: 8192 × 8192, or 4096 × 16384.
import { } from "takumi-js";
const = await (< ="p-8 text-4xl">Hello Takumi</>, {
: 3840,
: 2160,
});A larger viewport fails with Invalid viewport dimensions: a canvas holds 1 to 64 megapixels. Each pixel takes 4 bytes, so a full 64-megapixel render holds 256 MiB before encoding.
Painting cost follows the area a node covers, not the canvas size. A backdrop-filter blurs only the pixels under its node. A clip mask costs only the clipped box.
Last updated on