resvg-js: SVG to PNG across Node, Deno, Bun and the browser
A high-performance SVG renderer and toolkit, powered by Rust based resvg and napi-rs.
At a glance
- What is it?
- resvg-js wraps the Rust resvg renderer in a prebuilt napi-rs addon for Node.js, Deno and Bun, and publishes a separate WebAssembly build for browsers. The two backends share one options object and part company on exactly one thing that changes output: font discovery.
- Who is it for?
- Adopt resvg-js when a service must turn SVG into PNG with the same code on Node, Deno and Bun, or in a browser through the wasm build, and when installing without a C++ toolchain matters more than hand-picking a rasteriser. Step aside if you need SVG features the bundled resvg 0.34.0 does not handle, or if you must patch the Rust core, since editing src/ means running napi build yourself.
- Can I use it commercially?
- Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 114 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 18, 2026, and from our analysis. They are not legal advice.
Editorial analysis
No node-gyp, no postinstall: twelve prebuilt napi targets ship to npm
resvg-js is a binding rather than a port. Rust does the rendering, JavaScript moves bytes, and that split explains the packaging. The npm tarball carries compiled native binaries, so an install pulls no C++ toolchain, runs no node-gyp and fires no postinstall hook. The README gives the reason in one line: the `.node` file has been compiled for you. package.json names the compiled library resvgjs and publishes four files, index.js, index.d.ts, js-binding.js and js-binding.d.ts, so the platform decision happens when the addon is required rather than when it is built.
The target list is where deployment planning starts. It covers x86_64-unknown-linux-gnu, x86_64-apple-darwin, x86_64-pc-windows-msvc, x86_64-unknown-linux-musl, aarch64-unknown-linux-gnu, i686-pc-windows-msvc, armv7-unknown-linux-gnueabihf, aarch64-apple-darwin, aarch64-unknown-linux-musl, aarch64-pc-windows-msvc, aarch64-linux-android and arm-linux-androideabi, twelve entries in all. aarch64-apple-darwin is what backs the Apple silicon support the feature list calls out. Two entries are Android targets, and the README has no section on loading a native addon on a phone, so treat Android as built and undocumented. Alpine images need the musl entries rather than the gnu ones, and getting that wrong surfaces as a load failure rather than a helpful error.
fitTo and background fix the raster before render() is called
Construction and rendering are separate steps, and the options object holds every decision. `new Resvg(svg, opts)` parses the markup and fixes the canvas, `resvg.render()` returns a rendered image object, and `pngData.asPng()` encodes it. Three fields cover most real work. `fitTo` takes a mode and a value, so `mode: 'width'` with `value: 1200` rescales the output to that width, and `background` takes any colour string, 'rgba(238, 235, 230, .9)' in the bundled example, which fills the regions an SVG leaves transparent. The font block is a third object carrying `fontFiles`, `loadSystemFonts` and `defaultFontFamily`.
The two size properties are a trap for anyone reading a log. resvg.width and resvg.height report the intrinsic SVG size, while pngData.width and pngData.height report the raster, and in the Deno example the same file reports 1324 x 687 and then 1200 x 623. Height is therefore derived from the width scale factor and rounded, not chosen independently.
Since v2 the object exposes the generated PNG dimensions directly and adds two capabilities the earlier line did not have: a simplified SVG string where shapes such as rect and circle become `<path>` elements, and a bounding box you can crop against. The example folder keeps bbox.js, bbox.svg, bbox2.svg and bbox-out.png, so cropping ships as runnable code rather than prose.
A first render in one install command and one options object
The npm scope is not the repository name. The repository is thx/resvg-js, the package is `@resvg/resvg-js`:
npm i @resvg/resvg-jsThe Node.js entry point is a CommonJS require, and the import pulls in three helpers plus the class:
const { promises } = require('fs')
const { join } = require('path')
const { Resvg } = require('@resvg/resvg-js')Everything else is the options object and the three calls that turn it into a file on disk:
const opts = {
background: 'rgba(238, 235, 230, .9)',
fitTo: {
mode: 'width',
value: 1200,
},
font: {
fontFiles: ['./example/SourceHanSerifCN-Light-subset.ttf'], // Load custom fonts.
loadSystemFonts: false, // It will be faster to disable loading system fonts.
// defaultFontFamily: 'Source Han Serif CN Light', // You can omit this.
},
}
const resvg = new Resvg(svg, opts)
const pngData = resvg.render()
const pngBuffer = pngData.asPng()Running the bundled example writes ./text-out.png beside the source SVG:
node example/index.jsBun runs the same file unmodified from version 0.8.1 onwards, with `bun example/index.js`. Deno needs three permissions on the command line, read, write and ffi, and the import specifier changes too:
deno run --unstable --allow-read --allow-write --allow-ffi example/index-deno.jsThat Deno script imports the class from npm:@resvg/resvg-js and writes ./text-out-deno.png. Both runtimes report the two size lines, the intrinsic SVG size and the raster width and height, which is the quickest way to confirm your fitTo value took effect.
The wasm build drops system-fonts and pins wasm-bindgen to 0.2.92
One codebase produces two backends, and Cargo.toml is where they diverge. Native targets pull resvg 0.34.0 with default-features off and the features raster-images, text and system-fonts, while the wasm32 target asks for raster-images and text only. That single difference decides what a browser user has to do. With system-fonts absent there is nothing to discover, so fonts arrive as bytes in `fontBuffers`, which the README marks as new in 2.5.0.
Wasm has to be initialised before anything else runs, and the package itself arrives through a script tag pointing at `https://unpkg.com/@resvg/resvg-wasm`:
await resvg.initWasm(fetch('https://unpkg.com/@resvg/resvg-wasm/index_bg.wasm'))The dependency pin carries a written reason. Cargo.toml holds wasm-bindgen at `=0.2.92`, and the comment above that line says reference-types is disabled because Node.js 16 does not support it, with a link to the upstream discussion. Moving the browser backend forward is therefore not a routine dependency bump.
One line in the browser example rewards a close read. It builds the object with `new resvg.Resvg(svg, opts)` and then calls `resvgJS.render(svg, opts)`, handing over the SVG string and the options a second time, while the Node.js example calls `resvg.render()` with no arguments. Copying the browser snippet verbatim means passing constructor arguments twice, and the two code paths are not interchangeable line for line. The README also says the same Wasm build can run under Node.js through example/wasm-node.js, but slower there, which is the trade the browser backend accepts.
loadSystemFonts false is the switch that decides which families exist
Text is where rasterisers usually disagree with the design tool that produced the file, and this binding spends three options on the problem. `fontFiles` takes paths you supply, `loadSystemFonts` keeps or drops the machine's font collection, and `defaultFontFamily` names the fallback for a family nobody has. The bundled example sets `loadSystemFonts` to false and says why in a comment: it will be faster to disable loading system fonts. The matching log line reads "Loaded 1 font faces", counting only the ./example/SourceHanSerifCN-Light-subset.ttf file it was told about, not the thousands a Linux box would otherwise scan.
Cargo.toml enables the text feature for both wasm32 and native targets, so shaping exists everywhere, and the woff2 dependency is what lets a browser hand over a webfont such as the Pacifico file the browser example fetches. On a build server the opposite setting is the common one: turn `loadSystemFonts` back on so system families resolve, and add files only for the families the machine lacks.
What is missing is a documented fallback order between `defaultFontFamily`, the files you loaded and the system collection, so a missing family changes the output instead of raising an error. Two further behaviours matter for servers. Since v2, `<image>` elements pointing at external URLs are fetched during rendering, and the repository ships example/image-url.js, so rendering SVG from an untrusted source can reach the network. The documented output format is PNG alone, and the README points to index.d.ts and wasm/index.d.ts for the full typed surface.
The bench script measures width resizing, not text, fonts or filters
The closest thing to a comparison is a script in the repository, and it starts by installing the rivals:
npm i [email protected] [email protected] @types/sharp [email protected]`npm run bench` then runs `node -r @swc-node/register benchmark/bench.ts`. The suite the README prints is named resize width, and it reports 12 ops/s for resvg-js (Rust), 9 ops/s for sharp, 7 ops/s for skr-canvas (Rust) and 6 ops/s for svg2img (canvg and node-canvas).
Read the labels before you read the numbers. This is one suite about width, not a general rendering benchmark, and the install line pins the rivals loosely at [email protected], [email protected] and [email protected] while leaving resvg-js unpinned, so a rerun measures your machine and those major versions rather than the figures above. sharp is also the only entry carrying no stack annotation at all, while skr-canvas is marked Rust and svg2img is named by its two dependencies, canvg and node-canvas.
The example folder makes the fairer comparison available as files: out-sharp.png, out-skr-canvas.png, out-resvg-js.png, the @2x variant of each and compare.js. Ops per second on a resizing suite says nothing about text rendering, font fallback or how a library treats an SVG feature it does not implement, so the images and your own SVG corpus carry more weight than the table.
Three files disagree about the Node.js floor, and two counters never met
Three statements about supported Node.js versions do not agree, and you will meet all three. package.json declares an engines field of `"node": ">= 10"`. The README support matrix stops mid-grid after the Node.js 12, Node.js 14 and Node.js 16 column headers, so it confirms nothing above 16 and nothing below it either. The Cargo.toml comment ties wasm-bindgen to Node.js 16 because that runtime lacks reference-types, which makes 16 the column with a concrete reason behind it while the engines value of 10 has no explanation anywhere in the repository.
The version numbers diverge just as sharply. Cargo.toml still carries version = "1.0.0" for a crate named resvg-js while the npm manifest sits at 2.7.0-alpha.2, so the two counters have never been tied together and a Cargo.lock lookup will not tell you which npm build you are running. The Rust edition in Cargo.toml is 2024 while the README badge claims Rust 1.65+, and a rust-toolchain.toml at the repository root settles the question for anyone compiling from source.
Building from source is the escape hatch when no prebuilt binary fits, and it costs the zero-build install. The napi build script wired into package.json needs a Rust toolchain and the napi CLI on the machine, and so does the wasm-pack step that produces the browser artifact. The optimisation profile in Cargo.toml sets lto = true and opt-level = 3, and a comment there explains that catch_unwind has to stay enabled so a Rust panic becomes an error returned to the JavaScript caller instead of killing the process, at the price of a larger binary.
One more fact belongs in any adoption decision: the last push on this repository was on 2026-06-30, while the npm manifest still carries 2.7.0-alpha.2 from a run of three alpha tags dated 2026-01-22, 2026-01-27 and 2026-01-28, so the gap between the newest npm version and the newest work on main is five months. Both the npm manifest and the Cargo crate declare MPL-2.0 and the LICENSE file sits at the root, so read that licence before patching the Rust source, since MPL-2.0 is file-level copyleft and vendored edits travel with your build.
The docs are two declaration files generated from two bindings
There is no prose API reference. The README's documentation section points at index.d.ts for Node.js and wasm/index.d.ts for WebAssembly, which means the typed surface is the documentation. Each of those files is produced by the build rather than maintained by hand: the native declaration file comes out of the napi build script in package.json, and the browser types are bundled by `dts-bundle-generator --external-types -o wasm/index.d.ts wasm-binding.ts` from wasm-binding.ts.
Two maintainer-side commands produce those files, one per backend:
napi build --platform --release --js js-binding.js --dts js-binding.d.ts
wasm-pack build --target web --out-name index --out-dir wasm/dist --releaseThe practical consequence is that one Rust core sits behind two independently generated surfaces, and the README lists no step that keeps them in step. Keeping a render identical between your server and your browser means hand-checking that both option objects still accept the same keys, because the browser build lacks the system font collection and the native build has it. Read both declaration files before writing the shared config object, because that is where the two backends stop agreeing.
Editorial conclusion
Adopt resvg-js when a service must turn SVG into PNG with the same code on Node, Deno and Bun, or in a browser through the wasm build, and when installing without a C++ toolchain matters more than hand-picking a rasteriser. Step aside if you need SVG features the bundled resvg 0.34.0 does not handle, or if you must patch the Rust core, since editing src/ means running napi build yourself. Before committing, read index.d.ts for the option surface, confirm your platform sits in the twelve napi targets, and settle how fonts reach the renderer.
Frequently asked questions
Does resvg-js need node-gyp or a Rust toolchain to install?
No. The README states that the .node file has been compiled for you, with no need for node-gyp and no postinstall step, so `npm i @resvg/resvg-js` is the whole installation. A Rust toolchain is only needed if you build the binding yourself with napi.
Which Node.js versions does resvg-js support?
package.json declares an engines field of `"node": ">= 10"`, while the README support matrix is cut off after the Node.js 16 column header. A Cargo.toml comment ties the wasm-bindgen pin to Node.js 16 because that runtime does not support reference-types, so treat 16 as the runtime the project has a documented reason for.
How do I render text with a custom font in resvg-js?
Pass the paths you want in font.fontFiles and set loadSystemFonts to false when you want to skip the machine's font collection, which is what the bundled example does. In the browser the wasm build leaves out system fonts, so fonts go through fontBuffers instead, a field the README marks as new in 2.5.0.
Can resvg-js run in a browser?
Yes, through the separate @resvg/resvg-wasm package loaded from unpkg with a script tag. You must call resvg.initWasm with the index_bg.wasm file before constructing an object, and the README notes the same Wasm build also runs under Node.js through example/wasm-node.js, but slower there.
How does resvg-js compare with sharp?
The sample benchmark in the repository reports 12 ops/s for resvg-js against 9 ops/s for sharp, 7 ops/s for skr-canvas and 6 ops/s for svg2img in a resize width suite, produced by npm run bench. Those figures come from one suite with the rivals pinned only to major versions, so treat them as a starting point rather than a result you can reproduce exactly.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/thx-resvg-js)