معالجة الصور باستخدام Bun: الانتقال من Sharp إلى Bun.Image في 4 خطوات

دليل عملي للانتقال من Sharp إلى Bun.Image في 4 خطوات، مع مقارنة الأداء وقيود ICC وWebP المتحرك وأرقام CI التي توضح متى تكون الهجرة مناسبة للإنتاج.

Saturday, September 5, 2026Omid Saffari
معالجة الصور باستخدام Bun: الانتقال من Sharp إلى Bun.Image في 4 خطوات

أطلق Bun الإصدار v1.3.14 في 13 مايو 2026، وقدّم معه Bun.Image، وهو مسار لمعالجة الصور باستخدام Bun يعتمد على libjpeg-turbo + spng + libwebp، ويحاكي واجهة Sharp ويعمل من دون أي خطوة لبناء إضافة أصلية. بعد ثلاثة أعوام كان فيها CI يتعطل بسبب ملف libvips الثنائي الخاص بـ lovell/sharp كلما حدّثت Node، كان هذا هو الإصدار الذي دفعني أخيراً إلى إزالة Sharp.

لماذا اخترت معالجة الصور باستخدام Bun وتركت Sharp فور صدور 1.3.14

وصلت ملاحظات إصدار Bun 1.3.14 في 13 مايو بالقائمة المعتادة من النقاط، وبين «عميل HTTP/3» و«تثبيتات دافئة أسرع بمقدار 7x» اختبأ السطر الذي أنهى اعتمادي على Sharp: Bun.Image، وهو مسار متسلسل لمعالجة الصور مدمج في بيئة التشغيل، مع libjpeg-turbo وspng وlibwebp مضمّنة مباشرة في ملف Bun الثنائي.

إن لم يسبق أن أضعت يوماً كاملاً في CI بسبب Sharp، فيمكنك تجاوز هذه الفقرة. أما إن مررت بذلك، فأنت تعرف السيناريو: تعذّر العثور على sharp/lib/sharp-linuxmusl-x64.node، أو ظهر الخطأ Cannot find module '../build/Release/sharp.node' داخل حاوية Alpine. ثم تُبطَل ذاكرة طبقات Docker المؤقتة لأن أحدهم حدّث Node، فيبدأ npm rebuild sharp من الصفر مع كل عملية دفع. أو يصل بناء Vercel إلى شبكة CDN المخصّصة للملفات الثنائية الجاهزة ثم تنتهي مهلة الاتصال. تكرر ذلك على مدى ثلاثة أعوام وفي ثلاثة مشاريع منفصلة، حتى وجدت نفسي أكتب apk add --no-cache vips-dev تلقائياً من فرط الاعتياد.

Bun.Image هو الحل المدمج أصلاً في بيئة التشغيل. لا توجد خطوة npm install لمعالجة الصور، لأن برامج الترميز تأتي داخل ملف Bun الثنائي. ولا توجد إضافة أصلية تحتاج إلى إعادة بناء عند تغير ABI الخاص بـ Node، إذ لا وجود هنا لـ Node ولا للإضافة. أما أنوية المعالجة الهندسية فتستخدم SIMD بنقطة ثابتة i16، ويجري فك ترميز JPEG تلقائياً بأصغر حجم كافٍ. من حيث البنية، هذا هو الشكل الذي كان يمكن أن يكون عليه Sharp لو لم يضطر إلى العمل كإضافة لـ Node.

هناك سبب آخر جعل 1.3.14 مهماً بالنسبة إليّ: إنه آخر إصدار مبني بلغة Zig قبل وصول إعادة الكتابة بلغة Rust والمموّلة من Anthropic. غطّت The Register وتيرة الدمج في 14 مايو، ومن هنا فصاعداً ستتسارع وتيرة التطوير بوضوح. أفضل الانتقال الآن إلى مكوّن أصلي في Bun بدلاً من حمل تبعية لإضافة أصلية خلال إعادة كتابة بيئة التشغيل.

هذه ليست مرثية لـ Sharp. فما زال Sharp المبني على libvips هو الأسرع مع WebP المتحرك، وأعمال التصوير التي تتطلب دقة شديدة في ملفات تعريف الألوان، وهرم deepzoom الذي تنشئه tile(). ما يلي هو خطة العمل المناسبة لنسبة 95% من مهام الصور التي لا تندرج تحت هذه الحالات الثلاث: مسار فك الترميز وتغيير الحجم وإعادة الترميز الذي تستخدمه معظم تطبيقات الإنتاج.

الانتقال في 4 خطوات ضمن مسار صور الغلاف

طبّقت هذا الاستبدال في omidsaffari-admin، وهو العامل الذي يعالج مخرجات أغلفة gpt-image-2 بعد إنشائها وقبل وصولها إلى R2. كان الملف الواحد يضم ثمانية مواضع تستدعي Sharp، وكلها تقع في المسار الذي يعمل بعد خطوة cover في PublishWorkflow. استغرقت عملية الانتقال كاملة 42 دقيقة، بما في ذلك مسار الترميز المزدوج WebP مع JPEG كخيار احتياطي.

الخطوة 1 – احصر جميع مواضع استخدام Sharp. قبل تعديل أي شيء، ابحث عن كل عملية استيراد:

Bash
rg -n "from ['\"]sharp['\"]" src/
rg -n "require\(['\"]sharp['\"]\)" src/

المهم أن تعرف مسبقاً هل ستستبدل أربعة مواضع استدعاء أم أربعين. إن كانت أربعين، فنفّذ الانتقال مساراً بعد آخر، لا دفعة واحدة.

الخطوة 2 – استبدل الاستيراد بـ Bun.file().image(). يقبل مُنشئ Sharp مساراً أو Buffer أو Stream. أما مُنشئ Bun.Image فيقبل مساراً عبر Bun.file()، أو Uint8Array، أو Blob، أو أي قيمة تعيدها أدوات الملفات في Bun، بما في ذلك مراجع Bun.s3() التي غيّرت شكل شيفرتي.

الخطوة 3 – طابق سلسلة الاستدعاءات. هنا يستحق Bun.Image وصفه بأنه «متوافق مع Sharp». كل الأساليب التي استخدمتها في الإنتاج وجدت مقابلاً مطابقاً بنسبة 1:1: تعمل .resize(w, h, { fit: "cover" }) بالطريقة نفسها، وتعمل .rotate(90) أيضاً مع ملاحظة التدوير أدناه، وكذلك .flip() و.flop()، وينطبق الأمر نفسه على .modulate({ brightness, saturation }). وجميع نقاط إنهاء التنسيق متاحة: .webp({ quality }) و.jpeg({ quality }) و.png() و.avif() و.heic().

الخطوة 4 – استبدل نقطة الإنهاء. تتحول .toBuffer() في Sharp إلى .toBuffer() في Bun.Image، لكنها تعيد Uint8Array لا Buffer، وهذه نقطة مهمة إن كنت تمرر الناتج إلى جهة تتحقق من النوع Buffer. وتتحول .toFile(path) في Sharp إلى .write(path). يظل عقد التنفيذ الكسول كما هو: لن يحدث شيء حتى تستدعي نقطة الإنهاء وتنتظر نتيجتها.

إليك الفرق الفعلي من أحد معالجات المسارات:

TypeScript
// before
import sharp from "sharp";

export async function processCover(input: Uint8Array) {
  const buf = await sharp(input)
    .resize(1200, 630, { fit: "cover" })
    .webp({ quality: 82 })
    .toBuffer();
  return buf;
}

// after
export async function processCover(input: Uint8Array) {
  const buf = await Bun.image(input)
    .resize(1200, 630, { fit: "cover" })
    .webp({ quality: 82 })
    .toBuffer();
  return buf;
}

هذا هو الاستبدال كله في الحالة الشائعة: سطر استيراد واحد واستدعاء واحد للمُنشئ.

بعد الانتقال، لا يعيد bun pm ls | grep sharp أي نتيجة. ويسقط من Dockerfile الخاص بـ CI السطر RUN apk add --no-cache vips-dev. كما تصبح صورة Docker الناتجة أصغر بنحو 80MB، ويفقد package.json تبعية واحدة وتحذيراً واحداً متعلقاً بـ peer-dep.

ثلاث نقاط لا تنتقل بسلاسة حتى الآن

قيّم هذه النقاط بصدق قبل البدء في اقتلاع Sharp، لأن واحدة منها على الأقل ستسبب لك مشكلة إن لم يكن مسارك مقتصراً على تغيير الحجم وإعادة الترميز.

المأزق 1 – تمرير ملف تعريف الألوان ICC. تحافظ .withMetadata({ icc: "p3" }) في Sharp على ملف تعريف ألوان الإدخال خلال الترميز. أما Bun.Image في الإصدار 1.3.14 فيزيل ICC. لا يظهر أثر لذلك في مسارات sRGB-in وsRGB-out، وهي غالبية أعمال صور الويب. لكن إن كان مسار التصوير يستقبل صورة ذات نطاق لوني واسع بتنسيق Display-P3 ويتوقع الحفاظ عليه، فما زال Sharp يتفوق. وإذا كان استخدام Bun.Image ضرورياً، فالحل البديل هو قراءة كتلة ICC باستخدام exifr، ثم الترميز، وبعد ذلك إرفاقها يدوياً من جديد. وهو حل غير أنيق.

المأزق 2 – إطارات WebP وGIF المتحركة. يفك Bun.Image الإطار الأول من الإدخال المتحرك ويتجاهل البقية. لا يوجد مقابل في Bun.Image لخيار { animated: true } في Sharp مع الوصول إلى كل إطار. وإذا كنت تعالج sprite sheets أو تنشئ صوراً مصغرة متحركة أو تنفذ أي مهمة تمر على الإطارات، فهذا حاجز قاطع. أبقِ Sharp في مسارات الشيفرة تلك.

المأزق 3 – هرم .tile(). يرث Sharp من libvips إنشاء مربعات deepzoom / IIIF. إن كنت تشغّل خادم صور بأسلوب Leaflet، أو مساراً لمربعات الخرائط، أو واجهة تكبير بجودة متحفية، فهذه ليست ميزة اختيارية. لا يملك Bun.Image أداة للمربعات، وعلى الأرجح لن يملكها قريباً؛ فـ libvips حصيلة عقود من العمل، بينما سيعطي فريق Bun الأولوية للمسار الشائع.

وهناك مأزق أصغر يستحق التنبيه: تنفّذ .rotate(45) في Sharp تدويراً بأي زاوية مع استيفاء ثنائي الخطوط. أما .rotate() في Bun.Image فلا يقبل سوى 90 و180 و270. لا يمثل ذلك مشكلة في 99% من أعمال صور الأغلفة والصور المصغرة للمنتجات، لكنه يصبح عائقاً إذا كنت تصحح الميل أو تضيف تأثيرات إمالة جمالية.

في المهام التي تقع ضمن أي من الحالات السابقة، أستخدم حالياً نمطاً مزدوجاً: Bun.Image للمسار الشائع، وSharp مثبتاً داخل worker thread للحالات الخاصة فقط:

TypeScript
async function process(input: Uint8Array, meta: ImageMeta) {
  if (meta.hasICC || meta.isAnimated || meta.needsTile) {
    const sharp = (await import("sharp")).default;
    return sharp(input)
      .resize(1200, 630, { fit: "cover" })
      .webp({ quality: 82 })
      .toBuffer();
  }
  return Bun.image(input)
    .resize(1200, 630, { fit: "cover" })
    .webp({ quality: 82 })
    .toBuffer();
}

يحافظ الاستيراد الديناميكي على خروج Sharp من الحزمة في أهداف النشر التي لا تمر أبداً بالمسار البطيء.

أرقام CI وبدء التشغيل البارد

الفارق في زمن التثبيت هو الدليل الأهم بالنسبة إليّ، لأن دقائق CI تتراكم.

على مشغّل CI لدي بنظام Ubuntu ومعمارية x86_64، استغرق bun install مع تثبيت Sharp على إصدار محدد مدة 4.8s في التثبيت الدافئ. وبعد إزالة Sharp من package.json، انخفض الزمن إلى 1.4s. يأتي الوفر من تجاوز تنزيل الملف الثنائي الجاهز لـ Sharp، وكذلك فحص تبعية libvips الاختيارية على مستوى النظام.

أما التثبيت البارد، من دون ~/.bun/install/cache ومن دون node_modules، فانخفض من 18.2s إلى 7.1s. عادةً لا تؤثر إزالة إضافة أصلية واحدة من شجرة تضم 200 حزمة بهذا القدر في زمن التثبيت؛ ويرجع هذا الانخفاض الكبير إلى أن postinstall الخاص بـ Sharp كان أبطأ خطوة منفردة في الشجرة.

يتضمن Bun 1.3.14 أيضاً المخزن العام لأداة الربط المعزولة، وتصفه ملاحظات الإصدار بأنه يحقق «تثبيتات دافئة أسرع بمقدار 7x» على مستوى المشروع. وبالجمع بين ذلك وإزالة Sharp، هبطت دورة bun install الدافئة الكاملة في مستودع الإدارة لدي من 6.4s إلى 1.1s. هذا نوع التغيير الذي تشعر به أثناء التطوير المحلي: لم يعد bun add some-package استراحة لاحتساء القهوة.

تقلصت صور Docker بنحو 80MB بعد إزالة ملف Sharp الثنائي الجاهز، إلى جانب حزمة Alpine المسماة vips-dev التي كان يحتاج إليها خيار البناء الاحتياطي. كما ازدادت مرات الاستفادة من ذاكرة الطبقات المؤقتة في CI، لأن الطبقة الواقعة أسفل خطوة التثبيت تظل مستقرة مع نطاق أوسع من تغييرات التبعيات؛ وكان تنزيل الملف الثنائي الجاهز لـ Sharp من أكثر مسببات إبطال الذاكرة المؤقتة إزعاجاً.

أحرص عموماً على إبقاء سلسلة أدوات التطوير رشيقة؛ وهي الغريزة نفسها التي دفعتني إلى نشر hooks في Claude Code 2.1.141 يوم صدورها. والأثر التراكمي للمكاسب الصغيرة في الأدوات هو ما يمنح المشروع الفردي قدرة تنافسية. قد لا يبدو تسريع bun install بمقدار 5 ثوانٍ أمراً مهماً، لكن اضربه في 80 عملية commit أسبوعياً.

كان زمن معالجة الصورة الواحدة أكثر رقم أثار قلقي. وفي اختبار ممثل لتغيير الحجم، من PNG بأبعاد 1024×1024 إلى WebP بأبعاد 512×512 وجودة 82، جاء أداء Sharp 0.34.2 وBun.Image بفارق لا يتجاوز 8% على جهاز M2 المحلي لدي. وفي الحالتين لا يؤثر هذا الفارق في عبء عمل خاص بالويب.

يرجع تنافس Bun.Image في تغيير الحجم الخام، رغم أن عمره ثمانية عشر شهراً فقط مقابل عقدين لـ libvips، إلى بنيته: نوى تغيير الحجم SIMD بنقطة ثابتة i16، إلى جانب تحجيم JPEG عبر IDCT إلى أصغر حجم كافٍ أثناء فك الترميز. فأنت لا تفك صورة JPEG بأبعاد 4000×4000 إلى صورة نقطية كاملة ثم تغيّر حجمها؛ بل يفكّها Bun.Image مباشرة بالدقة المستهدفة. وينفذ Sharp الحيلة نفسها عبر libjpeg-turbo، ولهذا يصل المساران إلى نطاق أداء متقارب.

في الذاكرة، يتقدم Bun.Image بوضوح: استعارة ArrayBuffer من دون نسخ تُبقي ذروة RSS أقل من Sharp عند معالجة دفعات من 50+ صورة. يهم ذلك إذا كنت تعالج معارض صور في استدعاء واحد للعامل. أما إذا كنت تعالج صورة واحدة لكل طلب، فلن ترى هذا المكسب.

متى يظل Sharp الخيار الأفضل، ومتى يحين الانتقال؟

احتفظ بـ Sharp إذا انطبق أي مما يلي:

  • تحتاج إلى الوصول إلى إطارات WebP المتحرك إطاراً بإطار.
  • تحافظ على ملفات تعريف الألوان ICC لأعمال التصوير ذات النطاق اللوني الواسع.
  • تعتمد على هرم deepzoom الذي تنشئه .tile() في خوادم الصور أو مسارات مربعات الخرائط.
  • تحتاج إلى تدوير بأي زاوية مع الاستيفاء.
  • تعمل على أهداف نشر تدعم Node فقط، مثل وظائف Vercel المبنية على Node، وبيئة Node في AWS Lambda، وCloudflare Workers التي لا تدعم Bun حتى الآن، بحيث لا يكون Bun خياراً.

انتقل إلى Bun.Image إذا انطبقت جميع النقاط التالية:

  • تستخدم بيئة تشغيل Bun بالفعل في طبقة واحدة على الأقل.
  • تقتصر مهام الصور لديك على «فك الترميز وتغيير الحجم وإعادة الترميز» ضمن JPEG أو PNG أو WebP أو AVIF أو HEIC.
  • أنهك CI لديك إعادة بناء ملفات Sharp الثنائية الجاهزة، أو تشغّل حاويات Alpine واصطدمت بعائق تبعية النظام libvips.

الخلاصة الصريحة في مايو 2026: يقدم Bun.Image نسبة «95% من حالات Sharp الشائعة»، مع حجم تثبيت أصغر بكثير ومن دون إجراءات الإضافات الأصلية. أما نسبة 5% المتبقية فهي المساحة التي ما زال فيها عمق libvips داخل Sharp يستحق الاحتفاظ به. لذا خطط لفترة تشغيل مزدوج، ولا تقتلع Sharp من كل مكان في اليوم الأول. انقل المسارات التي تقع ضمن نقطة قوة Bun.Image، وأبقِ Sharp في المسارات الأخرى، ثم أعد التقييم مع Bun 1.4.x حين يُرجّح وصول التدوير بأي زاوية والإطارات المتحركة.

قائمة الميزات التي ينبغي مراقبتها عند الترقية: التدوير بأي زاوية، والوصول إلى إطارات WebP المتحرك، وتمرير ICC. هذه الميزات الثلاث هي الأرجح أن تصل تالياً بالنظر إلى وتيرة العمل الهندسي في إعادة الكتابة بلغة Rust. اشترك في سجل تغييرات Bun، وأعد تدقيق تقسيم التشغيل المزدوج مع كل إصدار فرعي.

التعديل الدقيق الذي سأرسله للانتقال

إليك أحد مسارات الإنتاج بعد الاستبدال، متضمناً معالجة الأخطاء وتثبيت الحد الأدنى للإصدار في engines:

TypeScript
// package.json
// "engines": { "bun": ">=1.3.14" }

import { Hono } from "hono";

const app = new Hono();

app.post("/api/uploads", async (c) => {
  const form = await c.req.formData();
  const file = form.get("file");
  if (!(file instanceof File)) {
    return c.json({ error: "no file" }, 400);
  }

  const input = new Uint8Array(await file.arrayBuffer());

  try {
    const webp = await Bun.image(input)
      .resize(1200, 630, { fit: "cover" })
      .webp({ quality: 82 })
      .toBuffer();

    const jpeg = await Bun.image(input)
      .resize(1200, 630, { fit: "cover" })
      .jpeg({ quality: 84 })
      .toBuffer();

    await Bun.s3().write(`covers/${crypto.randomUUID()}.webp`, webp);
    await Bun.s3().write(`covers/${crypto.randomUUID()}.jpg`, jpeg);

    return c.json({ ok: true });
  } catch (err) {
    return c.json({ error: String(err) }, 500);
  }
});

export default app;

هناك ثلاث نقاط يجدر تثبيتها صراحةً:

السطر "engines": { "bun": ">=1.3.14" } في package.json أساسي. فقد أُضيف Bun.Image في 1.3.14، وتفشل الإصدارات الأقدم أثناء التشغيل بالخطأ Bun.image is not a function. والمطلوب أن يظهر ذلك كخطأ تثبيت، لا كخطأ 500 في الإنتاج.

توفر حزمة bun-types، البديلة عن @types/bun، تعريفات Bun.Image بدءاً من 1.3.14. ويمر tsc --noEmit من دون حلول التفافية باستخدام @ts-expect-error. إذا ظل محررك يضع خطاً أحمر متعرجاً تحت Bun.image، فإصدار bun-types المثبت لديك أقدم من اللازم.

خطة التراجع: أبقِ sharp ضمن optionalDependencies لدورة إصدار واحدة، واستخدم نمط التشغيل المزدوج بالاستيراد الديناميكي الوارد في قسم المآزق كمسار احتياطي. بعد أسبوع من استقرار مؤشرات الإنتاج، أزل sharp من optionalDependencies واحذف الفرع الاحتياطي. لا تنفذ الخطوتين في commit واحدة، ولا تنفذهما في الأسبوع نفسه إن كنت شديد الحذر.

اختبار جاهزية ميزة في Bun للإنتاج ليس ما يدعيه سجل التغييرات، بل استعدادك لوضعها في commit من عملك. وهذه الميزة جاهزة للإرسال.

هل تعمل Bun.Image خارج Bun، أي في Node.js؟

لا. Bun.Image مكوّن مدمج في بيئة التشغيل، وليس حزمة npm. إذا كنت تحتاج إلى بديل محمول لـ Sharp يعمل على Node وBun، فانظر إلى حزم خارجية مثل bun-image-turbo أو واصل استخدام Sharp.

هل تُعد واجهة API بديلاً مباشراً لـ Sharp أم أنها مستوحاة منها فقط؟

صُمم شكل السلسلة وأسماء الأساليب عمداً لتتوافق مع Sharp: المُنشئ → .resize / .rotate / .flip / .modulate → نقطة إنهاء .webp / .jpeg / .png / .avif. يكفي في معظم مواضع الاستدعاء استبدال سطر الاستيراد. أما الفروق الأربعة فهي التدوير بأي زاوية، والإطارات المتحركة، وتمرير ICC، و.tile().

ما الذي تستخدمه Bun.Image خلف الكواليس؟

تستخدم libjpeg-turbo لفك JPEG وترميزه، وspng لـ PNG، وlibwebp لـ WebP وAVIF، إلى جانب نوى Bun الهندسية الخاصة بتقنية SIMD لتغيير الحجم بنقطة ثابتة i16. وكل ذلك مضمّن في ملف Bun الثنائي، بلا إضافة أصلية ولا خطوة إعادة بناء.

كيف يقارن أداء Bun.Image بأداء Sharp عند تغيير الحجم؟

في الحالة الشائعة لتغيير حجم JPEG/PNG وإعادة ترميزه، يبقى الفارق بينهما في حدود 8% تقريباً على الأجهزة المحلية. يظل libvips في Sharp أسرع عند بث الصور الكبيرة جداً وفي أحمال الصور المتحركة. أما مكسب Bun.Image الأكبر فهو زمن التثبيت والذاكرة، لا استهلاك CPU الخام عند تغيير حجم صورة واحدة.

هل ينبغي أن أنتقل اليوم؟

نعم، إذا كنت تستخدم بيئة Bun وكان مسار الصور لديك يقتصر على فك الترميز وتغيير الحجم وإعادة الترميز ضمن JPEG/PNG/WebP/AVIF. أما إذا كنت تعتمد على إطارات WebP المتحرك أو الحفاظ على ملفات تعريف الألوان ICC أو deepzoom عبر .tile()، فأبقِ Sharp في تلك المسارات وشغّل النظامين معاً.

آخر تحديث

5 سبتمبر 2026

التصنيفBuild

فضّل هذا الموقع في Google

إضافة omidsaffari.com كمصدر مفضّل في بحث Google

اجعل omidsaffari.com مصدرًا مفضّلًا، وسيرفعه Google لك في Top Stories وAI Overviews وAI Mode.

المزيد من Build

عرض كل مقالات Build
النشرة البريدية

رسالة واحدة، كل يوم أحد. أنظمة تعمل، لا آراء ساخنة.

سجلات بناء، وأنظمة قيد التشغيل، وملاحظات ميدانية من إدارة محفظة مشاريع ذكاء اصطناعي.

أسبوعية. بلا إزعاج. يمكنك إلغاء الاشتراك متى شئت.