
A few months ago, I wrote about how to automate dynamic social images using Satori and Sharp. That pipeline gave me consistent, branded 1200×630 Open Graph (OG) cards for every blog post, tag page, and case study at build time without touching manual design tools.
It worked reliably. But as the number of articles, tag archives, and documentation pages grew past 150 routes, a clear bottleneck emerged: static build times kept creeping up.
Every full site rebuild was spending dozens of seconds spinning inside V8, computing flexbox layouts with Yoga WASM, and generating SVGs.
To fix this, I migrated the entire OG image rendering engine from Vercel’s Satori to Takumi (takumi-js)—a high-performance, Rust-native image engine.
Here is why I made the switch, how the two engines compare under the hood, how to execute the migration with zero layout changes, and the exact real-world benchmark numbers from my Astro build pipeline.
Why Migrate? The Hidden Cost of SSG Social Previews
When you generate social cards at static build time (SSG), each unique route requires:
- Building a virtual DOM tree (VNode object).
- Resolving custom fonts, character glyphs, and line wraps.
- Calculating CSS flexbox layouts.
- Serializing the layout tree into an SVG string.
- Rasterizing the SVG into a compressed WebP or JPEG via Sharp.
SSG Build Pipeline:
[Route Metadata]
│
▼
[Virtual DOM / JSX Spec]
│
▼
[Layout & Typography Engine] ─── (Satori Yoga WASM vs. Takumi Rust Core)
│
▼
[Rendered SVG Code]
│
▼
[Sharp Rasterizer]
│
▼
[Cached 1200x630 JPEG / WebP Asset]The Satori Bottleneck
Satori is an incredible tool that popularized JSX-to-SVG generation. However, it relies on a JavaScript orchestration layer combined with Yoga compiled to WebAssembly.
When generating over 150 images in a single build pass:
- V8 Garbage Collection Overhead: Constructing thousands of intermediate JS objects for deep JSX trees stresses Node’s memory heap.
- WASM Boundary Crossing: Passing layout constraints and text metrics back and forth across the JS-WASM boundary adds microsecond overhead that compounds over hundreds of runs.
- Single-Thread Execution: While Astro handles concurrency at the route level, heavy CPU work in JavaScript blocks the event loop from resolving file I/O swiftly.
I wanted something faster that would run natively without requiring a redesign of my existing JSX card templates.
What is Takumi?
Takumi (
takumi-js) is an open-source, ultra-fast image generation engine written in Rust. It provides a drop-in API compatible with Satori, rendering HTML/JSX-like node trees and CSS flexbox styling directly into SVGs and images with native Rust performance.GitHub: takumi-rs/takumi | Docs: takumi.kane.tw
Takumi is architected from the ground up for speed:
- Native Rust Core (
@takumi-rs/core): Ships precompiled N-API native binaries for Linux (x64, ARM64, musl, glibc), macOS (Apple Silicon & Intel), and Windows. - WASM Fallback (
@takumi-rs/wasm): If running in edge workers or environments where native binaries cannot load, it falls back seamlessly to WebAssembly. - Advanced CSS Support: In addition to flexbox, Takumi adds cleaner text wrapping, linear & radial gradients, drop shadows, and rich font shaping out of the box.
Satori vs. Takumi: Architecture Comparison
| Feature / Metric | Vercel Satori | Takumi (takumi-js) |
|---|---|---|
| Core Engine | TypeScript + Yoga (WASM) | Pure Rust (@takumi-rs/core) |
| Execution Model | JS runtime + WASM bridge | Native N-API C++ / Rust bindings |
| API Signature | satori(element, options) | renderSvg(element, options) |
| Node Input Schema | JSX / VNode ({ type, props }) | Identical JSX / VNode schema |
| CSS Capabilities | Standard flexbox subset | Flexbox + expanded gradients & shadows |
| Font Formats | TTF, OTF, WOFF | TTF, OTF, WOFF, WOFF2 |
| Memory Footprint | Moderate (V8 heap allocations) | Low (Rust native memory management) |
Because Takumi was designed with Satori API parity in mind, you do not have to redesign your card layouts.
Step-by-Step Migration Guide
Let’s walk through the exact code changes made in this repository to migrate from Satori to Takumi.
1. Install Dependencies
Add takumi-js to your project dependencies:
npm install takumi-jsThe package automatically resolves the appropriate native binary for your OS (e.g., @takumi-rs/core-linux-x64-gnu on Debian/Ubuntu or @takumi-rs/core-darwin-arm64 on Apple Silicon).
2. Add an Engine Switch to Centralized Config
To allow seamless benchmarking and provide a fallback safety net, I added an ogRender option in src/config.ts:
// src/config.ts
export const siteConfig = {
// ... other configuration options
/**
* OG Image Renderer ('satori' | 'takumi')
* Controlled via OG_RENDER environment variable
*/
ogRender: (getEnv('OG_RENDER', 'takumi').toLowerCase() === 'satori'
? 'satori'
: 'takumi') as 'satori' | 'takumi',
} as const;This lets you toggle between engines instantly using environment variables:
OG_RENDER=takumi npm run build(Default, ultra-fast)OG_RENDER=satori npm run build(Baseline comparison)
3. Update the Dynamic OG Endpoint
In Astro, dynamic OG images are generated via an endpoint file like src/pages/og/[...slug].jpg.ts.
Here is the diff showing how simple the migration is:
// src/pages/og/[...slug].jpg.ts
import satori from 'satori';
import { renderSvg as takumiRenderSvg } from 'takumi-js';
import { siteConfig } from '../../config';
// 1. Your existing VNode / JSX object definition remains 100% identical:
const ogElement: any = {
type: 'div',
props: {
style: {
display: 'flex',
height: '100%',
width: '100%',
flexDirection: 'column',
justifyContent: 'space-between',
backgroundColor: '#0c0f17',
padding: '60px 80px',
},
children: [
// Title, author avatar, tags, branding...
],
},
};
// 2. Options configuration:
const renderOptions: any = {
width: 1200,
height: 630,
fonts: [
{ name: 'Inter', data: fontRegularData, weight: 400, style: 'normal' },
{ name: 'Inter', data: fontBoldData, weight: 700, style: 'normal' },
],
embedFont: true,
loadAdditionalAsset: async (code: string, segment: string) => {
if (code === 'emoji') {
return await getEmojiDataUrl(segment);
}
return [];
},
};
// 3. Render SVG dynamically based on configured engine:
const svg =
siteConfig.ogRender === 'takumi'
? await takumiRenderSvg(ogElement, renderOptions)
: await satori(ogElement, renderOptions);
// 4. Rasterize to high-quality progressive JPEG via Sharp:
const jpegBuffer = await sharp(Buffer.from(svg))
.jpeg({ quality: 80, progressive: true })
.toBuffer();4. Add Comparative Build Scripts to package.json
Add dedicated benchmark scripts to your package.json:
{
"scripts": {
"dev": "astro dev",
"btakumi": "OG_RENDER=takumi npm run build",
"bsatori": "OG_RENDER=satori npm run build",
"build": "astro check && astro build"
}
}Now anyone on your team can verify parity or run isolated performance tests with a single command.
Real-World Performance Benchmarks
To obtain an accurate, unpolluted baseline, I cleared all local caches (rm -rf og-cache dist .astro) before each run and executed both builds against the exact same content set (160 total pages, including articles, tags, projects, and index routes).
Here are the measured numbers:
Build Time Comparison (Cold Cache, 160 Routes)
| Metric | Vercel Satori (npm run bsatori) | Takumi (npm run btakumi) | Improvement |
|---|---|---|---|
| Astro Page Generation Phase | 1m 07.00s (67.0s) | 42.24s | 37.0% faster ⚡ |
Total Wall Clock Time (time) | 1m 23.70s (83.7s) | 57.94s | 30.8% faster ⚡ |
| User CPU Time | 1m 24.45s | 1m 01.73s | 26.9% less CPU load |
| System Time | 5.94s | 4.49s | 24.4% less kernel overhead |
Astro Build Duration (Cold Cache):
Satori: [███████████████████████████████████] 67.0s
Takumi: [██████████████████████] 42.2s (-37% reduction)
Total Wall Clock Time:
Satori: [████████████████████████████████████████] 83.7s
Takumi: [████████████████████████████] 57.9s (-30.8% reduction)Why the Difference is Significant
In Static Site Generation (SSG), continuous integration (CI) pipelines on GitHub Actions or Vercel are billed by compute duration.
- Faster Feedback Loops: Saving 25–30 seconds on every build means pull request previews and production deployments finish in under a minute.
- Scalability: As the blog grows to 300+ articles, the 37% rendering speed differential prevents CI builds from timing out or hitting memory thresholds.
- Identical Visual Fidelity: The generated SVG and JPEG artifacts match the original layout pixel for pixel with zero visual regression.
Lessons Learned & Best Practices
- Keep Dual Renderers During Transition: Wrapping the renderer in a site configuration toggle made it effortless to benchmark both engines in CI without committing destructive changes.
- Cache Font Buffers in Module Scope: Regardless of whether you use Satori or Takumi, load your TTF/WOFF font buffers once at module initialization rather than reading from disk on every endpoint invocation.
- Invalidate Cache on Renderer Changes: Include the active renderer and version tag in your disk cache hash (
crypto.createHash('sha256').update(JSON.stringify({ ..., renderer: siteConfig.ogRender, v: '2.0' }))). This guarantees that switching engines immediately regenerates fresh assets without stale artifacts.
Conclusion
If your Astro, Next.js, or SvelteKit site generates dynamic social previews at build time, migrating from Satori to Takumi is one of the highest-ROI optimizations you can make.
You get:
- Drop-in compatibility: Zero layout redesign required.
- Rust-native speed: Over 30% faster total build times.
- Seamless developer experience: Easy configuration toggles and clean integration with Sharp.
Try sharing this post on Twitter, LinkedIn, or WhatsApp to see the newly generated Takumi dynamic card in action!





