تحميل أسرع للصور مع معاينات الصور المضمنة

نشرت: 2022-03-10
ملخص سريع ↬ تتيح لنا تقنية معاينة الصورة المضمنة (EIP) المقدمة في هذه المقالة تحميل صور المعاينة أثناء التحميل البطيء باستخدام طلبات نطاق JPEG و Ajax و HTTP دون الحاجة إلى نقل بيانات إضافية.

تعد معاينة الصورة منخفضة الجودة (LQIP) والمتغير المستند إلى SVG SQIP هما الأسلوبان السائدان لتحميل الصور البطيئة. القاسم المشترك بينهما هو أنك تقوم أولاً بإنشاء صورة معاينة منخفضة الجودة. سيتم عرض هذا بشكل غير واضح واستبداله بالصورة الأصلية لاحقًا. ماذا لو كان بإمكانك تقديم صورة معاينة إلى زائر الموقع دون الحاجة إلى تحميل بيانات إضافية؟

تتمتع ملفات JPEG ، التي يتم استخدام التحميل البطيء لها في الغالب ، بإمكانية ، وفقًا للمواصفات ، لتخزين البيانات الموجودة بها بطريقة يتم فيها عرض محتويات الصورة التقريبية ثم التفصيلية أولاً. بدلاً من إنشاء الصورة من أعلى إلى أسفل أثناء التحميل (وضع الخط الأساسي) ، يمكن عرض الصورة غير الواضحة بسرعة كبيرة ، والتي تصبح تدريجياً أكثر وضوحًا ووضوحًا (الوضع التدريجي).

تمثيل الهيكل الزمني لـ JPEG في وضع الخط الأساسي
الوضع الأساسي (معاينة كبيرة)
تمثيل الهيكل الزمني لـ JPEG في الوضع التدريجي
الوضع التدريجي (معاينة كبيرة)

بالإضافة إلى تجربة المستخدم الأفضل التي يوفرها المظهر الذي يتم عرضه بسرعة أكبر ، عادةً ما تكون ملفات JPEG التقدمية أصغر من نظيراتها ذات التشفير الأساسي. بالنسبة للملفات التي يزيد حجمها عن 10 كيلو بايت ، هناك احتمال بنسبة 94 بالمائة لصورة أصغر عند استخدام الوضع التدريجي وفقًا لستويان ستيفانوف من فريق تطوير Yahoo.

إذا كان موقع الويب الخاص بك يتكون من العديد من ملفات JPEG ، فستلاحظ أنه يتم تحميل ملفات JPEG واحدة تلو الأخرى. وذلك لأن المتصفحات الحديثة تسمح فقط بستة اتصالات متزامنة إلى مجال. وبالتالي ، فإن ملفات JPEG التقدمية وحدها ليست الحل لمنح المستخدم أسرع انطباع ممكن عن الصفحة. في أسوأ الأحوال ، سيقوم المتصفح بتحميل الصورة بالكامل قبل أن يبدأ في تحميل الصورة التالية.

الفكرة المقدمة هنا الآن هي تحميل عدد كبير جدًا من وحدات البايت من JPEG التدريجي من الخادم بحيث يمكنك الحصول بسرعة على انطباع عن محتوى الصورة. لاحقًا ، في الوقت الذي حددناه (على سبيل المثال ، عند تحميل جميع صور المعاينة في منفذ العرض الحالي) ، يجب تحميل بقية الصورة دون طلب الجزء المطلوب بالفعل للمعاينة مرة أخرى.

يوضح الطريقة التي تقوم بها تقنية EIP (معاينة الصورة المضمنة) بتحميل بيانات الصورة في طلبين.
تحميل ملف JPEG تقدمي بطلبين (معاينة كبيرة)

لسوء الحظ ، لا يمكنك تحديد علامة img في إحدى السمات كم يجب تحميل الصورة في أي وقت. مع Ajax ، يكون هذا ممكنًا ، بشرط أن يدعم الخادم الذي يرسل الصورة طلبات نطاق HTTP.

باستخدام طلبات نطاق HTTP ، يمكن للعميل إبلاغ الخادم في رأس طلب HTTP أي بايتات الملف المطلوب يجب تضمينها في استجابة HTTP. هذه الميزة ، المدعومة من قبل كل من الخوادم الكبيرة (Apache ، IIS ، nginx) ، تستخدم بشكل أساسي لتشغيل الفيديو. إذا قفز المستخدم إلى نهاية الفيديو ، فلن يكون تحميل الفيديو كاملاً فعالاً قبل أن يتمكن المستخدم أخيرًا من رؤية الجزء المطلوب. لذلك ، لا يطلب الخادم سوى بيانات الفيديو حول الوقت الذي يطلبه المستخدم ، بحيث يمكن للمستخدم مشاهدة الفيديو بأسرع ما يمكن.

نواجه الآن التحديات الثلاثة التالية:

  1. إنشاء JPEG التقدمي
  2. حدد إزاحة البايت التي يجب أن يقوم طلب نطاق HTTP الأول بتحميل صورة المعاينة لها
  3. إنشاء كود JavaScript للواجهة الأمامية
المزيد بعد القفز! أكمل القراءة أدناه ↓

1. إنشاء JPEG التقدمي

يتكون JPEG التدريجي من عدة ما يسمى بمقاطع المسح ، كل منها يحتوي على جزء من الصورة النهائية. يُظهر المسح الأول الصورة بشكل تقريبي فقط ، بينما تضيف الصور التي تليها لاحقًا في الملف معلومات أكثر وأكثر تفصيلاً إلى البيانات التي تم تحميلها بالفعل وتشكل المظهر النهائي في النهاية.

يتم تحديد الشكل الدقيق لعمليات المسح الفردية بواسطة البرنامج الذي يقوم بإنشاء ملفات JPEG. في برامج سطر الأوامر مثل cjpeg من مشروع mozjpeg ، يمكنك حتى تحديد البيانات التي تحتوي عليها عمليات الفحص هذه. ومع ذلك ، يتطلب هذا مزيدًا من المعرفة المتعمقة ، والتي ستتجاوز نطاق هذه المقالة. لهذا ، أود أن أشير إلى مقالتي "فهم JPG أخيرًا" ، والتي تعلم أساسيات ضغط JPEG. المعلمات الدقيقة التي يجب تمريرها إلى البرنامج في نص مسح ضوئي موضحة في wizard.txt لمشروع mozjpeg. في رأيي ، معلمات نص المسح (سبع عمليات مسح) المستخدمة بواسطة mozjpeg افتراضيًا هي حل وسط جيد بين البنية التقدمية السريعة وحجم الملف ، وبالتالي يمكن اعتمادها.

لتحويل JPEG الأولي الخاص بنا إلى JPEG تقدمي ، نستخدم jpegtran من مشروع mozjpeg. هذه أداة لإجراء تغييرات غير ضائعة على ملف JPEG موجود. تتوفر الإصدارات المجمعة مسبقًا لنظامي التشغيل Windows و Linux هنا: https://mozjpeg.codelove.de/binaries.html. إذا كنت تفضل تشغيلها بأمان لأسباب أمنية ، فمن الأفضل أن تبنيها بنفسك.

من سطر الأوامر ، نقوم الآن بإنشاء JPEG التقدمي الخاص بنا:

 $ jpegtran input.jpg > progressive.jpg

يفترض jpegtran حقيقة أننا نريد إنشاء JPEG تقدمي ولا يلزم تحديدها بشكل صريح. لن يتم تغيير بيانات الصورة بأي شكل من الأشكال. يتم فقط تغيير ترتيب بيانات الصورة داخل الملف.

البيانات الوصفية غير ذات الصلة بمظهر الصورة (مثل بيانات Exif أو IPTC أو XMP) ، يجب إزالتها بشكل مثالي من JPEG حيث لا يمكن قراءة المقاطع المقابلة إلا بواسطة مفككات تشفير البيانات الوصفية إذا كانت تسبق محتوى الصورة. نظرًا لأنه لا يمكننا نقلها خلف بيانات الصورة في الملف لهذا السبب ، فسيتم تسليمها بالفعل مع صورة المعاينة وتكبير الطلب الأول وفقًا لذلك. باستخدام برنامج سطر الأوامر exiftool يمكنك بسهولة إزالة هذه البيانات الوصفية:

 $ exiftool -all= progressive.jpg

إذا كنت لا تريد استخدام أداة سطر أوامر ، فيمكنك أيضًا استخدام خدمة الضغط عبر الإنترنت compress-or-die.com لإنشاء ملف JPEG تقدمي بدون بيانات وصفية.

2. تحديد إزاحة البايت التي يجب أن يقوم طلب نطاق HTTP الأول بتحميل صورة المعاينة إليها

ينقسم ملف JPEG إلى مقاطع مختلفة ، يحتوي كل منها على مكونات مختلفة (بيانات الصورة ، والبيانات الوصفية مثل IPTC ، و Exif و XMP ، وملفات تعريف الألوان المضمنة ، وجداول التكميم ، وما إلى ذلك). يبدأ كل جزء من هذه المقاطع بعلامة مقدمة بواسطة بايت FF سداسي عشري. ويتبع ذلك بايت يشير إلى نوع المقطع. على سبيل المثال ، يكمل D8 العلامة إلى محدد SOI FF D8 (بداية الصورة) ، والذي يبدأ به كل ملف JPEG.

يتم تمييز كل بداية للمسح بعلامة SOS (بدء المسح ، سداسي عشري FF DA ). نظرًا لأن البيانات الموجودة خلف علامة SOS مشفرة بالانتروبيا (تستخدم JPEGs ترميز Huffman) ، فهناك مقطع آخر مع جداول Huffman (DHT ، سداسي عشري FF C4 ) مطلوب لفك التشفير قبل مقطع SOS. وبالتالي ، فإن مجال الاهتمام بالنسبة لنا داخل ملف JPEG التدريجي يتكون من جداول Huffman المتناوبة / مقاطع بيانات المسح. وبالتالي ، إذا أردنا عرض أول مسح تقريبي للغاية للصورة ، فعلينا أن نطلب جميع البايتات حتى التكرار الثاني لمقطع DHT (سداسي عشري FF C4 ) من الخادم.

يظهر علامات SOS في ملف JPEG
هيكل ملف JPEG (معاينة كبيرة)

في PHP ، يمكننا استخدام الكود التالي لقراءة عدد البايتات المطلوبة لجميع عمليات المسح في المصفوفة:

 <?php $img = "progressive.jpg"; $jpgdata = file_get_contents($img); $positions = []; $offset = 0; while ($pos = strpos($jpgdata, "\xFF\xC4", $offset)) { $positions[] = $pos+2; $offset = $pos+2; }

يتعين علينا إضافة قيمة اثنين إلى الموضع الذي تم العثور عليه لأن المتصفح يعرض الصف الأخير فقط من صورة المعاينة عندما يواجه علامة جديدة (والتي تتكون من وحدتي بايت كما ذكرنا للتو).

نظرًا لأننا مهتمون بالصورة الأولى للمعاينة في هذا المثال ، فإننا نجد الموضع الصحيح في المواضع $positions[1] حتى نطلب الملف عبر HTTP Range Request. لطلب صورة ذات دقة أفضل ، يمكننا استخدام موضع لاحق في المصفوفة ، على سبيل المثال ، $positions[3] .

3. إنشاء كود JavaScript للواجهة الأمامية

بادئ ذي بدء ، نحدد علامة img ، والتي نعطيها موضع البايت الذي تم تقييمه للتو:

 <img data-src="progressive.jpg" data-bytes="<?= $positions[1] ?>">

كما هو الحال غالبًا مع مكتبات التحميل البطيء ، لا نحدد سمة src مباشرة حتى لا يبدأ المتصفح فورًا في طلب الصورة من الخادم عند تحليل كود HTML.

باستخدام كود JavaScript التالي ، نقوم الآن بتحميل صورة المعاينة:

 var $img = document.querySelector("img[data-src]"); var URL = window.URL || window.webkitURL; var xhr = new XMLHttpRequest(); xhr.onload = function(){ if (this.status === 206){ $img.src_part = this.response; $img.src = URL.createObjectURL(this.response); } } xhr.open('GET', $img.getAttribute('data-src')); xhr.setRequestHeader("Range", "bytes=0-" + $img.getAttribute('data-bytes')); xhr.responseType = 'blob'; xhr.send();

ينشئ هذا الرمز طلب Ajax الذي يخبر الخادم في رأس نطاق HTTP بإرجاع الملف من البداية إلى الموضع المحدد في data-bytes ... وليس أكثر. إذا كان الخادم يفهم طلبات نطاق HTTP ، فإنه يُرجع بيانات الصورة الثنائية في استجابة HTTP-206 (HTTP 206 = محتوى جزئي) في شكل blob ، يمكننا من خلاله إنشاء عنوان URL داخلي للمتصفح باستخدام createObjectURL . نحن نستخدم عنوان URL هذا كـ src لعلامة img الخاصة بنا. وهكذا قمنا بتحميل صورة المعاينة الخاصة بنا.

نقوم أيضًا بتخزين blob في كائن DOM في الخاصية src_part ، لأننا سنحتاج إلى هذه البيانات على الفور.

في علامة تبويب الشبكة لوحدة تحكم المطورين ، يمكنك التحقق من أننا لم نحمل الصورة الكاملة ، ولكن جزء صغير منها فقط. بالإضافة إلى ذلك ، يجب عرض تحميل blob URL بحجم 0 بايت.

يعرض وحدة تحكم الشبكة وأحجام طلبات HTTP
وحدة تحكم الشبكة عند تحميل صورة المعاينة (معاينة كبيرة)

نظرًا لأننا نقوم بالفعل بتحميل رأس JPEG للملف الأصلي ، فإن صورة المعاينة لها الحجم الصحيح. وبالتالي ، بناءً على التطبيق ، يمكننا حذف ارتفاع وعرض علامة img .

بديل: تحميل صورة المعاينة بشكل مضمّن

لأسباب تتعلق بالأداء ، من الممكن أيضًا نقل بيانات صورة المعاينة على أنها بيانات URI مباشرة في كود مصدر HTML. هذا يوفر علينا عبء نقل رؤوس HTTP ، لكن تشفير base64 يجعل بيانات الصورة أكبر بمقدار الثلث. يكون هذا نسبيًا إذا قدمت كود HTML مع ترميز محتوى مثل gzip أو brotli ، ولكن لا يزال يتعين عليك استخدام عناوين URL للبيانات لصور المعاينة الصغيرة.

الأهم من ذلك هو حقيقة أن صور المعاينة متاحة على الفور ولا يوجد تأخير ملحوظ للمستخدم عند إنشاء الصفحة.

بادئ ذي بدء ، يتعين علينا إنشاء عنوان URI للبيانات ، والذي نستخدمه بعد ذلك في علامة img كـ src . لهذا ، نقوم بإنشاء URI للبيانات عبر PHP ، حيث يعتمد هذا الرمز على الكود الذي تم إنشاؤه للتو ، والذي يحدد إزاحات البايت لعلامات SOS:

 <?php … $fp = fopen($img, 'r'); $data_uri = 'data:image/jpeg;base64,'. base64_encode(fread($fp, $positions[1])); fclose($fp);

يتم الآن إدراج URI للبيانات التي تم إنشاؤها مباشرةً في علامة `img` كـ src :

 <img src="<?= $data_uri ?>" data-src="progressive.jpg" alt="">

بالطبع ، يجب أيضًا تعديل كود JavaScript:

 <script> var $img = document.querySelector("img[data-src]"); var binary = atob($img.src.slice(23)); var n = binary.length; var view = new Uint8Array(n); while(n--) { view[n] = binary.charCodeAt(n); } $img.src_part = new Blob([view], { type: 'image/jpeg' }); $img.setAttribute('data-bytes', $img.src_part.size - 1); </script>

بدلاً من طلب البيانات عبر طلب Ajax ، حيث نتلقى على الفور blob ، في هذه الحالة يتعين علينا إنشاء blob بأنفسنا من معرّف البيانات URI. للقيام بذلك ، نقوم بتحرير data-URI من الجزء الذي لا يحتوي على بيانات الصورة: data:image/jpeg;base64 . نقوم بفك تشفير البيانات المشفرة base64 المتبقية باستخدام الأمر atob . من أجل إنشاء blob من بيانات السلسلة الثنائية الآن ، يتعين علينا نقل البيانات إلى مصفوفة Uint8 ، مما يضمن عدم التعامل مع البيانات كنص UTF-8 مشفر. من هذه المصفوفة ، يمكننا الآن إنشاء blob ثنائي ببيانات الصورة لصورة المعاينة.

حتى لا نضطر إلى تكييف الكود التالي لهذا الإصدار المضمّن ، نضيف data-bytes السمة إلى علامة img ، والتي تحتوي في المثال السابق على إزاحة البايت التي يجب تحميل الجزء الثاني من الصورة منها .

في علامة تبويب الشبكة في وحدة تحكم المطور ، يمكنك أيضًا التحقق هنا من أن تحميل صورة المعاينة لا ينتج عنه طلبًا إضافيًا ، بينما يزداد حجم ملف صفحة HTML.

يعرض وحدة تحكم الشبكة وأحجام طلبات HTTP
وحدة تحكم الشبكة عند تحميل صورة المعاينة على هيئة بيانات URI (معاينة كبيرة)

تحميل الصورة النهائية

في الخطوة الثانية نقوم بتحميل باقي ملف الصورة بعد ثانيتين كمثال:

 setTimeout(function(){ var xhr = new XMLHttpRequest(); xhr.onload = function(){ if (this.status === 206){ var blob = new Blob([$img.src_part, this.response], { type: 'image/jpeg'} ); $img.src = URL.createObjectURL(blob); } } xhr.open('GET', $img.getAttribute('data-src')); xhr.setRequestHeader("Range", "bytes="+ (parseInt($img.getAttribute('data-bytes'), 10)+1) +'-'); xhr.responseType = 'blob'; xhr.send(); }, 2000);

في رأس النطاق هذه المرة نحدد أننا نريد أن نطلب الصورة من الموضع النهائي لصورة المعاينة إلى نهاية الملف. يتم تخزين إجابة الطلب الأول في الخاصية src_part من كائن DOM. نستخدم الردود من كلا الطلبين لإنشاء blob new Blob() ، والذي يحتوي على بيانات الصورة بأكملها. يتم استخدام عنوان URL الخاص بـ blob الذي تم إنشاؤه من هذا مرة أخرى كـ src لكائن DOM. الآن تم تحميل الصورة بالكامل.

الآن يمكننا أيضًا التحقق من الأحجام المحملة في علامة تبويب الشبكة بوحدة تحكم المطور مرة أخرى ..

يعرض وحدة تحكم الشبكة وأحجام طلبات HTTP
وحدة تحكم الشبكة عند تحميل الصورة بأكملها (31.7 كيلو بايت) (معاينة كبيرة)

النموذج المبدئي

في عنوان URL التالي ، قدمت نموذجًا أوليًا حيث يمكنك تجربة معلمات مختلفة: https://embedded-image-preview.cerdmann.com/prototype/

يمكن العثور على مستودع GitHub للنموذج الأولي هنا: https://github.com/McSodbrenner/embedded-image-preview

اعتبارات في النهاية

باستخدام تقنية معاينة الصورة المضمنة (EIP) المعروضة هنا ، يمكننا تحميل صور معاينة مختلفة نوعياً من ملفات JPEG التقدمية بمساعدة طلبات نطاق Ajax و HTTP. لا يتم تجاهل البيانات من صور المعاينة هذه ولكن بدلاً من ذلك يُعاد استخدامها لعرض الصورة بأكملها.

علاوة على ذلك ، لا يلزم إنشاء صور معاينة. على جانب الخادم ، يجب تحديد وحفظ إزاحة البايت التي تنتهي عندها صورة المعاينة. في نظام CMS ، يجب أن يكون من الممكن حفظ هذا الرقم كسمة على صورة وأخذها في الاعتبار عند إخراجها في علامة img . حتى سير العمل يمكن تصوره ، والذي يكمل اسم ملف الصورة بواسطة الإزاحة ، على سبيل المثال progressive-8343.jpg ، حتى لا تضطر إلى حفظ الإزاحة بعيدًا عن ملف الصورة. يمكن استخراج هذه الإزاحة بواسطة كود JavaScript.

نظرًا لإعادة استخدام بيانات صورة المعاينة ، يمكن أن تكون هذه التقنية بديلاً أفضل للنهج المعتاد لتحميل صورة معاينة ثم WebP (وتوفير نسخة احتياطية بتنسيق JPEG للمتصفحات التي لا تدعم WebP). غالبًا ما تدمر صورة المعاينة مزايا التخزين الخاصة بـ WebP ، والتي لا تدعم الوضع التدريجي.

حاليًا ، تعد صور المعاينة في LQIP العادي ذات جودة منخفضة ، حيث يُفترض أن تحميل بيانات المعاينة يتطلب نطاقًا تردديًا إضافيًا. كما أوضح Robin Osborne بالفعل في منشور مدونة في عام 2018 ، ليس من المنطقي إظهار العناصر النائبة التي لا تعطيك فكرة عن الصورة النهائية. باستخدام التقنية المقترحة هنا ، يمكننا إظهار بعض المزيد من الصورة النهائية كصورة معاينة دون تردد من خلال تقديم مسح لاحق لصورة JPEG المتقدمة للمستخدم.

في حالة اتصال الشبكة الضعيف للمستخدم ، قد يكون من المنطقي ، اعتمادًا على التطبيق ، عدم تحميل ملف JPEG بالكامل ، ولكن على سبيل المثال حذف آخر عمليتي مسح ضوئي. ينتج عن هذا تنسيق JPEG أصغر بكثير بجودة منخفضة قليلاً فقط. سوف يشكرنا المستخدم على ذلك ، ولا يتعين علينا تخزين ملف إضافي على الخادم.

الآن أتمنى لك الكثير من المرح في تجربة النموذج الأولي وأتطلع إلى تعليقاتك.