تصغير حمولات الشبكة وضغطها باستخدام 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 بايت كبيرًا في البداية، ولكن مع ذلك، انخفض حجمه بنسبة %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',
  //...

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

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

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

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

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

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

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

الضغط

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

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

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

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

ألقِ نظرة على العنوان accept-encoding في Request Headers.

عنوان Accept-Encoding

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

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

التطبيق النموذجي في هذا البرنامج التعليمي مطابق للتطبيق الذي تم إكماله في الدرس التطبيقي حول الترميز "إزالة الرمز غير المستخدَم"، باستثناء أنّه يتم الآن استخدام 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 وJS و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'));

//...

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

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

من 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 للاطّلاع على محتوى الدليل 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 مرة أخرى.

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

وكما كان الحال من قبل، تم تقليل حجم الحزمة بشكل كبير.

الخاتمة

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