تصغير حمولات الشبكة وضغطها باستخدام 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 B. إذا تم تشويهها بشكل أكبر عن طريق تقليل طول اسم المتغير وتعديل بعض التعبيرات، فقد ينتهي الأمر بالمظهر النهائي كما يلي:

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

الضغط

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

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

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

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

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

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

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

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

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

وللتأكّد من ضغط جميع مواد العرض في كل مرة يتم فيها طلبها، يمكن استخدام مكتبة البرمجيات الوسيطة الضغط. ابدأ بإضافته كـ 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/ النهائي الذي يعرضه خادم العقدة.

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

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

تم حفظ الإصدار المضغوط من الحزمة main.bundle.js.gz هنا أيضًا. يضغط 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' كوسيطة أولى أنّ هذه الطريقة مناسبة لكل نقطة نهاية يتم تنشيطها لجلب ملف JavaScript.
  • ضمن معاودة الاتصال، يتم إرفاق .gz بعنوان URL للطلب، ويتم ضبط عنوان الاستجابة Content-Encoding على gzip.
  • وأخيرًا، تضمن السمة next() استمرار التسلسل في أي معاودة اتصال قد تكون بعد ذلك.

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

تقليل حجم الحزمة عن طريق الضغط الثابت

وتمامًا كما كان الحال من قبل، سيتم خفض حجم الحزمة بشكل كبير!

الخلاصة

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