تصغير حمولات الشبكة وضغطها باستخدام gzip

يتناول هذا الدرس التطبيقي حول الترميز كيفية تحسين أداء الصفحة من خلال تصغير حِزمة JavaScript للتطبيق التالي وضغطها عن طريق تقليل حجم طلب التطبيق.

لقطة شاشة التطبيق

القياس

قبل البدء في إضافة تحسينات، من الأفضل دائمًا تحليل الحالة الحالية للتطبيق أولاً.

  • لمعاينة الموقع الإلكتروني، اضغط على عرض التطبيق. ثم اضغط على ملء الشاشة ملء الشاشة.

يتيح لك هذا التطبيق، الذي تم تناوله أيضًا في الدرس التطبيقي "إزالة الرمز المبرمَج غير المستخدَم"، التصويت على قطّتك المفضّلة. 🐈

اطّلِع الآن على حجم هذا التطبيق:

  1. اضغط على Ctrl ‏+ Shift ‏+ J (أو Command ‏+ Option ‏+ J على نظام التشغيل Mac) لفتح DevTools.
  2. انقر على علامة التبويب الشبكة.
  3. ضَع علامة في مربّع الاختيار إيقاف ذاكرة التخزين المؤقت.
  4. أعِد تحميل التطبيق.

حجم الحِزمة الأصلي في لوحة "الشبكة"

على الرغم من أنّه تم إحراز الكثير من التقدّم في "إزالة الرموز غير المستخدَمة" في ورشة العمل البرمجية لتقليل حجم هذه الحِزمة، لا يزال حجمها 225 كيلوبايت كبيرًا جدًا.

تصغير

فكِّر في مجموعة الرموز البرمجية التالية.

function soNice() {
  let counter = 0;

  while (counter < 100) {
    console.log('nice');
    counter++;
  }
}

إذا تم حفظ هذه الدالة في ملف خاص بها، يكون حجم الملف حوالي 112 بايت (بايت).

في حال إزالة جميع المسافات البيضاء، سيظهر الرمز الناتج على النحو التالي:

function soNice(){let counter=0;while(counter<100){console.log("nice");counter++;}}

سيكون حجم الملف الآن حوالي 83 بايت. إذا تم تشويهه أكثر من خلال تقليل طول اسم المتغيّر وتعديل بعض التعبيرات، قد ينتهي الأمر بأن يظهر الرمز النهائي بالشكل التالي:

function soNice(){for(let i=0;i<100;)console.log("nice"),i++}

يصل حجم الملف الآن إلى 62 بايت.

ومع كل خطوة، يصبح الرمز أكثر صعوبة في القراءة. ومع ذلك، يفسّر محرّك JavaScript في المتصفّح كلّ عنصر من هذه العناصر بالطريقة نفسها تمامًا. إنّ فائدة تشويش الرموز البرمجية بهذه الطريقة هي تقليل حجم الملفات. لم يكن حجم التطبيق الذي يبلغ 112 غيغابايت كبيرًا في البداية، ولكن تم خفضه بنسبة ‎50% .

في هذا التطبيق، يتم استخدام الإصدار 4 من webpack كأداة تجميع للوحدات. يمكن الاطّلاع على الإصدار المحدّد في package.json.

"devDependencies": {
  //...
  "webpack": "^4.16.4",
  //...
}

يُصغر الإصدار 4 الحزمة تلقائيًا أثناء وضع الإنتاج. ويستخدم TerserWebpackPlugin مكوّنًا إضافيًا Terser. ‫Terser هي أداة شائعة تُستخدَم لضغط رمز JavaScript.

للحصول على فكرة عن شكل الرمز البرمجي المُكثَّف، انقر على main.bundle.js أثناء التواجد في لوحة الشبكة في DevTools. الآن، انقر على علامة التبويب الردّ.

الاستجابة المُكثَّفة

يظهر الرمز في شكله النهائي، مُصغّرًا ومُعدَّلاً، في نص الاستجابة. لمعرفة حجم الحزمة التي كانت ستبلغه في حال عدم تصغيرها، افتحملف webpack.config.js وعدِّل إعدادات mode.

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

أعِد تحميل التطبيق واطّلِع على حجم الحزمة مرة أخرى من خلال لوحة الشبكة في IDE.

حجم الحِزمة 767 كيلوبايت

هذا فرق كبير جدًا. 😅

احرص على التراجع عن التغييرات هنا قبل المتابعة.

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

يعتمد تضمين عملية تصغير الرمز في تطبيقك على الأدوات التي تستخدمها:

  • في حال استخدام الإصدار 4 من webpack أو إصدار أحدث، ليس عليك إجراء أي عمل إضافي، لأنّه يتم تصغير الرمز تلقائيًا في وضع الإنتاج. 👍
  • في حال استخدام إصدار قديم من webpack، يجب تثبيت TerserWebpackPlugin وتضمينها في عملية إنشاء webpack. يوضّح المستند هذا بالتفصيل.
  • تتوفّر أيضًا مكوّنات إضافية أخرى لتصغير الملفات ويمكن استخدامها بدلاً من ذلك، مثل BabelMinifyWebpackPlugin وClosureCompilerPlugin.
  • إذا لم يتم استخدام أداة تجميع وحدات على الإطلاق، استخدِم Terser كأداة لواجهة سطر أوامر أو أدرِجه مباشرةً كعنصر تابع.

الضغط

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

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

مع كل طلب واستجابة HTTP، يمكن للمتصفّحات وخوادم الويب إضافة عناوين لتضمين معلومات إضافية عن مادة العرض التي يتم جلبها أو استلامها. يمكن الاطّلاع على ذلك في علامة التبويب Headers ضمن لوحة "الشبكة" في DevTools، حيث يتم عرض ثلاثة أنواع:

  • يمثّل عام العناوين العامة ذات الصلة بالتفاعل الكامل بين الطلب والرد.
  • تعرِض رؤوس الاستجابة قائمة برؤوس خاصة بالاستجابة الفعلية من الخادم.
  • تعرِض عناوين الطلبات قائمة بالعناوين المرفقة بالطلب من قِبل العميل.

اطّلِع على عنوان accept-encoding في Request Headers.

قبول عنوان التشفير

يستخدم المتصفّح السمة accept-encoding لتحديد تنسيقات الترميز للمحتوى أو خوارزميات الضغط التي يتوافق معها. هناك العديد من خوارزميات ضغط النصوص، ولكن هناك ثلاث خوارزميات فقط متوافقة هنا لضغط (وفك ضغط) طلبات شبكة HTTP:

  • Gzip (gzip): تنسيق الضغط الأكثر استخدامًا لتفاعلات الخادم والعميل ويستند إلى خوارزمية Deflate ، وهو متوافق مع جميع المتصفحات الحالية.
  • Deflate (deflate): لا يتم استخدامه بشكل شائع.
  • Brotli (br): خوارزمية ضغط أحدث تهدف إلى تحسين نسب الضغط بشكلٍ أكبر، ما يمكن أن يؤدي إلى loadingتحميل الصفحات بشكلٍ أسرع. وهو متاح في أحدث إصدارات معظم المتصفّحات.

نموذج التطبيق في هذا الدليل التعليمي مطابق للتطبيق الذي أكملته في الدرس التطبيقي حول الترميز بعنوان "إزالة الرمز غير المستخدَم"، باستثناء أنّه يتم الآن استخدام مكتبة Express كإطار عمل لخادم. في القسمين التاليين، تتم مناقشة كل من الضغط الثابت والديناميكي.

الضغط الديناميكي

يتضمن الضغط الديناميكي ضغط مواد العرض أثناء التشغيل عندما يطلبها المتصفّح.

الإيجابيات

  • ولا يلزم إنشاء نُسخ مضغوطة محفوظة من مواد العرض وتعديلها.
  • يعمل الضغط أثناء التنقل بشكلٍ جيد بشكلٍ خاص مع صفحات الويب التي يتم إنشاؤها ديناميكيًا.

السلبيات

  • إنّ ضغط الملفات بمستويات أعلى لتحقيق نسب ضغط أفضل يستغرق وقتًا أطول. ويمكن أن يؤدي ذلك إلى انخفاض الأداء بينما ينتظر المستخدم ملفّات الأصول التي يتم ضغطها قبل أن يرسلها الخادم.

الضغط الديناميكي باستخدام Node/Express

يتولّى ملف server.js إعداد خادم Node الذي يستضيف التطبيق.

const express = require('express');

const app = express();

app.use(express.static('public'));

const listener = app.listen(process.env.PORT, function() {
  console.log('Your app is listening on port ' + listener.address().port);
});

لا يؤدي كل هذا حاليًا سوى استيراد express واستخدام express.static الوسيط لتحميل جميع ملفات HTML وJavaScript وCSS الثابتة في الدليل public/ (وتنشئ أداة webpack هذه الملفات مع كل عملية إنشاء).

للتأكّد من ضغط جميع مواد العرض في كل مرة يتم فيها طلبها، يمكن استخدام مكتبة الوسيط compression. ابدأ بإضافة المنتج كـ devDependency في package.json:

"devDependencies": {
  //...
  "compression": "^1.7.3"
},

واستورِده إلى ملف الخادم server.js:

const express = require('express');
const compression = require('compression');

وأضِفها كبرنامج وسيط قبل تثبيت express.static:

//...

const app = express();

app.use(compression());

app.use(express.static('public'));

//...

أعِد الآن تحميل التطبيق واطّلِع على حجم الحزمة في لوحة الشبكة.

حجم الحِزمة باستخدام ميزة &quot;التصغير الديناميكي&quot;

من 225 كيلوبايت إلى 61.6 كيلوبايت في Response Headers الآن، يعرض عنوان content-encoding أنّ الخادم يُرسِل هذا الملف المشفَّر باستخدام gzip.

رأس ترميز المحتوى

الضغط الثابت

إنّ الفكرة من ضغط المواد الثابتة هي ضغط مواد العرض وحفظها مسبقًا.

الإيجابيات

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

السلبيات

  • يجب ضغط مواد العرض مع كل عملية إنشاء. يمكن أن تزيد مدّة الإنشاء بشكل كبير في حال استخدام مستويات ضغط عالية.

الضغط الثابت باستخدام Node/Express وWebpack

بما أنّ الضغط الثابت ينطوي على ضغط الملفات مسبقًا، يمكن تعديل إعدادات webpack لضغط مواد العرض كجزء من خطوة الإنشاء. يمكن استخدام CompressionPlugin لهذا الغرض.

ابدأ بإضافة المنتج كـ devDependency في package.json:

"devDependencies": {
  //...
  "compression-webpack-plugin": "^1.1.11"
},

استورِده في ملف الإعدادات، مثل أي مكوّن إضافي آخر من webpack، webpack.config.js:

const path = require("path");

//...

const CompressionPlugin = require("compression-webpack-plugin");

وأدرِج هذا العنصر في مصفوفة plugins:

module.exports = {
  //...
  plugins: [
    //...
    new CompressionPlugin()
  ]
}

يضغط المكوّن الإضافي ملفات الإنشاء تلقائيًا باستخدام gzip. اطّلِع على المستندات لمعرفة كيفية إضافة خيارات لاستخدام خوارزمية مختلفة أو تضمين/استبعاد ملفات معيّنة.

عند إعادة تحميل التطبيق وإعادة إنشائه، يتم الآن إنشاء نسخة مضغوطة من الحزمة الرئيسية. افتح Glitch Console للاطّلاع على ما بداخل directory public/ النهائي الذي يعرضه خادم Node.

  • انقر على زر الأدوات.
  • انقر على الزر وحدة التحكّم.
  • في وحدة التحكّم، نفِّذ الأوامر التالية للانتقال إلى الدليل public والاطّلاع على جميع ملفاته:
cd public
ls

الملفات النهائية التي تمّت إخراجها في الدليل العام

تم أيضًا حفظ نسخة الحزمة المضغوطة بتنسيق gzip، وهي main.bundle.js.gz، هنا. يضغط CompressionPlugin أيضًا index.html تلقائيًا.

الخطوة التالية التي يجب اتّخاذها هي إخبار الخادم بإرسال هذه الملفات المضغوطة بتنسيق gzip عند طلب إصدارات JavaScript الأصلية. ويمكن إجراء ذلك من خلال تحديد مسار جديد في server.js قبل عرض الملفات باستخدام express.static.

const express = require('express');
const app = express();

app.get('*.js', (req, res, next) => {
  req.url = req.url + '.gz';
  res.set('Content-Encoding', 'gzip');
  next();
});

app.use(express.static('public'));

//...

يتم استخدام app.get لإعلام الخادم بطريقة الردّ على طلب GET لنقطة نهاية معيّنة. بعد ذلك، يتم استخدام دالة ردّ اتصال لتحديد كيفية معالجة هذا الطلب. ويعمل المسار على النحو التالي:

  • يشير تحديد '*.js' كوسيطة أولى إلى أنّ هذا يعمل مع كل نقطة نهاية يتم تشغيلها لجلب ملف JS.
  • في ردّ الاتصال، يتم إرفاق .gz بعنوان URL للطلب ويتم ضبط عنوان استجابة Content-Encoding على gzip.
  • أخيرًا، تضمن next() استمرار التسلسل إلى أيّ أسلوب استدعاء يليه.

بعد إعادة تحميل التطبيق، اطّلِع على لوحة Network مرة أخرى.

تقليل حجم الحِزمة باستخدام ميزة &quot;الضغط الثابت&quot;

كما هو الحال في السابق، تمّ تخفيض حجم الحِزمة بشكلٍ كبير.

الخاتمة

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