Ditching Puppeteer for 2.37x faster opengraph image generation
GitHub dynamically generates opengraph images for repositories and other pages such as issues and pull requests of a particular repository.
GitHub outlined their approach on their engineering blog: they spin up a headless Chromium instance via Puppeteer, load a template with HTML & CSS, and take a screenshot of the DOM.
They optimized their render pipeline from an initial ~2.25s to an average of 280ms, but orchestrating a full browser engine still adds substantial overhead. I measured that browser overhead in time and memory against decal.
Setup
I used the github-card example from the decal repository. This example exactly replicates the GitHub opengraph image for the nasa/fprime repository. The scene is first rendered to an SVG which is then rasterized into a PNG image.
The decal implementation uses a macro to define the layout and styling directly in Rust:
let mut gh_card = decal! {
Column {
Row {
Column {
Scene(header("nasa", "fprime"))
Text("F' - A flight software and embedded systems framework")
.color(rgb(0x6e7681))
}
.gap(48)
.width(pct(70))
Image("https://avatars.githubusercontent.com/u/848102?s=200", 200.0, 200.0)
}
.justify_content(JustifyContent::SpaceBetween)
.gap(64)
.flex_grow(1.0)
// ... stars, forks, and language breakdown
}
.font_size(32.0)
.line_height(46.0)
.font_family("Mona Sans")
.background(rgb(0xffffff))
.size((1200, 600))
.padding(80)
};I compiled the github-card example in release mode on my MacBook Air M2 for benchmarking.
Benchmarking the first run typically includes a slight delay due to networking overhead when fetching the repository profile picture over the internet. The subsequent runs provide a stable metric for the rendering since the images are cached.

Rendering speed
I recorded the render time for decal and compared it to the generation metrics published by GitHub.
- GitHub (Puppeteer):
~280ms - Decal (cold start):
~118ms - Decal (warm cache):
~3.1ms
GitHub’s generator executes at roughly 280ms per image. Their Puppeteer pipeline originally took over 2.25s per image, but they later discovered that tweaking network idle settings allowed them to capture the screenshot much earlier without waiting for full page readiness.
On the very first iteration, decal took ~118ms. Most of this render time is dominated by network I/O (fetching the remote repository profile picture).
Once the engine caches fonts and remote images, performance improves. For cached or locally available images, the average render time dropped to ~3.1ms.
Memory footprint
The overhead of the browser engine becomes much more obvious when we look at memory utilization.
- GitHub (Puppeteer):
> 512MB - Decal:
23.1MB peak
To achieve their 280ms speed, GitHub explicitly had to bypass Chromium’s low-spec device flag. On devices with less than 512MB of memory, Chromium runs processes sequentially to conserve RAM. By bypassing this, GitHub effectively requires allocating more than 512MB of memory per rendering instance to prevent throttling.
decal reached a peak memory usage of ~23 MB while rendering the same image 100 times in a loop.
Cost of rendering in browser
Headless browsers allow developers to use HTML and CSS for defining image content. But orchestrating a browser to take a screenshot forces unnecessary work.
By stripping out the browser and using a native layout engine, decal executes ~2.37 times faster and uses approximately ~22 times less memory. In a production environment, this means you could run dozens of concurrent decal renderers on a 512MB VPS that would otherwise struggle to smoothly run a single Puppeteer instance.
Caching
Opengraph images like GitHub’s preview cards display dynamic metrics (stars, forks, open issues, etc.) that change frequently.
An expensive Puppeteer pipeline requires images to be cached to prevent server overload, so users often see stale data when sharing links. Because decal’s cached rendering takes 3.1ms, you can reduce cache expiration times or avoid caching the final image entirely. The renderer can fetch the latest metrics and generate the image on the fly for every request.