Takumi

Takumi vs. Satori

Compare Takumi with Satori and next/og, including CSS support, output formats, and migration steps.

satori pioneered OG images without a headless browser and powers next/og. It turns JSX into an SVG string, so a bitmap takes a pipeline: yoga computes layout in WebAssembly, satori emits SVG, and resvg or sharp rasterizes it. Takumi is one Rust engine that does the whole job: JSX in, encoded image out.

JSX satori SVG string resvg / sharp PNG JSX Takumi PNG / WebP / SVG / GIF

Start migration by replacing the import. ImageResponse provides a next/og-compatible API, but the renderers have different layout and font behavior. Keep explicit Flexbox styles and compare your templates before deploying.

Compare rendered output across providers at image-bench.kane.tw.

Features

Templates and styling

Featuresatori / next/ogTakumi
Template inputJSXJSX, HTML strings, node trees
StylingInline styles, tw prop+ <style> tags, stylesheets
Selectors, ::before / ::after
@media, @keyframes, @supports
calc()

Selector support covers class, id, descendant, :is(), ::before, and ::after. See Styling.

Layout and text

Featuresatori / next/ogTakumi
Layout modesFlexbox onlyFlexbox, CSS Grid, block, inline, float
z-index❌ paint order only
backdrop-filter, blend modes
RTL text

Output

Featuresatori / next/ogTakumi
Raster (PNG, JPEG, WebP, ICO)❌ needs resvg or sharp✅ direct
Vector SVGrenderSvg
Animated (GIF, APNG, WebP)
Raw pixel frames (video pipelines)

Fonts, emoji, runtime

Featuresatori / next/ogTakumi
Default fontGeist 400 (next/og only)✅ Geist 300 to 800
WOFF2
Emoji providersnext/og only✅ built in
RuntimeNode, Edge, browser+ Cloudflare Workers, Rust crate
ImageResponse API✅ Native✅ Compatible

Layout defaults

Satori defaults to display: flex and requires an explicit supported display value on a <div> with multiple children. Takumi defaults a bare <div> to display: block. Keep display: flex on containers that need Flexbox layout.

Migrate from satori

renderSvg replaces satori(). It accepts JSX and returns an SVG string. The built-in Latin font makes a fonts entry optional.

Replace satori with renderSvg
import satori from "satori"; 
import { renderSvg } from "takumi-js"; 

const svg = await satori(<div style={{ display: "flex" }}>Hello</div>, { 
const svg = await renderSvg(<div style={{ display: "flex" }}>Hello</div>, { 
  width: 1200,
  height: 630,
  fonts: [{ name: "Inter", data: interBytes, weight: 400, style: "normal" }], 
});

Skip the SVG step entirely when the goal is a bitmap. satori pipelines pair with resvg or sharp to rasterize; render returns encoded bytes in one call.

Render PNG directly
import {  } from "takumi-js";

const  = await (< ="w-full h-full flex items-center justify-center">Hello</>, {
  : 1200,
  : 630,
});

Migrate from next/og

ImageResponse matches the next/og API, including the satori-compatible emoji option. Swap the import.

Replace the next/og import
import { ImageResponse } from "next/og"; 
import { ImageResponse } from "takumi-js/response"; 

export function GET() {
  return new ImageResponse(<div tw="w-full h-full flex items-center justify-center">Hello</div>, {
    width: 1200,
    height: 630,
  });
}

The Next.js integration guide covers opengraph-image.tsx and route handlers.

Fonts

satori throws without at least one fonts entry. Takumi ships a built-in last-resort font (Geist, Latin only, weights 300 to 800), so Latin text renders with zero setup. For anything else, googleFonts fetches families in one call, and fonts also accepts raw bytes, descriptors, or bare URL strings.

Load a CJK font
import {  } from "takumi-js";
import {  } from "takumi-js/helpers";

const  = await (
  < ={{ : "Noto Sans TC" }} ="w-full h-full flex text-7xl">
    你好,匠
  </>,
  {
    : 1200,
    : 630,
    : ([{ : "Noto Sans TC", : 700 }]),
  },
);

See Fonts for script routing and preloading with Renderer.

Emoji

next/og resolves emoji through satori's loadAdditionalAsset callback. Takumi's ImageResponse accepts the same emoji option: twemoji, blobmoji, noto, openmoji, fluent, and fluentFlat. See Load images.

Animation

satori renders one static frame. Takumi threads a time axis through the pipeline: CSS @keyframes, the animation shorthand, and Tailwind animation utilities resolve at render time, and renderAnimation samples the same tree across timestamps into GIF, APNG, or animated WebP.

Performance and memory

Satori generates an SVG string on the calling thread. For raster output, pass that string to a separate rasterizer. Takumi's native binding moves the whole pipeline off the event loop:

  • Renders run on worker threads. render executes as an async task, so the event loop stays free, and concurrent renders read the font store without locking.
  • Animation frames render in parallel across a thread pool and stream into the encoder, so encoded animations do not need to retain every raw frame.
  • Images decode at draw size. A large photo drawn in a small box retains pixels at the draw size, not the source size, and the decode cache reuses them across renders.
  • Fonts download by coverage. googleFonts splits each family into unicode-range subsets, and a render fetches only the subsets its codepoints hit.

See Performance and optimization for tuning.

Tradeoffs

Measured from a clean npm install (macOS arm64) and gzip of the published artifacts, takumi-js 2.2.0 against satori 0.26 and @vercel/og 0.11:

PackageInstall size (node_modules)Edge bundle (gzip)
satori + @resvg/resvg-js14 MB
@vercel/og35 MB (16 MB is optional sharp)~0.7 MB (index.edge.js + resvg.wasm)
takumi-js8.6 MB1.5 MB (takumi_wasm_bg.wasm)

In these versions, the Takumi WebAssembly binary measured 3.7 MB raw and 1.5 MB gzipped, about twice the measured @vercel/og edge stack. Include download and compilation costs when measuring cold starts. On Node.js, Takumi uses a native binary. Check current package sizes against your deployment target before choosing a backend.

Used in production

Dcard, TanStack, Fumadocs, and Luma render share images with Takumi, and Nuxt OG Image ships it as a built-in renderer. More in the showcase.

Last updated on

On this page