Bekzod Erkinov
Listen to Article
Loading...Table of contents · 9 sections
Node.js Image Resize: Sharp vs Jimp Benchmark You Can Rerun
Resizing twelve 3000×2000 JPEGs to 800 px wide took sharp 1.1 s and jimp 24.2 s on my laptop, in one session; with sharp's decoder shortcut switched off the gap is 14x instead of 22x.
| Tool (3000×2000 → 800 wide, JPEG q80) | Median for 12 images | Min–max of 5 rounds | Per image | Peak RSS | Avg output |
|---|---|---|---|---|---|
| sharp (default) | 1.08 s | 1.02–1.15 s | 90 ms | 89 MB | 54.5 KB |
sharp (fastShrinkOnLoad: false) |
1.73 s | 1.64–1.81 s | 144 ms | 113 MB | 54.6 KB |
| jimp (default bilinear) | 24.2 s | 23.1–29.3 s | 2,017 ms | 397 MB | 60.5 KB |
jimp (BICUBIC) |
41.8 s | 38.8–49.3 s | 3,486 ms | 393 MB | 61.3 KB |
jimp (HERMITE) |
43.5 s | 38.5–52.1 s | 3,627 ms | 396 MB | 61.0 KB |
Use sharp unless you can't run a native binary; the last section covers what I tested for that case. Everything below is a set of scripts that generate their own test images, so you can rerun each number.
Test machine and versions
Intel Core i7-8550U (4 cores / 8 threads), 31.8 GB RAM, Windows 11, Node.js 24.19.0, sharp 0.35.5, jimp 1.6.1, ssim.js 3.5.0, run on 2026-10-11. The table is one session: five rounds, and in each round every tool ran once, one after another, so drift in machine speed hits all tools alike. Your hardware will shift the absolute figures. Sharp's own performance page lists 89.63 ops/s for sharp and 3.44 for jimp (JPEG, AMD64 c8a.xlarge, Node.js 24.15.0), about 26x; I read those figures from the live page on 2026-10-11.
Set up and run order
mkdir resize-bench && cd resize-bench
npm init -y
npm pkg set type=module
npm i [email protected] [email protected] [email protected]
# save each script below under the filename in its first comment, then:
node gen.mjs # writes images/ and manifest.json
node session.mjs 5 | tee results-session.jsonl # the table above (takes ~10 min, mostly jimp)
node concurrent.mjs sharp 5 # four resizes in flight
node concurrent.mjs jimp 3 # four worker_threads
# quality on real images (download steps in that section)
node photoqual.mjs photos/hubble.jpg photos/mars.jpg
node chroma.mjs # chroma subsampling of the outputs
node jimpdefault.mjs # jimp's default JPEG quality
sh install.sh # install size and import time, each library alone
npm pkg set type=module makes the .mjs scripts and package.json agree on ES modules. According to sharp's install docs, it needs "a Node-API v9 compatible runtime e.g. Node.js >= 20.9.0".
Generate test images you can verify
gen.mjs builds 12 deterministic 3000×2000 images from seeded multi-octave value noise, hard checkerboard edges and a little grain, saved as JPEG q90, and writes a SHA-256 for each file to manifest.json.
// Generates 12 deterministic "photo-like" JPEGs (3000x2000, q90) + manifest.json with SHA-256.
import sharp from 'sharp';
import { mkdirSync, writeFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
const W = 3000, H = 2000, N = 12;
function rng(seed) { let s = seed >>> 0; return () => ((s = (Math.imul(s, 1664525) + 1013904223) >>> 0) / 2 ** 32); }
function noiseGrid(r, gw, gh) { return Float32Array.from({ length: gw * gh }, r); }
function sample(g, gw, gh, x, y) { // bilinear value noise, smoothstep
const x0 = Math.floor(x), y0 = Math.floor(y), fx = x - x0, fy = y - y0;
const sx = fx * fx * (3 - 2 * fx), sy = fy * fy * (3 - 2 * fy);
const a = g[(y0 % gh) * gw + (x0 % gw)], b = g[(y0 % gh) * gw + ((x0 + 1) % gw)];
const c = g[((y0 + 1) % gh) * gw + (x0 % gw)], d = g[((y0 + 1) % gh) * gw + ((x0 + 1) % gw)];
return (a + (b - a) * sx) * (1 - sy) + (c + (d - c) * sx) * sy;
}
mkdirSync('images', { recursive: true });
const manifest = [];
for (let i = 0; i < N; i++) {
const r = rng(1000 + i);
const octaves = [[6, 4], [24, 16], [96, 64], [300, 200]].map(([gw, gh]) => ({ gw, gh, g: noiseGrid(r, gw, gh) }));
const px = Buffer.alloc(W * H * 3);
const hue = [r(), r(), r()];
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
let v = 0, amp = 0.5;
for (const o of octaves) { v += amp * sample(o.g, o.gw, o.gh, (x / W) * (o.gw - 1), (y / H) * (o.gh - 1)); amp *= 0.55; }
const edge = ((x >> 7) ^ (y >> 7)) & 1 ? 0.06 : 0; // hard edges so resamplers differ
const n = (r() - 0.5) * 0.04; // sensor-like grain
const o = (y * W + x) * 3;
for (let c = 0; c < 3; c++) px[o + c] = Math.max(0, Math.min(255, (v * (0.6 + 0.4 * hue[c]) + edge + n) * 255 * 1.1));
}
const file = `images/img${String(i).padStart(2, '0')}.jpg`;
const buf = await sharp(px, { raw: { width: W, height: H, channels: 3 } }).jpeg({ quality: 90 }).toBuffer();
writeFileSync(file, buf);
manifest.push({ file, bytes: buf.length, sha256: createHash('sha256').update(buf).digest('hex') });
}
writeFileSync('manifest.json', JSON.stringify(manifest, null, 2));
console.table(manifest.map(m => ({ file: m.file, KB: (m.bytes / 1024).toFixed(0), sha256: m.sha256.slice(0, 12) })));
On my machine it produced 12 files between 969 and 1,026 KB. The first three SHA-256 prefixes were 31a140b807da, 9fb75a216f7f and 252e28ea7678. The pixels are deterministic but the JPEG bytes depend on the encoder, so pin sharp 0.35.5 if you want to compare checksums. These are synthetic images, not photos; the quality section uses real ones.
The benchmark scripts
Each tool runs in its own process so peak memory is attributable. Files are read into memory first, so disk speed isn't measured. worker.mjs times decode, resize to 800 px wide and JPEG q80 encode after one untimed warm-up image. session.mjs calls it once per tool per round, interleaved.
// worker.mjs
// usage: node worker.mjs <sharp|sharp-noshrink|jimp-bilinear|jimp-bicubic|jimp-hermite> <runs>
// Decode -> resize to 800px wide -> encode JPEG q80, from in-memory buffers. Prints JSON.
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
const [tool, runs = '5'] = process.argv.slice(2);
const files = JSON.parse(readFileSync('manifest.json')).map(m => m.file);
const bufs = files.map(f => readFileSync(f));
const Q = 80, WIDTH = 800;
let fn;
if (tool.startsWith('sharp')) {
const sharp = (await import('sharp')).default;
const fastShrinkOnLoad = tool !== 'sharp-noshrink';
fn = b => sharp(b).resize({ width: WIDTH, fastShrinkOnLoad }).jpeg({ quality: Q }).toBuffer();
} else {
const { Jimp, ResizeStrategy } = await import('jimp');
const mode = { 'jimp-bilinear': undefined, 'jimp-bicubic': ResizeStrategy.BICUBIC, 'jimp-hermite': ResizeStrategy.HERMITE }[tool];
fn = async b => { const im = await Jimp.read(b); im.resize({ w: WIDTH, mode }); return im.getBuffer('image/jpeg', { quality: Q }); };
}
await fn(bufs[0]); // warm-up, not timed
const times = []; let outs;
for (let r = 0; r < +runs; r++) {
const t0 = performance.now(); outs = [];
for (const b of bufs) outs.push(await fn(b));
times.push(performance.now() - t0);
}
mkdirSync(`out/${tool}`, { recursive: true });
outs.forEach((o, i) => writeFileSync(`out/${tool}/${i}.jpg`, o));
times.sort((a, b) => a - b);
const median = times[Math.floor(times.length / 2)];
console.log(JSON.stringify({ tool, runs: +runs, images: bufs.length, medianMs: Math.round(median), perImageMs: Math.round(median / bufs.length), imgPerSec: +(bufs.length / median * 1000).toFixed(2), allMs: times.map(Math.round), outKBavg: +(outs.reduce((s, o) => s + o.length, 0) / outs.length / 1024).toFixed(1), peakRssMB: Math.round(process.resourceUsage().maxRSS / 1024) }));
// session.mjs - usage: node session.mjs <rounds>
// One session, interleaved: each round runs every tool once in its own process (1 warm-up + 1 timed pass).
import { execFileSync } from 'node:child_process';
const rounds = +(process.argv[2] ?? 5);
const tools = ['sharp', 'sharp-noshrink', 'jimp-bilinear', 'jimp-bicubic', 'jimp-hermite'];
const res = Object.fromEntries(tools.map(t => [t, []]));
for (let r = 0; r < rounds; r++) for (const t of tools) {
const o = JSON.parse(execFileSync('node', ['worker.mjs', t, '1'], { encoding: 'utf8' }));
res[t].push(o); console.error(`round ${r + 1} ${t} ${o.medianMs} ms`);
}
for (const t of tools) {
const ms = res[t].map(o => o.medianMs).sort((a, b) => a - b);
console.log(JSON.stringify({ tool: t, rounds, medianMs: ms[Math.floor(ms.length / 2)], minMs: ms[0], maxMs: ms.at(-1), perImageMs: Math.round(ms[Math.floor(ms.length / 2)] / 12), peakRssMB: Math.max(...res[t].map(o => o.peakRssMB)), outKBavg: res[t][0].outKBavg }));
}
Output (these lines are the source of the table at the top; the rounds field is the number of interleaved rounds):
{"tool":"sharp","rounds":5,"medianMs":1083,"minMs":1020,"maxMs":1146,"perImageMs":90,"peakRssMB":89,"outKBavg":54.5}
{"tool":"sharp-noshrink","rounds":5,"medianMs":1730,"minMs":1636,"maxMs":1807,"perImageMs":144,"peakRssMB":113,"outKBavg":54.6}
{"tool":"jimp-bilinear","rounds":5,"medianMs":24199,"minMs":23082,"maxMs":29287,"perImageMs":2017,"peakRssMB":397,"outKBavg":60.5}
{"tool":"jimp-bicubic","rounds":5,"medianMs":41827,"minMs":38832,"maxMs":49330,"perImageMs":3486,"peakRssMB":393,"outKBavg":61.3}
{"tool":"jimp-hermite","rounds":5,"medianMs":43526,"minMs":38514,"maxMs":52094,"perImageMs":3627,"peakRssMB":396,"outKBavg":61}
Fairness: sharp's default shortcut
Sharp's fastShrinkOnLoad defaults to true, which lets the JPEG decoder shrink the image while loading; the resize docs say it can cause a slight moiré pattern. For a 3000→800 resize that skips real work, so the like-for-like comparison is sharp-noshrink against jimp-bilinear: 24,199 / 1,730 ≈ 14x. With the default it is 24,199 / 1,083 ≈ 22x.
Concurrency: is the ratio single-stream?
concurrent.mjs runs sharp with four resizes in flight (Promise.all) and jimp across four worker_threads, each handling three of the 12 images. The jimp number includes worker start-up and module loading in every pass.
// concurrent.mjs - usage: node concurrent.mjs <sharp|jimp> <runs>
// sharp: 4 resizes in flight at once (Promise.all). jimp: 4 worker_threads, each given 3 of the 12 images.
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
import { readFileSync } from 'node:fs';
const WIDTH = 800, Q = 80;
if (!isMainThread) {
const { Jimp } = await import('jimp');
const bufs = workerData.files.map(f => readFileSync(f));
for (const b of bufs) { const im = await Jimp.read(b); im.resize({ w: WIDTH }); await im.getBuffer('image/jpeg', { quality: Q }); }
parentPort.postMessage('done');
} else {
const [tool, runs = '3'] = process.argv.slice(2);
const files = JSON.parse(readFileSync('manifest.json')).map(m => m.file);
let job;
if (tool === 'sharp') {
const sharp = (await import('sharp')).default;
const bufs = files.map(f => readFileSync(f));
job = async () => { for (let i = 0; i < bufs.length; i += 4)
await Promise.all(bufs.slice(i, i + 4).map(b => sharp(b).resize({ width: WIDTH }).jpeg({ quality: Q }).toBuffer())); };
} else {
job = () => Promise.all([0, 1, 2, 3].map(k => new Promise((res, rej) => {
const w = new Worker(new URL(import.meta.url), { workerData: { files: files.slice(k * 3, k * 3 + 3) } });
w.on('message', res); w.on('error', rej); })));
}
await job(); // warm-up, not timed (for jimp this includes worker start-up in every run anyway)
const times = [];
for (let r = 0; r < +runs; r++) { const t0 = performance.now(); await job(); times.push(Math.round(performance.now() - t0)); }
times.sort((a, b) => a - b);
console.log(JSON.stringify({ tool: tool + '-concurrent', runs: +runs, medianMs: times[Math.floor(times.length / 2)], allMs: times, peakRssMB: Math.round(process.resourceUsage().maxRSS / 1024) }));
}
{"tool":"sharp-concurrent","runs":5,"medianMs":333,"allMs":[327,328,333,352,386],"peakRssMB":142}
{"tool":"jimp-concurrent","runs":3,"medianMs":8674,"allMs":[8536,8674,8918],"peakRssMB":1327}
Both are faster than their sequential default-settings medians (sharp 1,083 → 333 ms, jimp 24.2 → 8.7 s), and the ratio is about 26x (8,674 / 333) with the shortcut on. Jimp's worker run peaked at 1,327 MB RSS for the whole process, so don't compare that with the single-stream numbers. I tested four threads on a 4-core laptop only.
Does the output look the same?
I don't use the synthetic images for quality, because they are smooth noise rather than photographic detail. Instead I checked two real public-domain images from Wikimedia Commons, pinned by URL and checksum:
mkdir photos
curl -sL -o photos/hubble.jpg https://upload.wikimedia.org/wikipedia/commons/2/2f/Hubble_ultra_deep_field.jpg
curl -sL -o photos/mars.jpg https://upload.wikimedia.org/wikipedia/commons/3/36/Mars_Valles_Marineris_EDIT.jpg
sha256sum photos/*.jpg
ffb57f5daf8d23ff96e584488278670a402bceb4759a293b7d0b942ca4e98b01 *photos/hubble.jpg
3c88ef447e2c527281dcc110ee42243261e280d0f21e63f64e50fbf814e0b7e6 *photos/mars.jpg
The Commons file pages for Hubble ultra deep field and Mars Valles Marineris EDIT list them as public domain (I read the license field through the Commons API). These are astronomy images, not everyday photos, so treat the result as a second data point rather than a verdict.
To avoid grading the tools against one of themselves, the reference is an exact area-average downscale written in plain JS (each output pixel is the fractional-area mean of the source pixels it covers). Only the grayscale decode of the original goes through sharp. SSIM is computed on grayscale with ssim.js.
// photoqual.mjs - SSIM on real images against an independent reference: exact area-average (box) downscale written in plain JS.
// usage: node photoqual.mjs photos/a.jpg photos/b.jpg
import sharp from 'sharp';
import { Jimp, ResizeStrategy } from 'jimp';
import { readFileSync } from 'node:fs';
import { ssim } from 'ssim.js';
const W = 800, Q = 80;
const gray = async (input) => { const { data, info } = await sharp(input).greyscale().raw().toBuffer({ resolveWithObject: true });
return { data, width: info.width, height: info.height }; };
function areaAverage(src, outW, outH) { // each output pixel = exact fractional-area mean of the source pixels it covers
const out = new Uint8ClampedArray(outW * outH * 4), sx = src.width / outW, sy = src.height / outH;
for (let y = 0; y < outH; y++) for (let x = 0; x < outW; x++) {
let sum = 0, wsum = 0;
for (let j = Math.floor(y * sy); j < Math.ceil((y + 1) * sy); j++) { const wy = Math.min(j + 1, (y + 1) * sy) - Math.max(j, y * sy);
for (let i = Math.floor(x * sx); i < Math.ceil((x + 1) * sx); i++) { const w = wy * (Math.min(i + 1, (x + 1) * sx) - Math.max(i, x * sx));
sum += w * src.data[j * src.width + i]; wsum += w; } }
const v = Math.round(sum / wsum), o = (y * outW + x) * 4; out[o] = out[o + 1] = out[o + 2] = v; out[o + 3] = 255;
}
return { data: out, width: outW, height: outH };
}
const rgba = async (buf) => { const g = await gray(buf); const d = new Uint8ClampedArray(g.width * g.height * 4);
for (let p = 0; p < g.width * g.height; p++) { d[p * 4] = d[p * 4 + 1] = d[p * 4 + 2] = g.data[p]; d[p * 4 + 3] = 255; } return { data: d, width: g.width, height: g.height }; };
for (const file of process.argv.slice(2)) {
const input = readFileSync(file), full = await gray(input), h = Math.round(full.height * W / full.width);
const ref = areaAverage(full, W, h);
const outs = {
sharp: await sharp(input).resize({ width: W }).jpeg({ quality: Q }).toBuffer(),
'sharp-noshrink': await sharp(input).resize({ width: W, fastShrinkOnLoad: false }).jpeg({ quality: Q }).toBuffer(),
};
for (const [name, mode] of [['jimp-bilinear', undefined], ['jimp-bicubic', ResizeStrategy.BICUBIC], ['jimp-hermite', ResizeStrategy.HERMITE]]) {
const im = await Jimp.read(input); im.resize({ w: W, mode }); outs[name] = await im.getBuffer('image/jpeg', { quality: Q }); }
console.log(file, `${full.width}x${full.height} -> ${W}x${h}`);
for (const [name, b] of Object.entries(outs)) console.log(' ', name.padEnd(15), (b.length / 1024).toFixed(1).padStart(6), 'KB SSIM vs area-average', ssim(ref, await rgba(b)).mssim.toFixed(5));
}
photos/hubble.jpg 3100x3100 -> 800x800
sharp 96.2 KB SSIM vs area-average 0.99370
sharp-noshrink 97.2 KB SSIM vs area-average 0.99321
jimp-bilinear 110.4 KB SSIM vs area-average 0.99507
jimp-bicubic 111.4 KB SSIM vs area-average 0.98659
jimp-hermite 112.0 KB SSIM vs area-average 0.98928
photos/mars.jpg 1716x1716 -> 800x800
sharp 57.8 KB SSIM vs area-average 0.99706
sharp-noshrink 57.8 KB SSIM vs area-average 0.99706
jimp-bilinear 70.3 KB SSIM vs area-average 0.99692
jimp-bicubic 73.6 KB SSIM vs area-average 0.99419
jimp-hermite 72.2 KB SSIM vs area-average 0.99435
What these numbers show: jimp's default bilinear scored slightly higher than sharp on the Hubble image (0.99507 vs 0.99370) and slightly lower on Mars (0.99692 vs 0.99706), while its files were 15 to 22% bigger at the same quality: 80 (110.4 vs 96.2 KB; 70.3 vs 57.8 KB). Jimp's bicubic and hermite modes scored lower than both on both images. The area-average reference is itself a resampler, and bilinear is the closest of these to it, so it may favour jimp's default; two images are far too few to rank the tools. The speed gap is much larger than any quality gap I measured.
Why jimp's files are bigger
Sharp's JPEGs are 4:2:0 chroma-subsampled and jimp's are 4:4:4:
// chroma.mjs - chroma subsampling of the q80 outputs
import sharp from 'sharp';
for (const t of ['sharp', 'jimp-bilinear']) {
const m = await sharp(`out/${t}/0.jpg`).metadata();
console.log(t.padEnd(14), 'chromaSubsampling:', m.chromaSubsampling);
}
sharp chromaSubsampling: 4:2:0
jimp-bilinear chromaSubsampling: 4:4:4
The cause is visible in the installed encoder. Jimp's JPEG plugin imports jpeg-js (node_modules/@jimp/js-jpeg/src/index.ts line 1; jpeg-js 0.4.4 here), and writeSOF0 in node_modules/jpeg-js/lib/encoder.js writes the same sampling-factor byte for all three components:
writeByte(0x11); // HVY
writeByte(0x11); // HVU
writeByte(0x11); // HVV
0x11 is 1×1 sampling for Y, U and V, i.e. no chroma subsampling. I did not measure how much of the 15 to 22% size gap this accounts for.
Memory and install size
Peak resident memory for the process, from process.resourceUsage().maxRSS, was 89 MB for sharp and 393 to 397 MB for jimp (top table). I measured one image size only, so I can't say how it scales; if you resize large uploads in a small container, measure peak RSS on your own inputs.
For install size I installed each library alone in a fresh folder and measured the whole node_modules (transitive dependencies included). install.sh:
#!/bin/sh
# install.sh - each library alone in a fresh folder: install time, whole node_modules size, import time (median of 5)
for pkg in [email protected] [email protected]; do
d=fresh-${pkg%@*}; rm -rf $d; mkdir $d; (cd $d && npm init -y >/dev/null && npm pkg set type=module >/dev/null
s=$(date +%s%N); npm i $pkg >/dev/null 2>&1; e=$(date +%s%N)
echo "$pkg: npm i $(( (e - s) / 1000000 )) ms, node_modules $(du -sm node_modules | cut -f1) MB, $(ls node_modules | grep -v '^@' | wc -l) unscoped + $(ls -d node_modules/@*/* | wc -l) scoped packages"
name=${pkg%@*}; for i in 1 2 3 4 5; do s=$(date +%s%N); node -e "await import('$name')" --input-type=module >/dev/null; e=$(date +%s%N); echo $(( (e - s) / 1000000 )); done | sort -n | sed -n 3p | sed "s/^/ import median ms: /")
done
[email protected]: npm i 3434 ms, node_modules 21 MB, 3 unscoped + 2 scoped packages
import median ms: 367
[email protected]: npm i 8014 ms, node_modules 31 MB, 31 unscoped + 32 scoped packages
import median ms: 771
On Windows x64, sharp (21 MB) is smaller than jimp (31 MB) once dependencies are counted; the sharp directory alone is mostly the platform binary in @img. The install times are with a warm npm cache and the import times varied between two runs of this script (329 vs 367 ms for sharp, 517 vs 771 ms for jimp), so read them as "same order of magnitude, jimp slower to load here", not as precise figures.
Gotchas I hit
Jimp's JPEG default quality is 100. The encoder in the jimp source declares encode: (bitmap, { quality = 100 }: JPEGOptions = {}) =>. In practice:
// jimpdefault.mjs - jimp's JPEG size with and without an explicit quality
import { Jimp } from 'jimp'; import { readFileSync } from 'node:fs';
const im = await Jimp.read(readFileSync('images/img00.jpg')); im.resize({ w: 800 });
for (const o of [undefined, { quality: 80 }]) console.log(JSON.stringify(o), ((await im.getBuffer('image/jpeg', o)).length / 1024).toFixed(1), 'KB');
undefined 307.0 KB
{"quality":80} 62.4 KB
That is one resized image (img00.jpg): 307.0 KB with no options, 62.4 KB with { quality: 80 }. Always pass quality explicitly. The jimp API reference documents getBuffer(mime, { quality }).
Jimp v1 changed its API. It's import { Jimp } from "jimp" and image.resize({ w: 800 }), as in the jimp docs. Tutorials that call image.resize(800, Jimp.AUTO) or .quality(80) are for v0.x; I didn't run them on 1.x. The mode option I used for bicubic and hermite is declared in the installed type definitions (node_modules/@jimp/plugin-resize/dist/commonjs/index.d.ts: mode?: ResizeStrategy, with the comment "a scaling method (e.g. ResizeStrategy.BEZIER)"), and my worker.mjs runs with it, but the docs page I read only documents mode for cover and contain, so treat it as less documented.
When jimp's pure-JS install is still the right choice
What I tested, in node:24-alpine (Alpine 3.24.2, x86_64, Node 24.21.0) via Docker on the same laptop, each library installed alone and used to resize photos/mars.jpg to 800 px at q80:
[email protected]: node_modules 20 MB
sharp ok 59156 bytes
[email protected]: node_modules 34 MB
jimp ok 71992 bytes
Sharp installed and ran on musl with no extra steps, and the install docs list prebuilt musl binaries for x64 and ARM64. I did not test ARM64, so I can't say anything measured about Lambda on ARM.
For environments where the native binary can't run, I also tried sharp's optional WebAssembly package in the same container. The install docs say the @img/sharp-wasm32 package supports "runtime environments that provide multi-threaded Wasm via Workers", and list browsers and single-threaded environments as unsupported. I installed it with npm i --cpu=wasm32 [email protected] @img/sharp-wasm32, which skips the native binary:
colour
sharp-wasm32
sharp-webcontainers-wasm32
12
sharp wasm ok 59156 bytes
(ls node_modules/@img, then the whole node_modules in MB, then the resize.) The wasm build produced the same 59,156 bytes as the native build on Alpine; I did not time it.
So the rule I'd follow from this:
- Default to sharp. It installed and ran on Windows x64 and Alpine x64, and it was the smaller install once dependencies were counted on Windows.
- If a native binary can't run in your environment, try sharp's wasm32 package before switching libraries. It worked on Alpine x64 in my test; check the install docs' limits above for your runtime.
- Pick jimp if you need zero native code. Jimp's repository description says "zero external or native dependencies" (GitHub), and its README says "written entirely in JavaScript, with zero native dependencies". You pay for it: 14x to 22x slower on my runs, and about 4x the peak memory.
What to do next
Run the setup block, compare your sharp-noshrink to jimp-bilinear ratio with my ~14x, and point photoqual.mjs at a few of your own photos before you trust any quality ranking.
Keep reading
Complete Solution: Scaling a Node.js Application with Kubernetes and Docker
29 min · 903 views
Web DevelopmentMastering Multi-Tenant SaaS Architecture Patterns for Enterprise Applications
27 min · 863 views
Web Development10 Essential Tools for Modern Frontend Development
16 min · 810 views
Bekzod Erkinov
AuthorFounder of NextGenBeing. Software engineer working with Laravel, Python, and cloud infrastructure. Writes about patterns that actually hold up in production. Based in Tashkent, Uzbekistan.
Get the AI-Assisted Developer's Field Guide
The workflow, prompts, and tools I use to ship faster with AI — free when you subscribe. Plus new deep-dives in your inbox. No spam, unsubscribe anytime.
Comments (0)
Please log in to leave a comment.
Log In