Zooky — OG Image APIBlog ← back to zooky.net

Generating OG Images Without a Headless Browser: Satori + resvg vs. Puppeteer

2026-07-12 · Zooky — OG Image API

When developers need dynamic Open Graph images, the first instinct is almost always the same: spin up Puppeteer, render an HTML template, screenshot it, serve the PNG. It works. It also means you're running a complete web browser to draw a title on a colored rectangle.

There's a better pipeline — satori + resvg — and it's what powers Zooky in production. Here's how it works, what it actually costs you compared to headless Chrome, and where its limits are.

What the headless-browser approach really costs

Puppeteer and Playwright are excellent tools for what they're built for: driving real browsers. Using one as an image renderer means accepting the whole browser:

  • Deploy size. Chromium is a 280+ MB dependency. On serverless platforms this historically meant special stripped builds (chrome-aws-lambda and its descendants) and fighting layer size limits.
  • Memory. Each browser instance holds hundreds of megabytes. A pool of them — which you need under any real traffic — is gigabytes.
  • Cold starts. Launching Chromium takes seconds. If your og:image endpoint cold-starts while a crawler is waiting, the crawler may time out and cache the failure (LinkedIn and Facebook both give you only a few seconds).
  • Operational flakiness. Anyone who has run headless Chrome in containers knows the greatest hits: missing system fonts, emoji rendering as tofu boxes (□), zombie processes after crashed renders, and OOM kills at inconvenient hours.
  • Attack surface. A full browser executing markup is a lot of machinery to expose behind a public, unauthenticated endpoint.

None of this is Puppeteer's fault. It's just the wrong size of tool for "put text on a background, return a PNG."

The satori + resvg pipeline

The alternative is a two-stage pipeline with no browser anywhere in it:

Stage 1 — satori (open-sourced by Vercel, and the engine behind @vercel/og) takes a JSX-like element tree with a subset of CSS and produces an SVG. Layout is computed in-process with Yoga, the same flexbox engine React Native uses. Text is shaped against font files you supply. No DOM, no JavaScript execution, no network.

Stage 2 — resvg is a Rust SVG rasterizer that turns satori's SVG into a crisp PNG. It's small, fast, and deterministic — the same input produces the same bytes every time, which makes output trivially cacheable.

The practical difference is dramatic. In Zooky's production service, a warm render typically completes in tens of milliseconds, and the entire service runs in one small container — an order of magnitude less memory than a single pooled Chrome instance, never mind a fleet of them. There are no cold-start browser launches, no zombie processes to reap, and font rendering is identical on a laptop and in production because the fonts ship with the app.

What you give up

Satori renders a deliberately constrained subset of CSS, and it's worth knowing the edges before you commit:

  • Flexbox only. No CSS grid, no floats, no position: sticky. For card layouts — a column with a badge, a title, a subtitle — flexbox is genuinely all you need, but you can't paste arbitrary page CSS in and expect it to work.
  • Fonts are your problem. There's no system font discovery. You load font files as buffers and pass them in explicitly. This is extra setup, but it's also why output is deterministic — the "works locally, tofu in prod" class of bug disappears.
  • Emoji and non-Latin scripts need explicit handling. You either load fonts covering those ranges or map emoji to images. Headless Chrome handles this "for free" — when the container has the right fonts installed, which it often doesn't.
  • No JavaScript, canvas, or external stylesheets. You're rendering a template, not a web page.

When a browser is still the right call

Be honest about the boundary. Reach for Puppeteer or Playwright when you need pixel-perfect screenshots of existing pages, charts rendered with canvas or WebGL, or arbitrary user-supplied HTML.

But look at the OG images that actually get shared: a title, a subtitle, a badge or logo, a designed background. That's 95% of real-world social cards, it's squarely inside satori's subset, and running Chrome to produce it is like keeping a truck idling to deliver envelopes.

Try the pipeline without building it

If you want dynamic OG images and would rather not assemble this stack yourself, Zooky is the same architecture as a hosted URL:

https://zooky.net/api/og?title=Ship%20It&subtitle=blog.example.com&theme=midnight

That returns a rendered 1200 × 630 PNG — seven themes, square and wide variants, free tier with no signup (50 images/hour, small watermark), Pro at $12/month for no watermark and 10,000 images a month.

And if you'd rather build it: use satori with @resvg/resvg-js, load your fonts once at boot, set long Cache-Control headers, and treat identical query strings as immutable. Either way, you can drop headless Chrome from the hot path — your infra bill and your p99 will both thank you.

Why headless Chrome is overkill for og:image generation, and how the satori + resvg pipeline renders social cards in milliseconds — plus its real limits.

Try Zooky — OG Image API free