Backend Development 14 min read

Using Satori and Resvg (or Sharp) for Efficient Backend Image Generation: Architecture, Implementation, and Optimizations

This article examines various image‑generation approaches, compares web‑frontend, client‑side, and backend methods, introduces a new Node‑backend solution based on Satori to convert HTML to SVG and then to PNG with Resvg (later Sharp), and details performance and memory optimizations that dramatically improve speed, resource usage, and stability for large‑scale image‑service deployments.

Qunar Tech Salon
Qunar Tech Salon
Qunar Tech Salon
Using Satori and Resvg (or Sharp) for Efficient Backend Image Generation: Architecture, Implementation, and Optimizations

Background

When a sharing scenario lacks a natural image—such as visualising a train route from Beijing to Shanghai—a custom image must be generated. The article first outlines three classic generation strategies: web‑frontend rendering, client‑side rendering, and backend rendering.

Common Image‑Generation Methods

Web Frontend : Tools like html2canvas can capture a DOM element and produce a canvas‑based PNG, but they suffer from cross‑origin restrictions, limited CSS support, and require a modern browser environment.

Client Side : Native APIs such as View.draw(canvas) on Android or UIGraphicsBeginImageContextWithOptions on iOS can render views to images, yet they are platform‑specific and cannot be shared across devices.

Backend : A backend service can generate images once and serve them to any client, avoiding duplication of rendering logic.

Previous Backend Solutions

Java AWT : The team initially used java.awt.BufferedImage to draw tickets. While it worked across platforms, the low‑level API made complex layouts hard to maintain and caused frequent memory leaks.

Node + Headless Chromium : Using Puppeteer to launch a headless Chrome and screenshot a page offered richer rendering, but introduced four major drawbacks: difficulty of setup, slow cold‑start (~4 s), high resource consumption, and large binary size that exceeded serverless limits.

Introducing Satori – A New Node Backend Approach

Satori, an open‑source library from Vercel, converts HTML + CSS into SVG. It runs in Node, browsers, or Web Workers, and brings several advantages:

Easy to use – no Chromium required.

Fast – SVG generation is text‑based, making it ~5× quicker than headless Chrome.

Lightweight – the library is only ~3.9 MB.

Typical usage:

import fs from 'fs'
import satori from 'satori'

async function html2svg() {
  const fontData = fs.readFileSync('./assets/fonts/SourceHanSansSC-Normal.otf')
  const svg = await satori(
    <div style={{background: '#fff', display: 'flex', width: '100%', height: '100%', alignItems: 'center', justifyContent: 'center', fontSize: 36, color: '#000'}}>Hello, World!</div>, {
      width: 600,
      height: 400,
      fonts: [{name: 'Source Han Sans SC', data: fontData, weight: 400, style: 'normal'}]
    }
  )
  fs.writeFileSync('./hello.svg', svg)
}

html2svg()

The generated SVG can then be rasterised to PNG. The original implementation used Resvg (a Rust‑based SVG renderer):

import { Resvg } from '@resvg/resvg-js'

async function svg2png(svg) {
  const resvg = new Resvg(svg, {fitTo: {mode: 'width', value: 600}, font: {defaultFontFamily: 'Source Han Sans SC Normal'}})
  const png = resvg.render().asPng()
  fs.writeFileSync('./hello.png', png)
}

svg2png(svg)

Performance and Memory Optimizations

Initial benchmarks showed the Satori + Resvg pipeline took ~900 ms per image, slower than the previous Java solution (~500 ms). Two key optimisations were applied:

Disable embedded‑font optimisation : Setting embedFont: false avoided converting text to path elements, cutting the time to ~400 ms.

Replace Resvg with Sharp : Sharp (C++‑based) supports multiple output formats and is ~2× faster. After switching, generation time dropped to ~200 ms.

Memory usage was also a concern. The service leaked memory under heavy load, reaching 90 % RAM after a few days. Two measures solved this:

Run Node with jemalloc (e.g., export LD_PRELOAD=/usr/lib64/libjemalloc.so.1 ) to improve allocation efficiency.

Adopt a multi‑process worker model: when a worker shows high memory, stop accepting new requests, let it finish pending jobs, then restart it, isolating leaks from the main service.

Results and Trade‑offs

The Satori‑based solution dramatically improved development speed (one day vs. three days for a comparable Java template) and enabled complex dynamic layouts. However, it has limitations: only a subset of HTML/CSS is supported (no form elements, limited layout engines—only flex), and some advanced tags like link or style are ignored. Nevertheless, it supports many advanced CSS features such as filter , boxShadow , textShadow , mask , backgroundClip , and transform , allowing rich visual effects.

Conclusion

By combining Satori for HTML‑to‑SVG conversion with Sharp for rasterisation, and applying targeted speed and memory optimisations, the team built a robust, high‑throughput image‑generation service that has served over 100 million images across more than 20 scenarios with stable performance.

Backendperformance optimizationNode.jsimage generationSharpResvgSatori
Qunar Tech Salon
Written by

Qunar Tech Salon

Qunar Tech Salon is a learning and exchange platform for Qunar engineers and industry peers. We share cutting-edge technology trends and topics, providing a free platform for mid-to-senior technical professionals to exchange and learn.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.