المستوى: متوسط — الشرح يفترض معرفة أساسية بـ Node.js وسطر الأوامر، لكن مفاهيم الصور نفسها مشروحة من الصفر بمثال بسيط قبل الدخول في التفاصيل.
لو بترفع صور المستخدمين زي ما هي، إنت بتبعت 2.4 ميجابايت لكل زائر علشان يشوف الصورة على موبايل عرضه 400 بكسل. ده إهدار في الباندويدث وبطء في التحميل. في الدليل ده هتبني endpoint واحد بـ Node.js يستقبل أي صورة، يصغّرها ويطلّعها بصيغة WebP، فينزّل الحجم لحوالي 180 كيلوبايت — أقل بنسبة 92% تقريبًا — من غير ما تغيّر شكل تطبيقك.
خدمة تصغير وضغط الصور بـ Sharp في Node.js
المشكلة باختصار
الصور هي أثقل جزء في أغلب الصفحات. صورة كاميرا موبايل عادية بتطلع 1600 بكسل أو أكتر وحجمها 2 إلى 4 ميجا. المتصفح بينزّلها كلها حتى لو هيعرضها في مربع صغير. النتيجة: LCP (أكبر عنصر مرئي) بيتأخر، وفاتورة الـ CDN بتعلى. الحل مش إنك تمنع الرفع، الحل إنك تعالج الصورة مرة واحدة على السيرفر: تصغّر أبعادها للمقاس اللي محتاجه فعلاً، وتضغطها بصيغة أحدث.
الفكرة ببساطة ثم علميًا
تخيّل إنك عايز تبعت صورة مقاس لوحة إعلانات لحد هيعلّقها في إطار صغير على مكتبه. المنطقي إنك تقصّها للمقاس المناسب الأول، وبعدين تلفّها كويس علشان متتكرمشش. Sharp بيعمل بالظبط الحاجتين دول: بيغيّر أبعاد الصورة (resize)، وبيعيد ترميزها بصيغة مضغوطة (encode).
علميًا: Sharp مكتبة Node.js مبنية فوق libvips، وهي محرّك معالجة صور بيشتغل على أجزاء من الصورة بشكل متدفّق بدل ما يحمّلها كلها في الذاكرة، فبيستهلك رام أقل وبيكون أسرع من البدائل التقليدية في عمليات التصغير الشائعة. WebP صيغة من Google بتدي حجم أصغر من JPEG بنفس الجودة تقريبًا، لأنها بتستخدم ضغط أكفأ. الافتراض هنا إن مدخلاتك صور فوتوغرافية بيرفعها المستخدم وحجمها أقل من 8 ميجا.
نبنيها خطوة بخطوة
- جهّز المشروع. أنشئ مجلد جديد وابدأ مشروع Node فاضي:
mkdir image-optimizer && cd image-optimizer npm init -y - ثبّت الاعتماديات. Express للسيرفر، multer لاستقبال الملف، وsharp للمعالجة:
npm install express multer sharp - اكتب السيرفر. ملف
server.jsفيه endpoint واحد اسمه/optimize:const express = require('express'); const multer = require('multer'); const sharp = require('sharp'); // multer من غير storage بيستخدم الذاكرة، فالملف بيوصلنا كـ Buffer جاهز const upload = multer({ limits: { fileSize: 8 * 1024 * 1024 }, // سقف 8 ميجا fileFilter: (req, file, cb) => { const ok = ['image/jpeg', 'image/png', 'image/webp'].includes(file.mimetype); cb(ok ? null : new Error('نوع ملف غير مدعوم'), ok); }, }); const app = express(); app.post('/optimize', upload.single('image'), async (req, res) => { if (!req.file) return res.status(400).json({ error: 'لا يوجد ملف' }); try { const out = await sharp(req.file.buffer) .rotate() // يحترم اتجاه EXIF .resize({ width: 800, withoutEnlargement: true }) // مايكبّرش الصغيرة .webp({ quality: 80 }) .toBuffer(); res.set('Content-Type', 'image/webp'); res.send(out); } catch (e) { res.status(500).json({ error: 'فشل معالجة الصورة' }); } }); app.listen(3000, () => console.log('running on http://localhost:3000')); - افهم كل خطوة في السلسلة.
rotate()بدون وسيط بيصحّح اتجاه الصورة حسب بيانات EXIF علشان متتقلبش.resize({ width: 800 })بيثبّت العرض على 800 بكسل ويحسب الارتفاع تلقائيًا.withoutEnlargementبيمنع تكبير الصور اللي أصلاً أصغر من 800.webp({ quality: 80 })بيرمّز الناتج WebP بجودة 80، وهي نقطة متوازنة بين الحجم والوضوح. - شغّل الخدمة.
node server.js
التحقق من أنه يعمل
ابعت صورة حقيقية بـ curl واحفظ الناتج، وبعدين قارن الحجمين:
curl -F "image=@photo.jpg" http://localhost:3000/optimize -o out.webp
ls -lh photo.jpg out.webpالمفروض تلاقي photo.jpg حوالي 2.4 ميجا وout.webp حوالي 180 كيلوبايت. لو الناتج مش أصغر بكتير، غالبًا صورتك الأصلية أصغر من 800 بكسل أو مضغوطة أصلاً — وده طبيعي.
الأرقام والمقايضات
لصورة فوتوغرافية 1600 بكسل بحجم ~2.4 ميجا، التصغير لـ 800 بكسل مع WebP q80 بينزّلها لحوالي 180 كيلوبايت، يعني توفير قرب 92% في الحجم المرسل. زمن المعالجة على نواة واحدة بيكون في حدود 50 إلى 200 مللي ثانية للصورة النموذجية (رقم تقريبي بيختلف حسب المقاس والسيرفر).
الـ trade-off هنا: بتكسب حجم أقل وتحميل أسرع، بتخسر جزء من التفاصيل الدقيقة ووقت معالجة في كل طلب. لو نزّلت الجودة تحت 70 هتبدأ تشوف تشوّه في حواف النصوص والتدرّجات. والافتراض إن الحمل معقول؛ لو عندك آلاف الطلبات في الثانية، معالجة كل طلب لحظيًا هتاكل الـ CPU — وقتها الأفضل تعالج الصورة مرة وقت الرفع وتخزّن النسخة المحسّنة، أو تحطّها ورا طابور مهام.
متى لا تستخدم هذه الطريقة
ماتستخدمهاش لو المستخدم محتاج الصورة الأصلية بأعلى دقة (زي معرض تصوير بيبيع الملفات الأصلية). وماتعالجش لحظيًا لو مدخلاتك متحسّنة أصلاً أو صغيرة. ولو لازم تدعم متصفحات قديمة جدًا مبتفهمش WebP، وفّر نسخة JPEG احتياطية عبر عنصر picture بدل ما تفرض WebP على الكل.
الخطوة التالية
افتح أقرب endpoint رفع صور عندك، وضيف سطر .resize({ width: 800, withoutEnlargement: true }).webp({ quality: 80 }) قبل ما تخزّن الملف. بعدها شغّل ls -lh على الصورة قبل وبعد وقارن. لو التوفير أقل من المتوقع، جرّب تنزّل العرض أو الجودة درجة وقيس تاني.
المصادر
- توثيق Sharp الرسمي: sharp.pixelplumbing.com
- محرّك libvips: libvips.org
- صيغة WebP من Google: developers.google.com/speed/webp
- أفضل ممارسات الصور: web.dev — Serve images in WebP
- مكتبة multer: github.com/expressjs/multer
- دعم WebP في المتصفحات: caniuse.com/webp