يمكن أن تصبح تطبيقات الويب الحالية كبيرة جدًا، لا سيما الجزء الذي يستخدم JavaScript. في منتصف عام 2018، قدّر موقع HTTP Archive متوسط حجم نقل JavaScript على الأجهزة الجوّالة بنحو 350 كيلوبايت. وهذا هو حجم النقل فقط. يتم غالبًا ضغط JavaScript عند إرساله عبر الشبكة، ما يعني أنّ حجم JavaScript الفعلي يكون أكبر بكثير بعد أن يفكّ المتصفّح ضغطه. من المهم الإشارة إلى ذلك، لأنّه فيما يتعلّق بمعالجة الموارد، لا صلة لعملية الضغط بالموضوع. يظل حجم JavaScript غير المضغوط البالغ 900 كيلوبايت هو 900 كيلوبايت بالنسبة إلى المحلّل والمترجم، حتى لو كان حجمه المضغوط يبلغ 300 كيلوبايت تقريبًا.
تُعدّ لغة JavaScript من الموارد المكلفة من حيث المعالجة. على عكس الصور التي لا تستغرق وقتًا طويلاً نسبيًا في فك الترميز بعد تنزيلها، يجب تحليل JavaScript وتجميعها ثم تنفيذها في النهاية. وهذا يجعل JavaScript أكثر تكلفة من الأنواع الأخرى من الموارد.
مع أنّ التحسينات تُجرى باستمرار لتحسين كفاءة محركات JavaScript، يظلّ تحسين أداء JavaScript من مسؤولية المطوّرين، كما هو الحال دائمًا.
لتحقيق ذلك، هناك أساليب لتحسين أداء JavaScript. تقسيم الرموز البرمجية هو إحدى هذه التقنيات التي تحسّن الأداء من خلال تقسيم JavaScript للتطبيق إلى أجزاء، وعرض هذه الأجزاء فقط على مسارات التطبيق التي تحتاج إليها.
على الرغم من أنّ هذه التقنية فعّالة، إلا أنّها لا تعالج مشكلة شائعة في التطبيقات التي تعتمد بشكل كبير على JavaScript، وهي تضمين رمز برمجي لا يتم استخدامه مطلقًا. تحاول عملية حذف الرموز غير المستخدَمة حلّ هذه المشكلة.
ما هي عملية حذف الرموز غير المستخدَمة؟
Tree shaking هي شكل من أشكال إزالة الرموز غير المستخدَمة. اشتهر مصطلح "إزالة الرموز البرمجية غير المستخدَمة" بفضل Rollup، ولكنّ مفهوم إزالة الرموز البرمجية غير المستخدَمة كان موجودًا منذ بعض الوقت. وقد تمّ اعتماد هذا المفهوم أيضًا في webpack، كما هو موضّح في هذه المقالة من خلال نموذج تطبيق.
يأتي مصطلح "إزالة الأجزاء غير المستخدَمة" من النموذج الذهني لتطبيقك والعناصر التابعة له على شكل بنية شبيهة بالشجرة. تمثّل كل عقدة في الشجرة عنصرًا تابعًا يوفّر وظيفة مميّزة لتطبيقك. في التطبيقات الحديثة، يتم إدخال هذه العناصر التابعة من خلال عبارات static import على النحو التالي:
// Import all the array utilities!
import arrayUtils from "array-utils";
عندما يكون التطبيق جديدًا، قد يكون لديه عدد قليل من التبعيات. ويستخدم أيضًا معظم التبعيات التي تضيفها، إن لم يكن كلها. ومع ذلك، عند اكتمال تطبيقك، يمكن إضافة المزيد من التبعيات. ومما يزيد الأمر تعقيدًا أنّ التبعيات القديمة لم يعُد يتم استخدامها، ولكن قد لا يتم إزالتها من قاعدة الرموز البرمجية. والنتيجة النهائية هي أنّ التطبيق ينتهي به الأمر إلى تضمين الكثير من ملفات JavaScript غير المستخدَمة. تعالج عملية "إزالة الرموز غير المستخدَمة" هذه المشكلة من خلال الاستفادة من الطريقة التي تجلب بها عبارات import الثابتة أجزاءً معيّنة من وحدات ES6:
// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";
الفرق بين مثال import هذا والمثال السابق هو أنّه بدلاً من استيراد كل شيء من الوحدة "array-utils" (التي قد تتضمّن الكثير من الرموز البرمجية)، يستورد هذا المثال أجزاءً محدّدة منها فقط. في الإصدارات التجريبية، لا يؤدي ذلك إلى تغيير أي شيء، لأنّه يتم استيراد الوحدة بأكملها على أي حال. في إصدارات الإنتاج، يمكن ضبط Webpack على "إزالة" عمليات التصدير من وحدات ES6 التي لم يتم استيرادها بشكل صريح، ما يؤدي إلى تصغير حجم إصدارات الإنتاج هذه. في هذا الدليل، سنتعرّف على كيفية إجراء ذلك.
العثور على فرص لهزّ شجرة
لأغراض توضيحية، يتوفّر تطبيق نموذجي من صفحة واحدة يوضّح طريقة عمل ميزة "إزالة الرموز غير المستخدَمة". يمكنك استنساخها ومتابعتها إذا أردت، ولكننا سنشرح كل خطوة في هذا الدليل، لذا ليس من الضروري استنساخها (إلا إذا كنت تفضّل التعلّم العملي).
نموذج التطبيق هو قاعدة بيانات قابلة للبحث عن دواسات تأثير الغيتار. أدخِل طلب بحث وستظهر قائمة بدوّاسات المؤثرات الصوتية.
يتم تقسيم السلوك الذي يحفّز هذا التطبيق إلى بائع (أي Preact وEmotion) وحِزم الرموز البرمجية الخاصة بالتطبيقات (أو "الأجزاء"، كما يسمّيها webpack):
حِزم JavaScript المعروضة في الشكل أعلاه هي إصدارات مخصّصة للإنتاج، ما يعني أنّه تم تحسينها من خلال التشفير. إنّ حجم 21.1 كيلوبايت لحزمة خاصة بالتطبيق ليس سيئًا، ولكن يجب ملاحظة أنّه لا يتم إجراء أي عملية تقليل لحجم الرموز البرمجية غير المستخدَمة. لنلقِ نظرة على رمز التطبيق ونرى ما يمكن فعله لحلّ هذه المشكلة.
في أي تطبيق، ستتضمّن عملية البحث عن فرص تقليل حجم الرمز البرمجي البحث عن عبارات import ثابتة. بالقرب من أعلى ملف المكوّن الرئيسي، سيظهر لك سطر مثل هذا:
import * as utils from "../../utils/utils";
يمكنك استيراد وحدات ES6 بطرق مختلفة، ولكن يجب أن تلفت انتباهك الوحدات المشابهة لما يلي. يقول هذا السطر تحديدًا "import كل شيء من الوحدة utils، وضعه في مساحة اسم تُسمى utils". السؤال الكبير الذي يجب طرحه هنا هو "ما هو مقدار المحتوى الموجود في هذه الوحدة؟"
إذا نظرت إلى رمز المصدر الخاص بوحدة utils، ستجد أنّ هناك حوالي 1,300 سطر من الرموز البرمجية.
هل تحتاج إلى كل هذه الأشياء؟ لنتأكّد من ذلك من خلال البحث عن ملف المكوّن الرئيسي الذي يستورد الوحدة utils لمعرفة عدد المرات التي يظهر فيها مساحة الاسم هذه.
utils التي استوردنا منها الكثير من الوحدات ثلاث مرات فقط ضمن ملف المكوّن الرئيسي.
اتّضح أنّ مساحة الاسم utils تظهر في ثلاثة مواضع فقط في تطبيقنا، ولكن ما هي الوظائف التي تظهر فيها؟ إذا ألقيت نظرة على ملف المكوّن الرئيسي مرة أخرى، ستلاحظ أنّه يتضمّن دالة واحدة فقط، وهي utils.simpleSort، التي تُستخدَم لترتيب قائمة نتائج البحث حسب عدد من المعايير عند تغيير القوائم المنسدلة الخاصة بالترتيب:
if (this.state.sortBy === "model") {
// `simpleSort` gets used here...
json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
// ..and here...
json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
// ..and here.
json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}
من بين ملف يحتوي على 1,300 سطر مع مجموعة من عمليات التصدير، يتم استخدام عملية واحدة فقط. ويؤدي ذلك إلى إرسال الكثير من JavaScript غير المستخدَم.
على الرغم من أنّ هذا المثال على التطبيق يبدو مصطنعًا بعض الشيء، إلا أنّه لا يغيّر حقيقة أنّ هذا النوع من السيناريوهات الاصطناعية يشبه فرص التحسين الفعلية التي قد تواجهها في تطبيق ويب قيد الإنتاج. بعد أن حدّدت فرصة للاستفادة من ميزة "إزالة الأجزاء غير المستخدَمة"، كيف يتم تنفيذها فعليًا؟
منع Babel من تحويل وحدات ES6 إلى وحدات CommonJS
Babel هي أداة لا غنى عنها، ولكنّها قد تجعل ملاحظة تأثيرات تقليل حجم الرمز البرمجي غير المستخدَم أكثر صعوبة. إذا كنت تستخدم @babel/preset-env، قد يحوّل Babel وحدات ES6 إلى وحدات CommonJS أكثر توافقًا على نطاق واسع، أي الوحدات التي require بدلاً من import.
وبما أنّ عملية tree shaking أكثر صعوبة بالنسبة إلى وحدات CommonJS، لن يعرف webpack ما يجب إزالته من الحِزم إذا قررت استخدامها. الحلّ هو ضبط @babel/preset-env لترك وحدات ES6 بدون تغيير بشكل صريح. أينما ضبطت إعدادات Babel، سواء في babel.config.js أو package.json، يتضمّن ذلك إضافة بعض العناصر الإضافية:
// babel.config.js
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
يؤدي تحديد modules: false في إعدادات @babel/preset-env إلى جعل Babel يتصرف على النحو المطلوب، ما يسمح لـ webpack بتحليل شجرة الاعتماديات وإزالة الاعتماديات غير المستخدَمة.
مراعاة الآثار الجانبية
من الجوانب الأخرى التي يجب مراعاتها عند إزالة التبعيات غير المستخدَمة من تطبيقك ما إذا كانت وحدات مشروعك تتضمّن آثارًا جانبية. من الأمثلة على التأثيرات الجانبية عندما تعدّل دالة شيئًا خارج نطاقها، وهو ما يُعرف باسم التأثير الجانبي لتنفيذها:
let fruits = ["apple", "orange", "pear"];
console.log(fruits); // (3) ["apple", "orange", "pear"]
const addFruit = function(fruit) {
fruits.push(fruit);
};
addFruit("kiwi");
console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]
في هذا المثال، ينتج عن addFruit تأثير جانبي عندما يعدّل مصفوفة fruits، التي تقع خارج نطاقه.
تنطبق الآثار الجانبية أيضًا على وحدات ES6، وهذا مهم في سياق تقليل حجم الرمز البرمجي. الوحدات التي تتلقّى مدخلات يمكن توقّعها وتنتج مخرجات يمكن توقّعها بالمثل بدون تعديل أي شيء خارج نطاقها هي تبعيات يمكن إزالتها بأمان إذا لم نكن نستخدمها. وهي عبارة عن أجزاء نمطية من الرمز البرمجي. ومن هنا جاءت تسمية "الوحدات".
في ما يتعلّق بـ webpack، يمكن استخدام تلميح لتحديد أنّ الحزمة وتبعياتها لا تتضمّن أي آثار جانبية من خلال تحديد "sideEffects": false في ملف package.json الخاص بالمشروع:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": false
}
بدلاً من ذلك، يمكنك إخبار webpack بالملفات المحدّدة التي لا تكون خالية من الآثار الجانبية:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
في المثال الأخير، سيتم افتراض أنّ أي ملف لم يتم تحديده لا يتضمّن آثارًا جانبية. إذا كنت لا تريد إضافة ذلك إلى ملف package.json، يمكنك أيضًا تحديد هذه العلامة في إعدادات Webpack من خلال module.rules.
استيراد ما هو مطلوب فقط
بعد توجيه Babel بعدم تغيير وحدات ES6، يجب إجراء تعديل بسيط على بنية import لاستيراد الدوال المطلوبة فقط من وحدة utils. في مثال هذا الدليل، كل ما هو مطلوب هو الدالة simpleSort:
import { simpleSort } from "../../utils/utils";
بما أنّه يتم استيراد simpleSort فقط بدلاً من الوحدة utils بأكملها، يجب تغيير كل مثيل من utils.simpleSort إلى simpleSort:
if (this.state.sortBy === "model") {
json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
json = simpleSort(json, "type", this.state.sortOrder);
} else {
json = simpleSort(json, "manufacturer", this.state.sortOrder);
}
من المفترض أن يكون هذا كل ما هو مطلوب لكي تعمل عملية تقليل حجم الرموز البرمجية غير المستخدَمة في هذا المثال. في ما يلي ناتج webpack قبل إزالة الأجزاء غير المستخدَمة من شجرة التبعيات:
Asset Size Chunks Chunk Names
js/vendors.16262743.js 37.1 KiB 0 [emitted] vendors
js/main.797ebb8b.js 20.8 KiB 1 [emitted] main
في ما يلي الناتج بعد نجاح عملية تقليل حجم الشجرة:
Asset Size Chunks Chunk Names
js/vendors.45ce9b64.js 36.9 KiB 0 [emitted] vendors
js/main.559652be.js 8.46 KiB 1 [emitted] main
على الرغم من أنّ حجم الحزمتين قد انخفض، إلا أنّ الحزمة main هي التي تستفيد أكثر من غيرها. من خلال إزالة الأجزاء غير المستخدَمة من الوحدة utils، يقل حجم الحزمة main بنسبة %60 تقريبًا. لا يؤدي ذلك إلى تقليل الوقت الذي يستغرقه تنزيل النص البرمجي فحسب، بل يقلّل أيضًا من وقت المعالجة.
هيا بنا نهزّ بعض الأشجار!
يعتمد عدد الكيلومترات التي يمكنك قطعها باستخدام ميزة "إزالة الرموز غير المستخدَمة" على تطبيقك والعناصر التابعة له وبنيته. جرِّبها الآن. إذا كنت متأكدًا من أنّك لم تُعدّ حزمة الوحدات النمطية لإجراء عملية التحسين هذه، يمكنك تجربة ذلك ومعرفة مدى استفادة تطبيقك منها.
قد تحقّق تحسّنًا كبيرًا في الأداء من خلال عملية حذف الأجزاء غير المستخدَمة، أو قد لا تحقّق أي تحسّن على الإطلاق. ولكن من خلال ضبط نظام التصميم للاستفادة من هذا التحسين في الإصدارات المخصّصة للإنتاج واستيراد ما يحتاجه تطبيقك فقط بشكل انتقائي، ستتمكّن من الحفاظ على حجم حِزم التطبيق في أصغر حجم ممكن بشكل استباقي.
نتوجّه بالشكر إلى "كريستوفر باكستر" وجيسون ميلر وآدي أوسماني وجيف بوسنيك و"سام ساكون" وفيليب والتون على ملاحظاتهم القيّمة التي ساهمت بشكل كبير في تحسين جودة هذه المقالة.