تشغيل خادم وسائط باستخدام ميزات Bun المدمجة بدلاً من Sharp في Node.js
TuBrief 편집팀
2026년 8월 22일
0
Computing/Software원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
في بعض الأحيان، قد تجد أن صورة Docker التي تعمل بشكل جيد محلياً تواجه خطأ في البناء على خادم النشر. إذا بحثت عن السبب، ستجد أن الجاني في الغالب هو مكتبة sharp ومكتبة libvips اللتين تستخدمان ربط C++ الأصلي. وإذا نظرت إلى رسوم اشتراك Cloudinary أو Imgix التي تقتطع عشرات الدولارات شهرياً، فستشعر بالتساؤل عن سبب تحمل كل هذا العناء لمجرد إضافة ميزة معالجة صور بسيطة.
فيما يلي ملخص لطريقة الانتقال العملية لإنشاء مسار معالجة صور باستخدام الميزات المدمجة لبيئة تشغيل Bun الفردية وحدها، دون خطوات تجميع C++ الخارجية، مما يقلل من حجم الحاوية ويوفر التكاليف السحابية.
عند استخدام sharp في بيئة Node.js، فإنه يتصل ديناميكياً بمكتبات C الخاصة بنظام التشغيل عبر طبقة node-gyp و N-API. وعند محاولة إنشاء صورة Docker بناءً على نظام Alpine Linux خفيف الوزن، يحدث خطأ بسبب عدم التوافق بين مكتبات C الخاصة بـ glibc و musl، مما يجبر node-gyp rebuild على العمل داخل الحاوية.
خلال هذه العملية، تدخل أدوات التجميع مثل GCC و Python و make بالكامل داخل الصورة. وغالباً ما يتسبب ذلك في انهيار الكود الذي يعمل بسلاسة محلياً (على أجهزة macOS ARM64) بمجرد رفعها إلى خادم النشر (Linux x86_64) مع ظهور خطأ تجزئة الذاكرة (SIGSEGV).
بينما تضم بيئة Bun ترميزات JPEG و PNG و WebP مباشرة داخل ثنائيات بيئة التشغيل (Binary)، ولا تحتاج إلى تثبيت أي مترجمات خارجية أو حزم نظام تشغيل مثل libvips بشكل منفصل.
| مقارنة العناصر | Node.js (Sharp + libvips) | Bun الأصلي (Bun.Image) |
|---|---|---|
| تبعيات ربط C++ | ضرورية مثل node-gyp و N-API | غير موجودة (مدمجة في ثنائيات التشغيل) |
| أدوات البناء | تتطلب GCC و Python و make | غير مطلوبة |
| حجم حزمة النشر | مئات الميجابايت متضمنة الأدوات | مخفض إلى مستوى صورة الأساس |
| أخطاء وقت التشغيل | يحدث SIGSEGV عند عدم تطابق glibc/musl | يتم تجنبها عبر الربط الثابت المدمج |
تدعم واجهة Bun.Image التسلسل (Chaining)، مما يتيح لك نقل كود sharp الحالي تقريباً كما هو، وهي تتعامل مع Uint8Array مباشرة دون نسخ الذاكرة.
`typescript
// الكود القديم المبني على sharp
import sharp from "sharp";
export async function processImageSharp(inputBuffer: Buffer): Promise {
const image = sharp(inputBuffer);
const metadata = await image.metadata();
if (!metadata.width || metadata.width > 2000) {
return await image
.resize(1024, 1024, { fit: "inside", withoutEnlargement: true })
.rotate(90)
.webp({ quality: 85 })
.toBuffer();
}
return inputBuffer;
}
`
`typescript
// الكود المحول إلى Bun.Image
export async function processImageBun(inputBytes: Uint8Array): Promise {
// قراءة الترويسة بسرعة للتحقق من الحجم دون فك ضغط الصورة بأكملها
const meta = await new Bun.Image(inputBytes).metadata();
if (!meta.width || meta.width > 2000) {
return await new Bun.Image(inputBytes)
.resize(1024, 1024, { fit: "inside", withoutEnlargement: true })
.rotate(90)
.webp({ quality: 85 })
.bytes();
}
return inputBytes;
}
`
تقوم طريقة metadata() بتحليل منطقة الترويسة فقط دون فك تشفير الصورة بالكامل، مما يقلل من هدر وحدة المعالجة المركزية عند التعاملกับ الصور الأصلية الكبيرة.
طرق التعامل مع واجهات برمجة التطبيقات لكل مهمة:
new Bun.Image(bytes) أو Bun.file(path).image() بدلاً من sharp(buf)..webp({ quality: 85 }) كما هي..bytes() بدلاً من .toBuffer() لإرجاع Uint8Array..placeholder() المدمجة دون الحاجة لمكتبة خارجية.خطوات الانتقال بسيطة: قم بإزالة sharp و @types/sharp من package.json، ثم قم بتغيير تنسيق مخرجات وظائف الأدوات المساعدة إلى bytes()، وقم بإجراء التحقق من الوظائف باستخدام bun test.
ترتفع تكاليف الخدمات السحابية للصور مثل Cloudinary بسرعة حتى مع زيادة طفيفة في حركة المرور. في مرحلة تطوير الخدمات الفردية، يمكنك إنشاء خادم تخزين مؤقت لإعادة التحجيم الخاص بك وبكفاءة عالية باستخدام مزيج من bun:sqlite و Bun.serve فقط.
`typescript
import { Database } from "bun:sqlite";
const db = new Database("image_cache.sqlite");
// تطبيق وضع WAL لتحسين أداء القراءة والكتابة المتزامنة
db.exec("PRAGMA journal_mode = WAL;");
db.exec( CREATE TABLE IF NOT EXISTS image_cache ( key TEXT PRIMARY KEY, data BLOB NOT NULL, placeholder TEXT NOT NULL, mime_type TEXT NOT NULL, created_at INTEGER NOT NULL ));
const selectQuery = db.query("SELECT data, mime_type FROM image_cache WHERE key = ?");
const insertQuery = db.query( INSERT OR REPLACE INTO image_cache (key, data, placeholder, mime_type, created_at) VALUES (?, ?, ?, ?, ?));
export async function getOrGenerateThumbnail(
originalBytes: Uint8Array,
cacheKey: string,
width: number = 300
): Promise<{ bytes: Uint8Array; mimeType: string }> {
const cached = selectQuery.get(cacheKey) as { data: Uint8Array; mime_type: string } | null;
if (cached) {
return { bytes: cached.data, mimeType: cached.mime_type };
}
const imagePipeline = new Bun.Image(originalBytes);
const transformedBytes = await imagePipeline.resize(width).webp({ quality: 80 }).bytes();
const placeholder = await imagePipeline.placeholder();
insertQuery.run(cacheKey, transformedBytes, placeholder, "image/webp", Date.now());
return { bytes: transformedBytes, mimeType: "image/webp" };
}
`
`typescript
// نقطة نهاية خدمة الوسائط
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname.startsWith("/images/")) {
const imageId = url.pathname.replace("/images/", "");
const width = parseInt(url.searchParams.get("w") || "300", 10);
const cacheKey = `${imageId}_w${width}`;
const originalFile = Bun.file(`./uploads/${imageId}`);
if (!(await originalFile.exists())) {
return new Response("Image Not Found", { status: 404 });
}
const originalBytes = await originalFile.bytes();
const { bytes, mimeType } = await getOrGenerateThumbnail(originalBytes, cacheKey, width);
return new Response(bytes, {
headers: {
"Content-Type": mimeType,
"Cache-Control": "public, max-age=31536000, immutable",
},
});
}
return new Response("Not Found", { status: 404 });
},
});
`
يتم تمرير الطلبات الواردة لأول مرة فقط بعملية إعادة التحجيم وتخزينها في SQLite على هيئة BLOB، بينما يتم تقديم الطلبات اللاحقة مباشرة من ذاكرة التخزين المؤقت لقاعدة البيانات. وإذا قمت بضبط رأس الاستجابة Cache-Control لمدة طويلة، سيعمل التخزين المؤقت على مستوى المتصفح وش شبكة توصيل المحتوى (CDN) أيضاً.
تعتبر عمليات ترميز وفك ترميز الصور من المهام التي تستهلك وحدة المعالجة المركزية بشكل كبير. وعندما تتدفق طلبات الرفع، يتوقف استجابة الخادم بالكامل إذا تم تحويل الصور داخل حلقة الأحداث الرئيسية. وهذا هو سبب توقف خادم API بالكامل مع ارتفاع وقت الاستجابة P99 إلى مئات الملي ثانية.
يجب استخدام واجهة Worker الخاصة بـ Bun لنقل مهام معالجة الصور إلى خيوط الخلفية لضمان استمرار عمل الحلقة الرئيسية.
`typescript
// imageWorker.ts
declare var self: Worker;
interface ResizeTask {
id: string;
buffer: ArrayBuffer;
width: number;
}
self.onmessage = async (event: MessageEvent) => {
const { id, buffer, width } = event.data;
try {
const inputBytes = new Uint8Array(buffer);
const processedBytes = await new Bun.Image(inputBytes)
.resize(width)
.webp({ quality: 80 })
.bytes();
self.postMessage(
{ id, success: true, buffer: processedBytes.buffer },
[processedBytes.buffer] as any
);
} catch (error) {
self.postMessage({ id, success: false, error: (error as Error).message });
}
};
`
`typescript
// server.ts
const worker = new Worker("./imageWorker.ts");
const pendingTasks = new Map<string, (buf: ArrayBuffer) => void>();
worker.onmessage = (event) => {
const { id, success, buffer, error } = event.data;
const resolve = pendingTasks.get(id);
if (resolve && success) {
resolve(buffer);
pendingTasks.delete(id);
} else if (!success) {
console.error(فشل المهمة (${id}):, error);
pendingTasks.delete(id);
}
};
export function dispatchImageJob(id: string, buffer: ArrayBuffer, width: number): Promise {
return new Promise((resolve) => {
pendingTasks.set(id, resolve);
// نقل الملكية دون نسخ الذاكرة باستخدام كائن Transferable
worker.postMessage({ id, buffer, width }, [buffer]);
});
}
`
باستخدام Transferable ArrayBuffer، لا توجد تكلفة لنسخ الذاكرة حتى عند تبادل مخازن مؤقتة بحجم عدة ميجابايت بين الخيوط. وحتى مع تكدس الطلبات ذات الحجم الكبير، يقوم الخيار الرئيسي بإرجاع استجابة 202 Accepted ومعالجة طلبات API الأخرى دون تأخير.
يجب مراجعة بعض الاختلافات التي قد تحدث بين بيئة التطوير المحلية وبيئة حاوية النشر مسبقاً:
python3 و make و g++ و libvips-dev تماماً من ملف Dockerfile. سيؤدي ذلك إلى تسريع وقت البناء وجعل صورة الحاوية أخف بكثير./tmp الذي يستخدمه المخزن المؤقت لفك التشفير الداخلي لـ Bun حتى لا تتعطل العملية.PRAGMA journal_mode = WAL; فور إنشاء ملف SQLite، ستواجه أخطاء قفل قاعدة البيانات عند تكدس طلبات القراءة والكتابة المتزامنة.يؤدي التخلص من التبعيات الخارجية غير الضرورية إلى تقليل احتمالية فشل عملية البناء. فقط من خلال الاستفادة الفعالة من الأدوات المدمجة في بيئة التشغيل الفردية، يمكنك تقليل تعقيد تشغيل الخدمة وتكاليف صيانة البنية التحتية بشكل كبير.