لا تزال معظم تطبيقات الويب تتعامل مع عمليات رفع الصور كأنها "صندوق أسود". يقوم المستخدم بإسقاط ملف، فيقوم المتصفح بإرساله، ثم يقوم الخادم إما بقبول البيانات أو إرجاع خطأ 413 الذي لم يستعد له أحد. يغير الضغط من جانب المتصفح هذه المعادلة؛ فهو يمنحك فرصة لتقليص حجم البيانات قبل إرسالها عبر الشبكة، مما يعني رفعاً أسرع، وفواتير نطاق ترددي (bandwidth) أقل، وتقليلاً في حالات انتهاء مهلة الخادم (server timeouts). لكن هذا العمل من السهل أن يتم بشكل خاطئ. إذا تعاملت مع الضغط كأنه شريط منزلق سحري يحمل تسمية "الجودة"، فستقوم بإرسال صور تالفة، وصور مصغرة ممتدة، وتجارب مستخدم مربكة. النهج الأفضل هو التعامل مع التدفق بأكمله كمسار معالجة (pipeline).
فكر في مسارات المعالجة، لا في الأشرطة المنزلقة
قم بتقسيم المهمة إلى مراحل منفصلة. اقرأ الملف من عنصر الإدخال (input element). قم بتصغير حجم الصورة إلى الأبعاد المستهدفة. قم بترميز Blob جديد. ثم قم بعرض النتيجة للمستخدم. تقوم كل مرحلة بمهمة واحدة وتمرر مخرجاتها إلى المرحلة التالية. هذا الفصل ليس مجرد كود أكثر ترتيباً، بل يجعل اختبار الوحدات (unit testing) أمراً مباشراً. يمكنك تغذية "buffer" معروف إلى مرحلة تغيير الحجم دون المساس بمدخلات الملف. يمكنك التحقق من أن المرمز الخاص بك يخرج ملف JPEG بحجم أقل من 200 كيلوبايت دون انتظار رحلة ذهاب وإياب إلى الخادم. عندما يحدث خطأ ما، ستعرف بالضبط أي خطوة فشلت.
كما أن الحفاظ على فصل هذه المهام يمنع المفاجآت أثناء الرفع. إذا قمت بدمج تغيير الحجم والترميز في دالة واحدة متشابكة، فقد يؤدي خطأ في فك التشفير في منتصف العملية إلى ترك قائمة الرفع الخاصة بك في حالة غير متسقة. يجبرك مسار المعالجة على التحقق من الصحة عند كل حد فاصل. إذا تعذر فك تشفير الملف، فستكتشف ذلك قبل إنشاء canvas. وإذا كان الـ Blob المرمز كبيراً جداً، فستكتشف ذلك قبل أن تطلب من الخادم تخزينه.
حدد "عقداً" قبل كتابة الكود
قبل أن يكتب أي شخص استدعاء رسم على الـ canvas (canvas draw call)، قم بكتابة القواعد وشاركها مع الفريق. اختر أنواع MIME المقبولة. هل ستسمح بـ JPEG، أو PNG، أو WebP، أو AVIF؟ لكل منها تداعيات على قنوات ألفا (alpha channels)، ودعم المتصفحات، وتكلفة المعالج (CPU). حدد حداً أقصى لحجم الإدخال؛ فصورة خام بحجم 30 ميجابايت من هاتف رائد قد تؤدي إلى تجميد أو تعطل جهاز كمبيوتر محمول قديم إذا حاولت فك تشفيرها بالكامل في الذاكرة. حدد أبعاد المخرجات القصوى. إذا كانت واجهة المستخدم الخاصة بك لا تعرض أبداً صوراً أعرض من 2048 بكسل، فلا يوجد سبب للسماح لصورة بعرض 6000 بكسل بالمرور عبر مسار المعالجة.
والأهم من ذلك، خطط لإخفاقات فك التشفير. يمكن لملف تالف، أو ملف تعريف ألوان غريب، أو عملية رفع مبتورة أن تسبب خطأً في منشئ الصورة (Image constructor). يحتاج مسار المعالجة الخاص بك إلى كتلة catch واضحة ورسالة خطأ مفهومة للبشر. لا تترك المتصفح يموت بصمت وتترك المستخدم يحدق في أيقونة التحميل بينما لا يحدث شيء.
احترم الصورة
التشويه يبدو غير احترافي. حافظ على نسبة العرض إلى الارتفاع (aspect ratio) وضع حداً للضلع الأطول. إذا كان المربع المستهدف هو 1024 في 1024 بكسل، فيجب أن تستقر صورة بحجم 4000 في 3000 بكسل عند 1024 في 768، وليس 1024 في 1024. احسب عامل القياس من الحافة الأطول واترك الحافة الأقصر تتبعه. هذا يمنع الصور من التمدد إلى أشكال غريبة.
للتصدير الفعلي، استخدم طريقة toBlob الخاصة بالـ canvas. فهي تمنحك تحكماً مباشراً في تنسيق المخرجات وإعدادات الجودة، وتعمل بشكل غير متزامن (asynchronously) حتى لا تعطل الخيط الرئيسي (main thread). قم بإنشاء offscreen canvas (canvas خارج الشاشة)، وارسم الصورة التي تم تغيير حجمها عليه، ثم استدعِ canvas.toBlob مع النوع وقيمة الجودة المفضلة لديك. هذا الـ Blob الجديد هو ما ستسلمه إلى منطق الرفع أو واجهة برمجة تطبيقات التخزين (storage API) الخاصة بك.
أظهر الدليل
الضغط عمل غير مرئي. إذا لم تظهر الأرقام، فلن يثق المستخدمون في العملية. قم ببناء واجهة تسمح لهم بمقارنة الأصل بالنتيجة. اعرض حجم الملف الأصلي، وحجم الملف الجديد، والأبعاد الجديدة، ونوع التنسيق النهائي. رؤية صورة هاتف بحجم 4.2 ميجابايت وهي تنخفض إلى 380 كيلوبايت بتنسيق WebP يزيل الخوف من أنك تقوم بتشويه صورتهم سراً.
تساعد هذه الشفافية أيضاً في استكشاف الأخطاء وإصلاحها. عندما يشتكي مستخدم من فشل عملية الرفع، فإن أول شيء ستتحقق منه هو ما إذا كانت أبعاد المخرجات قد تجاوزت حد الخادم الخاص بك، أو ما إذا كان التنسيق قد تغير من PNG إلى JPEG وفقد قناة ألفا. ضع تلك البيانات في واجهة المستخدم حتى يتمكن المستخدم من تشخيص المشكلة بنفسه قبل فتح تذكرة دعم.
الإعدادات المسبقة تتفوق على إعادة الضغط
لا تقم أبداً بضغط نفس الصورة مرتين. فكل تمريرة عبر مشفر مع فقدان للبيانات (lossy encoder) تسلب المزيد من التفاصيل وتدخل تشوهات مربعة (blocky artifacts). إذا سمحت للمستخدم بالضغط على "تحسين" (optimize) بشكل متكرر، فستبدو النسخة الثالثة مثل نسخة مصورة من نسخة مصورة. بدلاً من ذلك، قم بإنشاء كل مخرج من ملف المصدر الأصلي وقدم إعدادات مسبقة (presets):
- ملف أصغر: خفّض الجودة واضبط الأبعاد بصرامة للصور المصغرة أو المعاينات السريعة.
- متوازن: استهدف مستوى جودة متوسطاً مع أبعاد منطقية، بحيث يكون مناسباً لخلاصات التواصل الاجتماعي والمعارض.
- مزيد من التفاصيل: حافظ على جودة عالية وأبعاد أكبر للتصوير الفوتوغرافي، أو الأعمال الفنية، أو معاينات الطباعة.
قم بتخزين الـ Blob الأصلي في الذاكرة حتى يتمكن المستخدم من التبديل بين الإعدادات المسبقة دون تراكم أجيال من فقدان الجودة. قم دائماً بالتوليد من المصدر، ولا تعتمد أبداً على المخرج الأخير.
اختبر كما يرفع مستخدموك الملفات
جهاز التطوير الخاص بك المتصل بشبكة ألياف ضوئية ويحتوي على ذاكرة وصول عشوائي (RAM) بسعة 32 جيجابايت ليس هو الواقع. اختبر باستخدام الملفات الفعلية التي يحملها المستخدمون الحقيقيون. تستخدم صور الهواتف من iOS و Android اتجاهات بيانات وصفية (metadata) مختلفة وقد تنشأ من مصادر HEIC. تتصرف الأصول الشفافة مثل الشعارات والأيقونات بشكل مختلف عند التحويل إلى JPEG لأن JPEG ببساطة لا يدعم قنوات ألفا (alpha channels). ستكشف الملفات الضخمة عن حدود الذاكرة في الأجهزة التي تحتوي على 2 جيجابايت من ذاكرة الوصول العشوائي. وستكشف المعالجات (CPUs) المحمولة البطيئة عن الوقت الذي يستغرقه استدعاء toBlob فعلياً.
استخدم Chrome DevTools لتقييد سرعة المعالج والشبكة. جرب هاتف Android عمره خمس سنوات. إذا تسبب مسار المعالجة (pipeline) في تجميد واجهة المستخدم لمدة ثلاث ثوانٍ أثناء الترميز، فستحتاج إلى نقل العمل الثقيل إلى Web Worker لتبقى الواجهة مستجيبة.
أطلق الأساسيات أولاً
من المغري دعم كل تنسيق و
