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

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

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

القياس

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

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

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

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

  1. اضغط على "Control+Shift+J" (أو "Command+Option+J" على نظام التشغيل Mac) لفتح "أدوات مطوري البرامج".
  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 B كثيرًا في البداية، ولكن كان لا يزال هناك انخفاض في الحجم بنسبة 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 ضمن لوحة "شبكة أدوات مطوّري البرامج" حيث تظهر ثلاثة أنواع:

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

يمكنك إلقاء نظرة على عنوان accept-encoding في Request Headers.

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

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

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

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

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

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

الإيجابيات

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

السلبيات

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

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

يكون الملف server.js مسؤولاً عن إعداد خادم العُقدة الذي يستضيف التطبيق.

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

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

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

إن الشيء التالي الذي يجب القيام به هو إخبار الخادم بإرسال هذه الملفات المضغوطة بطريقة gzip كلما تم طلب إصدارات JS الأصلية. ويمكن إجراء ذلك من خلال تحديد مسار جديد في 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;

وكما حدث من قبل، حدث انخفاض كبير في حجم الحزمة.

الخاتمة

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