חזרה לבלוג

Bun.Image

Bun 1.3.14 שחרר pipeline לעיבוד תמונה ישר בתוך ה-runtime — בלי dependencies מ-npm, בלי native build, JPEG/PNG/WebP/AVIF ו-ThumbHash placeholders ב-API שרשורי אחד. מה זה עושה ואיפה זה בולט.

bun javascript images performance tooling

מה חדש

Bun 1.3.14 (מאי 2026) שחרר את Bun.Image — pipeline שרשורי, lazy, לעיבוד תמונה שבנוי לתוך ה-runtime. דיקוד דרך libjpeg-turbo / spng / libwebp, גיאומטריה דרך SIMD, בלי שום dependency מ-npm, בלי native build step.

מה נתמך:

  • encode + decode — JPEG, PNG, WebP, בכל מקום.
  • decode בלבד — HEIC, AVIF, BMP, GIF, TIFF, איפה שמערכת ההפעלה תומכת.
  • קלט מה-clipboard — חינם ב-macOS וב-Windows.

כל ה-API נסוב סביב רעיון אחד: שרשרת שטוחה שנשארת lazy עד שעושים await על terminal call.

const bytes = await Bun.file("photo.jpg").image()
  .resize(800, undefined, { fit: "inside", withoutEnlargement: true })
  .webp({ quality: 80 })
  .bytes();

שום דבר לא מפוענח, שום דבר לא מקודד, אף פיקסל לא מועתק עד שעושים await על .bytes() (או .write(), או .blob(), או כל אחד מהטרמינלים האחרים).

השרשרת מתקמפלת למטה, רצה מחוץ ל-thread של ה-JS, ומחזירה את התוצאה.

למה זה משנה

כמה דברים בולטים ברגע שמתחילים להשתמש:

  • בלי dependencies מ-npm. זה ב-runtime — אין bun add, אין node_modules תפוח לעיבוד תמונה.
  • אותו בינארי בכל מקום. מק, CI, Linux ARM, Linux amd64. בלי platform flags, בלי rebuilds native, בלי פולחנים ב-Dockerfile.
  • AVIF פשוט עובד איפה שהמערכת תומכת, בלי setup של libheif / libaom.
  • Lazy כברירת מחדל. שום דבר לא מפוענח או מקודד עד שעושים await על terminal, וכל העבודה רצה מחוץ ל-thread של ה-JS.
  • מוכן ל-Response. מעבירים את השרשרת ישר ל-new Response(...) והאנקודר מחזיק ב-Content-Type.
  • ThumbHash מובנה. קריאה אחת מחזירה data URL מוכן ל-placeholder. בלי פקג’ נוסף.

זה אותו מהלך ש-Bun.serve עשה מול הצורך לגרור את Express: לא יכולת חדשה — פשוט בלי החיכוך.

הדמו

ביקשתי מקלוד לבנות לי דמו מהיר שמפעיל את ה-API באמת — ארבעה תרגילים: format showdown, שרת שעושה transforms תוך כדי, מחולל ThumbHash placeholders, ו-clipboard transformer ל-macOS. ה-repo: github.com/Nitzan94/bun-image-demo.

דמו 1 — format showdown

לוקחים תמונה אחת, מקודדים אותה ב-7 צורות, מדפיסים טבלה.

source: samples/photo1.jpg  (753 KB, 3000x2000, jpeg)

format     size        time     vs jpeg-q80
───────    ────────    ─────    ───────────
jpeg q80   835.9 KB    34.1ms   1.00x
jpeg q60   707.3 KB    31.8ms   0.85x
png        3.95 MB    594.2ms   4.84x
png q256   3.80 MB    877.9ms   4.65x
webp q80   599.5 KB   300.9ms   0.72x
webp loss  2.51 MB   1173.6ms   3.07x
avif q60   505.4 KB   284.3ms   0.60x

AVIF ב-q60 = 60% מ-JPEG q80, וזמן הקידוד תחרותי מול WebP. PNG עם palette quantization כמעט לא עזר בתמונות (צפוי — יותר מדי צבעים).

הפתעה אחת שימושית: קידוד מחדש של ה-JPEG המקורי ל-JPEG q80 הגדיל אותו מ-753 KB ל-836 KB. picsum כבר דחס אותו אגרסיבית מראש; ה”איכות הגבוהה” של ברירת המחדל הוסיפה בייטים חזרה. תמיד לבדוק לפני שמניחים שיותר q = יותר טוב.

דמו 2 — שרת transforms תוך כדי

route של 120 שורות ב-Bun.serve שמטפל ב-GET /img/<name>?w=&h=&fmt=&q=&rotate=&flip=&flop=&placeholder= — הגרעין של image CDN פנימי.

const out =
  fmt === "jpeg" ? img.jpeg({ quality }) :
  fmt === "png"  ? img.png() :
  fmt === "webp" ? img.webp({ quality }) :
                   img.avif({ quality });

return new Response(out);

שני דברים שכדאי לשים לב אליהם:

  1. Bun.Image זה body חוקי ל-Response. מעבירים את השרשרת ישר ל-new Response(...) — האנקודר מחזיק ב-MIME ו-content-type נקבע אוטומטית. אין boilerplate של headers: { "content-type": "image/webp" }, ומעבר בין פורמטים דרך query param פשוט עובד.
  2. כל הקידוד רץ מחוץ ל-thread של ה-JS. בקשות מקבילות לא חוסמות לך את ה-event loop על דיקוד JPEG.

ביקשתי גם frontend playground שמאפשר לשחק עם כל בקר דרך sliders, לראות סטטיסטיקות גודל/זמן חיות, להשוות למקור, ולעבור בין הגדרות פורמט/רוחב מוצעות בקליק על קלפים ב-grid השוואה.

Playground default view

Playground in AVIF mode with format strip and width ladder

ה-format strip (jpeg/webp/avif זה לצד זה) וה-width ladder (200/400/800/1600 של הפורמט הנבחר) שניהם רצים כ-fetches מקבילים — מזיזים בקר אחד ושלושת הפורמטים מקודדים מחדש במקביל, מהר יותר ממה שאפשר לקרוא את המספרים. זה הערך ב-dev loop ש-Bun.Image הופך לזמין בלי מאמץ.

Compare-to-source mode

זה הפיצ’ר הגנוב של ה-API. הדפוס של “preview מטושטש בזמן שהתמונה האמיתית נטענת” (Next.js Image, Medium) דורש בדרך כלל את הפקג’ thumbhash או blurhash + pipeline של Sharp שמייצר את ה-placeholders ב-build time.

Bun נותן מתודה .placeholder() שמחזירה URL של data:image/png;base64,... בקריאה אחת — בלר של ~32px מבוסס ThumbHash, בדרך כלל 100–1500 בייטים. שמים ישר ב-<img src=...>.

const lqip = await Bun.file("photo.jpg").image().placeholder();
// "data:image/png;base64,iVBORw0KGgoAAA…"  ← 200 בייטים, מוכן להטמעה

הסקריפט הולך על samples/, מייצר LQIPs, וכותב גלריית HTML עם דפוס ה-swap מהבלר לתמונה האמיתית. אם מצמצמים את הרשת ל-slow 3G ב-DevTools רואים את זה קורה.

ThumbHash gallery

דמו 4 — clipboard transformer ל-macOS

Bun.Image.fromClipboard() ב-macOS וב-Windows. עושים צילום מסך, מריצים alias של CLI, ומקבלים WebP בגודל הנכון על שולחן העבודה.

const img = Bun.Image.fromClipboard();
if (!img) { console.error("no image on clipboard"); process.exit(1); }

await img.resize(1200, undefined, { withoutEnlargement: true })
  .webp({ quality: 85 })
  .write(`~/Desktop/clip-${ts}.webp`);

צילומי מסך מ-iOS ו-Android הם 3000+ פיקסלים רוחב ו-2 MB. להדבקה ב-Slack, Linear, או PRs זה bandwidth מבוזבז ומפעיל אצל הצד השני את ה-spinner של “compressing image…”. הסקריפט הזה הופך את זה ל-alias bun clip.

גוצ’ה אחת שנתקלתי בה

Bun.Image.fromClipboard() מחזיר null כשאין תמונה ב-clipboard — הוא לא זורק. קל לפספס כי התיעוד מנסח את זה כ-”from clipboard”, לא כ-”from clipboard if any”. תמיד if (!img) … לפני שרשור.

מה יש ב-playground (החלק האינטראקטיבי)

ה-route ב-/playground הוא החלק שהייתי באמת פותחת ביום-יום:

  • Source picker — תמונות ממוזערות לחיצות בראש העמוד. לחיצה מחליפה מקור; כל מה שלמטה מקודד מחדש.
  • בקרים — width, height, format (jpeg/png/webp/avif), quality, rotate, flip, flop. תצוגה חיה עם debounce של 80ms.
  • סטטיסטיקות חיות — size, % מהמקור, roundtrip ms, ו-Content-Type בפלט.
  • Format strip — אותו מקור ו-resize מקודדים כ-jpeg/webp/avif במקביל. הקטן ביותר מקבל מסגרת פנימית צהובה (“best”); הנבחר מקבל מסגרת ציאן (“active”). לחיצה על קלף = אימוץ הפורמט שלו.
  • Width ladder — הפורמט הנוכחי מקודד ב-200/400/800/1600. לחיצה על קלף = הצמדת ה-width לערך שלו — ה-srcset שלך בתצוגה חיה.
  • Compare-to-source — בקר שמפצל את התצוגה ל-מקור-מול-מתורגם להשוואת איכות.
  • Download — השרת קובע Content-Disposition: attachment; filename="photo1-400w-q70.webp" והכפתור מפעיל הורדה אמיתית.
  • שמירת state ב-URL hash — כל שינוי בקר מעדכן את location.hash. מעתיקים את ה-URL, משתפים, נוחתים על אותה תצורה.

כל זה הוא קובץ Bun.serve אחד פלוס frontend של 200 שורות vanilla-JS. בלי build step. בלי bundler. בלי dev server. מריצים bun run server.ts ופותחים localhost:3000/playground.

איפה זה באמת עוזר בפיתוח

שישה use cases אמיתיים, מסודרים בערך לפי כמה פעמים באמת רציתי אותם:

Upload pipelines. משתמש מעלה תמונה של 12 מגה-פיקסל מהפלאפון; את רוצה WebP ברוחב מקסימלי של 1600px ב-q85 ב-storage. שלוש שורות, בלי התקנות, אותו פלט בכל מקום ש-Bun רץ:

await Bun.file(uploaded).image()
  .resize(1600, undefined, { fit: "inside", withoutEnlargement: true })
  .webp({ quality: 85 })
  .write(`storage/${id}.webp`);

LQIP / blur placeholders. .placeholder() הוא חיסכון הזמן האמיתי כאן. שווה להשתמש בו גם אם נשארים עם stack התמונות הקיים — להכניס אותו רק בשביל זה.

תמונות responsive תוך כדי. בדיוק מה ש-server.ts עושה. במקום לייצר חמישה גדלים מראש לכל תמונה ולשלוח ל-S3, מגישים קובץ master אחד ונותנים ל-?w=800&fmt=avif להיפתר ב-request time, עם cache ב-CDN לפי URL.

כל הקטגוריה של “image CDN” (imgix, Cloudinary, imgproxy) זה בעצם הדפוס הזה כשירות. לכלים פנימיים או אפליקציות קטנות, route של 120 שורות מחליף את ה-SaaS.

עיבוד נכסים ב-build time. כל מקום שהיית מגיעה אליו ל-sharp ב-build script:

  • ייצור OG cards
  • סולמות favicons
  • social previews
  • צילומי מסך ל-README שמשנים גודל לפני commit
  • app icons ממקור של 1024px

תהליך עבודה של צילומי מסך. דמו 4. ⌘⇧4-ctrl לצילום, alias bun image-paste ב-shell, WebP בגודל הנכון מוכן לגרור לכל מקום.

Format A/B testing ב-CI. הדפוס של showdown.ts, scripted. “השינוי האחרון בעיצוב פוצץ את תקציבי התמונות?” — מקודדים את כל הנכסים ב-repo, משווים ל-baseline של השבוע שעבר, נכשלים אם הסך גדל ב-10% או יותר.

קשה לעשות את זה בקלילות לפני Bun.Image כי המס של ה-tooling היה גבוה מדי לבדיקה חד-פעמית.

איפה זה (עדיין) לא הכלי הנכון

ניהול צבע עם ICC profiles, compositing מתוחכם או טקסט overlay, עבודה עם פריימים של GIF/WebP מונפש, פענוח raw של NEF/CR2/DNG. בעבודת web טיפוסית כמעט לא נוגעים בזה; עורכי תמונות ו-workflows של דפוס עדיין צריכים ספריית תמונות ייעודית.

הדפוס

האסטרטגיה של Bun היא ה-runtime זה ה-framework. Bun.serve ל-HTTP. bun:sqlite ל-SQLite. Bun.sql ל-Postgres. Bun.Image לתמונות.

היכולת שהיית מתקינה היא עכשיו מתודה על משהו שכבר יש לך. ל-90% מעבודת ה-web זו עסקה טובה יותר — פחות פקג’ים, פחות build flags, אותו פלט בכל מכונה שהקוד רץ עליה.

clone ל-repo, להריץ bun run server.ts, לפתוח את ה-playground. תדעי בחמש דקות אם זה מתאים ל-stack שלך.

Repo: github.com/Nitzan94/bun-image-demo תיעוד Bun: bun.com/docs/runtime/image