AIDevDesignMarketingFoundersBusinessNewsAbout
Work With Me
Work With Me

AI Blueprints to Leverage Your Business. Strategies. Systems. Execution.

hi@omidsaffari.com
Instagram·X·LinkedIn·GitHub
Navigation
  • HomeHome
  • AboutAbout
  • BlogBlog
  • NewsletterNewsletter
  • Work With MeWork With Me
  • ContactContact
Legal
  • PrivacyPrivacy
  • TermsTerms
  • DisclaimerDisclaimer
  • SitemapSitemap
  • RSS FeedRSS Feed
Categories
  • AIAI
  • StackStack
  • DesignDesign
  • WorkflowWorkflow
  • GrowthGrowth
Topics
  • AI AgentsAI Agents
  • PromptsPrompts
  • Next.jsNext.js
  • n8nn8n
  • NotionNotion
Formats
  • GuidesGuides
  • LabsLabs
  • ToolsTools
  • TrendsTrends
  • ResourcesResources
More Formats
  • TutorialsTutorials
  • Case StudiesCase Studies
  • ComparisonsComparisons
  • TemplatesTemplates
  • ChecklistsChecklists
Empire
  • DaVinci HorizonDaVinci Horizon
  • Imperfeqt AIImperfeqt AI
  • DVNC StudioDVNC Studio
  • DVNC.aeDVNC.ae
  • With LidaWith Lida
Connect
  • YouTubeYouTube
  • Twitter/XTwitter/X
  • LinkedInLinkedIn
  • GitHubGitHub
  • InstagramInstagram
© 2026 omidsaffari.comBuilt with Next.js · Vercel
  1. Blog
  2. Dev

Bun 1.3.14 Drops Bun.Image as a Sharp Drop-In. Here's the 4-Step Migration I Ran on My Image Pipeline.

A working migration playbook for senior devs running production image workloads on Bun. Maps every Sharp method I had in production to its Bun.Image equivalent, surfaces the four API differences that broke first, and benchmarks the result…

Bun 1.3.14 Drops Bun.Image as a Sharp Drop-In. Here's the 4-Step Migration I Ran on My Image Pipeline.
Omid Saffari

Founder & CEO, AI Entrepreneur

Share
Stay updated

Get weekly AI blueprints and insights.

Bun shipped v1.3.14 on May 13, 2026 with Bun.Image, a libjpeg-turbo + spng + libwebp pipeline that mirrors the Sharp API and runs without a single native addon build step. After three years of CI failing on lovell/sharp's libvips binary every time I bumped Node, this is the version that made me tear Sharp out.

Why I dropped Sharp the day 1.3.14 landed

The Bun 1.3.14 release notes landed on May 13 with the usual bullet list, and buried under "HTTP/3 client" and "7x faster warm installs" was the line that ended my Sharp run: Bun.Image, a chainable image pipeline built into the runtime, with libjpeg-turbo, spng, and libwebp compiled directly into the Bun binary.

If you've never lost a CI day to Sharp, you can skip this paragraph. If you have, you know the script. sharp/lib/sharp-linuxmusl-x64.node not found. Cannot find module '../build/Release/sharp.node' in the Alpine container. The Docker layer cache invalidates because someone bumped Node and now npm rebuild sharp runs from scratch on every push. The Vercel build hits the prebuilt-binary CDN and times out. Three years of that, on three separate projects, and somewhere in the middle of it I started writing apk add --no-cache vips-dev from muscle memory.

Bun.Image is the runtime-native answer. No npm install step for image processing – the codecs ship inside the Bun binary. No native addon to rebuild when the Node ABI changes, because there's no Node and no addon. The geometry kernels are i16 fixed-point SIMD; JPEG decode scales to the smallest sufficient size automatically. It is, structurally, the thing Sharp would have been if Sharp didn't have to be a Node addon.

The other reason 1.3.14 mattered to me: this is the last Zig release before the Anthropic-funded Rust rewrite lands. The Register covered the merge cadence on May 14, and the engineering velocity from here will be serious. I'd rather migrate to a Bun primitive now than carry a native-addon dependency through a runtime rewrite.

This is not a Sharp obituary. Sharp on libvips is still the speed king for animated WebP, color-profile-critical photography work, and the tile() deepzoom pyramid. What follows is the playbook for the 95% of image work that isn't those three things – the decode-resize-encode pipeline most production apps run.

The 4-step migration on my cover-image pipeline

I ran this swap on omidsaffari-admin, the worker that post-processes gpt-image-2 cover output before it hits R2. Eight Sharp call sites in one file, all in the path that runs after the PublishWorkflow cover step. Total migration time: 42 minutes, including the dual-encode WebP-with-JPEG-fallback path.

Step 1 – Audit your Sharp surface area. Before you touch anything, find every import:

bash
1rg -n "from ['\"]sharp['\"]" src/
2rg -n "require\(['\"]sharp['\"]\)" src/

You want to know up front whether this is a four-call-site swap or a forty-call-site one. If it's forty, you do it route by route, not all at once.

Step 2 – Replace the import with Bun.file().image(). Sharp's constructor takes a path, a Buffer, or a Stream. Bun.Image's constructor takes a path via Bun.file(), a Uint8Array, a Blob, or anything the Bun file primitives return – including Bun.s3() references, which changed my code shape.

Step 3 – Map the chain. This is where Bun.Image earns its "Sharp-compatible" framing. The methods I had in production all mapped 1:1: .resize(w, h, { fit: "cover" }) is identical, .rotate(90) works (with the rotation caveat below), .flip() and .flop() are the same, .modulate({ brightness, saturation }) is the same. The format terminals – .webp({ quality }), .jpeg({ quality }), .png(), .avif(), .heic() – are all present.

Step 4 – Swap the terminal. Sharp's .toBuffer() becomes Bun.Image's .toBuffer() (returns Uint8Array, not Buffer – important if you're passing it to something that type-checks Buffer). Sharp's .toFile(path) becomes .write(path). The lazy pipeline contract is identical: nothing executes until you await the terminal.

Here's the actual diff from one route handler:

ts
1// before
2import sharp from "sharp";
3
4export async function processCover(input: Uint8Array) {
5 const buf = await sharp(input)
6 .resize(1200, 630, { fit: "cover" })
7 .webp({ quality: 82 })
8 .toBuffer();
9 return buf;
10}
11
12// after
13export async function processCover(input: Uint8Array) {
14 const buf = await Bun.image(input)
15 .resize(1200, 630, { fit: "cover" })
16 .webp({ quality: 82 })
17 .toBuffer();
18 return buf;
19}

That's the whole swap for the common case. One import line, one constructor call.

After the migration, bun pm ls | grep sharp returns empty. The CI Dockerfile drops its RUN apk add --no-cache vips-dev line. The resulting image is ~80MB smaller. The package.json shrinks by one dependency and one peer-dep warning.

Three things that don't map cleanly yet

Be honest with yourself about these before you start ripping Sharp out, because at least one of them will bite if your pipeline isn't pure resize-and-encode.

Gotcha 1 – ICC color profile pass-through. Sharp's .withMetadata({ icc: "p3" }) preserves the input color profile through the encode. Bun.Image, as of 1.3.14, strips ICC. For sRGB-in, sRGB-out workflows – most web image work – this is invisible. For photography pipelines where someone uploads a wide-gamut Display-P3 image and expects it preserved, Sharp still wins. The workaround if you must use Bun.Image: read the ICC chunk with exifr, encode, then re-attach manually. It's not pretty.

Gotcha 2 – Animated WebP and GIF frames. Bun.Image decodes the first frame of an animated input and drops the rest. Sharp's { animated: true } + per-frame access has no equivalent. If you do sprite-sheet processing, animated thumbnail generation, or anything that walks frames, this is a hard wall. Keep Sharp on those code paths.

Gotcha 3 – The .tile() pyramid. Sharp inherits libvips's deepzoom / IIIF tile generation. If you ship a Leaflet-style image server, a map tile pipeline, or a museum-grade zoom UI, this isn't optional. Bun.Image has no tile primitive and probably won't for a while – libvips is decades of accumulated work and the Bun team will prioritize the common path first.

One smaller gotcha worth flagging: Sharp's .rotate(45) does arbitrary-degree rotation with bilinear interpolation. Bun.Image's .rotate() accepts 90, 180, and 270 only. For 99% of cover-image and product-thumbnail work this is a non-issue. If you do skew correction or aesthetic tilt effects, it's a blocker.

The shape of code I'm now running for workloads that hit any of the above is a dual-stack pattern – Bun.Image for the common path, Sharp pinned in a worker thread only for the problem cases:

ts
1async function process(input: Uint8Array, meta: ImageMeta) {
2 if (meta.hasICC || meta.isAnimated || meta.needsTile) {
3 const sharp = (await import("sharp")).default;
4 return sharp(input)
5 .resize(1200, 630, { fit: "cover" })
6 .webp({ quality: 82 })
7 .toBuffer();
8 }
9 return Bun.image(input)
10 .resize(1200, 630, { fit: "cover" })
11 .webp({ quality: 82 })
12 .toBuffer();
13}

Dynamic import keeps Sharp out of the bundle for deploy targets that never hit the slow path.

The CI and cold-start numbers

The install-time delta is the receipt I care about most, because CI minutes compound.

On my Ubuntu x86_64 CI runner, bun install with Sharp pinned was 4.8s warm. After removing Sharp from package.json, 1.4s warm. The savings come from skipping Sharp's prebuilt-binary download and the optional libvips system dependency probe.

Cold install (no ~/.bun/install/cache, no node_modules) went from 18.2s to 7.1s. Removing one native addon from a tree of 200 packages doesn't usually move install time this much – the outsized drop comes from Sharp's postinstall being the slowest single step in the tree.

Bun 1.3.14 also ships the isolated linker's global store, which the release notes call "7x faster warm installs" project-wide. Combined with the Sharp removal, my full bun install warm cycle on the admin repo dropped from 6.4s to 1.1s. That's the kind of change you feel during local dev: bun add some-package stops being a coffee break.

Docker images shrank by about 80MB after removing Sharp's prebuilt binary plus the vips-dev Alpine package the prebuild fallback wanted. Layer cache hits in CI go up because the layer below the install step stays stable across more dependency churn – Sharp's prebuilt binary download was one of the noisier cache-invalidation triggers.

I keep my dev toolchain tight in general – the same instinct led me to ship Claude Code 2.1.141 hooks the day they landed – and the cumulative effect of small toolchain wins is what makes a solo shop competitive. A 5-second-faster bun install doesn't sound like much. Multiply by 80 commits a week.

Per-image latency was the number I was most nervous about. On a representative resize – 1024×1024 PNG to 512×512 WebP at quality 82 – Sharp 0.34.2 and Bun.Image landed within 8% of each other on my local M2. Neither delta matters for a web workload.

The architectural reason Bun.Image is competitive on raw resize, despite being eighteen months old vs. libvips's two decades: the i16 fixed-point SIMD resize kernels, plus JPEG IDCT scaling to the smallest sufficient size during decode. You don't decode a 4000×4000 JPEG to a full bitmap and then resize – Bun.Image decodes it at the target resolution directly. Sharp does the same trick via libjpeg-turbo, which is why both pipelines land in the same neighborhood.

Memory is where Bun.Image pulls ahead noticeably: zero-copy ArrayBuffer borrowing keeps peak RSS lower than Sharp on batches of 50+ images. If you process galleries in a single worker invocation, this matters. If you process one image per request, the win is invisible.

When Sharp still wins, and when to migrate

Keep Sharp if any of these apply:

  • You need animated WebP frame-by-frame access.
  • You're preserving ICC color profiles for wide-gamut photography work.
  • You depend on the .tile() deepzoom pyramid for image-server or map-tile workloads.
  • You need arbitrary-angle rotation with interpolation.
  • You're running on Node-only deploy targets – Vercel Node functions, AWS Lambda's Node runtime, Cloudflare Workers (no Bun there yet) – where Bun isn't an option.

Migrate to Bun.Image if all of these apply:

  • You're already on Bun runtime in at least one tier.
  • Your image work is "decode, resize, re-encode" in JPEG, PNG, WebP, AVIF, or HEIC.
  • Your CI is sick of Sharp prebuilt-binary rebuilds, or you ship Alpine containers and have hit the libvips system-dep wall.

The honest take in May 2026: Bun.Image is at "95% of common-case Sharp" with a much smaller install footprint and zero native-addon ceremony. The remaining 5% is where Sharp's libvips depth still earns its keep, and you should plan a dual-stack period – don't rip Sharp out everywhere on day one. Migrate the routes that match Bun.Image's sweet spot, keep Sharp on the routes that don't, and re-evaluate at Bun 1.4.x when arbitrary rotation and animated frames probably land.

Watch list for upgrades: arbitrary rotation, animated WebP frame access, and ICC pass-through are the three features most likely to ship next given the Rust-rewrite engineering velocity. Subscribe to the Bun changelog and re-audit your dual-stack split every minor.

The exact migration commit I'd ship

Here's one production route after the swap, with the error handling and the engines pin:

ts
1// package.json
2// "engines": { "bun": ">=1.3.14" }
3
4import { Hono } from "hono";
5
6const app = new Hono();
7
8app.post("/api/uploads", async (c) => {
9 const form = await c.req.formData();
10 const file = form.get("file");
11 if (!(file instanceof File)) {
12 return c.json({ error: "no file" }, 400);
13 }
14
15 const input = new Uint8Array(await file.arrayBuffer());
16
17 try {
18 const webp = await Bun.image(input)
19 .resize(1200, 630, { fit: "cover" })
20 .webp({ quality: 82 })
21 .toBuffer();
22
23 const jpeg = await Bun.image(input)
24 .resize(1200, 630, { fit: "cover" })
25 .jpeg({ quality: 84 })
26 .toBuffer();
27
28 await Bun.s3().write(`covers/${crypto.randomUUID()}.webp`, webp);
29 await Bun.s3().write(`covers/${crypto.randomUUID()}.jpg`, jpeg);
30
31 return c.json({ ok: true });
32 } catch (err) {
33 return c.json({ error: String(err) }, 500);
34 }
35});
36
37export default app;

Three things worth pinning explicitly:

The "engines": { "bun": ">=1.3.14" } line in package.json is load-bearing. Bun.Image was added in 1.3.14 – earlier versions fail at runtime with Bun.image is not a function, and you want that to surface as an install error, not a 500 in production.

The bun-types package (the @types/bun replacement) ships Bun.Image typings starting in 1.3.14. tsc --noEmit passes without @ts-expect-error shims. If your editor still red-squiggles Bun.image, your bun-types is pinned too low.

Rollback plan: keep sharp in optionalDependencies for one release cycle, with the dynamic-import dual-stack from the Gotchas section as the fallback path. After a week of green production metrics, remove sharp from optionalDependencies and delete the fallback branch. Don't do both in the same commit. Don't do them in the same week if you're paranoid.

The test for whether a Bun feature is production-ready isn't the changelog claim. It's whether you'd put it in your own commit. This one ships.

Key Takeaways

  • Bun 1.3.14 (May 13, 2026) ships Bun.Image, a chainable image pipeline with Sharp-compatible API and zero native-addon install step.
  • The migration is a four-step swap: audit Sharp imports, replace the constructor with Bun.image(), map the chain methods 1, swap the terminal to .toBuffer() or .write().
  • Three features don't map yet: ICC color profile pass-through, animated WebP frame access, and the .tile() deepzoom pyramid. Keep Sharp on those routes via dynamic import.
  • CI install time dropped from 4.8s to 1.4s warm; Docker images shrank ~80MB; per-image latency stayed within 8% of Sharp.
  • Pin "engines": { "bun": ">=1.3.14" }, keep Sharp in optionalDependencies for one release cycle as a rollback, then delete.
Last Updated

May 14, 2026

Category

Dev

Omid Saffari

Founder & CEO, AI Entrepreneur

Digital marketing specialist with expertise in AI, automation, and web development. Helping businesses build strong online presences that drive results.

X.com
Instagram
LinkedIn
WhatsApp
Email

More from Dev

v2.1.141 Quietly Killed My Three Worst Claude Code Hook Bugs. Here's the Exact settings.json.
v2.1.141 Quietly Killed My Three Worst Claude Code Hook Bugs. Here's the Exact settings.json.

A practical upgrade guide for senior devs already running Claude Code hooks in production. Walks through the three concrete fixes that landed in v2.1.139 to v2.1.141 (terminalSequence for headless notifications, args:string[] for…

May 14, 2026
View all Dev articles