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

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

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

قياس

قبل الشروع في إضافة تحسينات، من الجيد دائمًا تحليل الحالة الراهنة للتطبيق.

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

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

يمكنك الآن إلقاء نظرة على حجم هذا التطبيق:

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

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

على الرغم من إحراز الكثير من التقدّم في الدرس التطبيقي حول الترميز "Remove unused code" (إزالة الرمز غير المستخدَم) لخفض حجم الحزمة، لا يزال حجم 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 وأنت في لوحة الشبكة في "أدوات مطوّري البرامج". انقر الآن على علامة التبويب الرد.

ردّ مصغّر

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

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

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

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

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

تأكَّد من التراجع عن التغييرات هنا قبل المتابعة.

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

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

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

الضغط

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

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

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

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

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

قبول عنوان الترميز

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

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

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

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

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

الإيجابيات

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

السلبيات

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

الضغط الديناميكي باستخدام 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 وJS وCSS الثابتة في الدليل public/ (ويتم إنشاء هذه الملفات بواسطة حزمة الويب مع كلّ إصدار).

للتأكّد من ضغط جميع مواد العرض في كل مرة يتم فيها طلبها، يمكن استخدام مكتبة البرمجيات الوسيطة 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'));

//...

يمكنك الآن إعادة تحميل التطبيق وإلقاء نظرة على حجم الحزمة في لوحة الشبكة.

حجم الحزمة مع الضغط الديناميكي

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

عنوان ترميز المحتوى

الضغط الثابت

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

الإيجابيات

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

السلبيات

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

ضغط ثابت مع Node/Express وwebpack

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

ابدأ بإضافته كـ devDependency في package.json:

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

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

const path = require("path");

//...

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

وأدرِجها في مصفوفة plugins:

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

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

وعند إعادة تحميل التطبيق وإعادة إنشائه، يتم الآن إنشاء نسخة مضغوطة من الحزمة الرئيسية. افتح Glitch Console لإلقاء نظرة على محتوى دليل 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 مرة أخرى.

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

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

الخلاصة

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