تزداد تطبيقات الويب في الوقت الحالي بشكل كبير، خاصةً جزء JavaScript منها. اعتبارًا من منتصف عام 2018، أصبح متوسط حجم نقل ملفات JavaScript على الأجهزة الجوّالة بحجم 350 كيلوبايت تقريبًا. وهذا مجرد حجم نقل! غالبًا ما يتم ضغط JavaScript عند إرسالها عبر الشبكة، ما يعني أنّ المقدار الفعلي من JavaScript يزيد إلى حد ما بعد أن يفكّ المتصفح ضغطه. ومن المهم الإشارة إلى هذا الأمر، لأنّه بالنسبة إلى معالجة الموارد، ليس للضغط أي تأثير. 900 كيلوبايت من محتوى JavaScript غير المضغوط لا يزال بحجم 900 كيلوبايت للمحلل اللغوي والمحول البرمجي، على الرغم من أنه قد يبلغ حوالي 300 كيلوبايت عند ضغطه.
تُعد JavaScript موردًا مكلفًا للمعالجة. وعلى عكس الصور التي لا تستغرق سوى وقت تافه نسبيًا لفك الترميز بعد تنزيلها، يجب تحليل JavaScript وتجميعها ثم تنفيذها في النهاية. بايت، وهذا يجعل JavaScript أكثر تكلفة من أنواع الموارد الأخرى.
في حين أنّ التحسينات المستمرة تتم بهدف تحسين كفاءة محرّكات JavaScript، فإنّ تحسين أداء JavaScript هو مهمة تقع على عاتق المطوّرين كالعادة.
ولتحقيق هذه الغاية، ثمة أساليب لتحسين أداء JavaScript. تقسيم الرموز هو أحد الأساليب التي تعمل على تحسين الأداء عن طريق تقسيم JavaScript للتطبيق إلى أجزاء، وعرض هذه المقاطع إلى مسارات التطبيق الذي يحتاج إليها فقط.
على الرغم من نجاح هذا الأسلوب، فإنّه لا يعالج مشكلة شائعة في التطبيقات التي تحتوي على JavaScript، وهي تضمين رمز برمجي لا يتم استخدامه أبدًا. تحاول اهتزاز الشجرة حل هذه المشكلة.
ما هي هزة الشجرة؟
هزة الشجرة هو شكل من أشكال التخلص من الرموز البرمجية. تم نشر العبارة بواسطة دمج القنوات، لكن مفهوم إزالة الرمز المعطَّل أصبح متاحًا منذ فترة. وقد ظهر المفهوم أيضًا عملية الشراء في webpack، كما هو موضَّح في هذه المقالة من خلال نموذج تطبيق.
مصطلح "اهتزاز الشجرة" يأتي من النموذج العقلي لتطبيقك وتبعياته كهيكل يشبه الشجرة. تمثل كل عقدة في الشجرة تبعية توفر وظائف مختلفة لتطبيقك. في التطبيقات الحديثة، يتم جلب هذه التبعيات من خلال عبارات 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"
، التي قد تكون عددًا كبيرًا من الرموز، يستورد هذا المثال أجزاءً معيّنة فقط منها. في إصدارات dev، لا يؤدي ذلك إلى حدوث أي تغيير، إذ يتم استيراد الوحدة بأكملها بغض النظر عن ذلك. في إصدارات الإنتاج، يمكن ضبط webpack على "الاهتزاز". عمليات التصدير من وحدات ES6 التي لم يتم استيرادها بشكل صريح، ما يجعل إصدارات الإنتاج هذه أصغر. ستتعرف في هذا الدليل على كيفية إجراء ذلك!
إيجاد فرص لهز شجرة
لأغراض التوضيح، يتوفّر نموذج تطبيق من صفحة واحدة يوضّح طريقة عمل اهتزاز الأشجار. يمكنك استنساخ هذه المعلومات والمتابعة إذا أردت، لكننا سنتناول كل خطوة من الخطوات معًا في هذا الدليل، لذا فإن استنساخ ليس ضروريًا (إلا إذا كان التعلم التطبيقي هو ما يناسبك).
نموذج التطبيق عبارة عن قاعدة بيانات قابلة للبحث عن دوّاسات تأثير الغيتار. تُدخل استعلامًا وستظهر قائمة بدوّاسات التأثير.
ويتم فصل السلوك الذي يعمل على تشغيل هذا التطبيق حسب المورّد (أي Preact وEmotion) وحِزم الرموز الخاصة بالتطبيق (أو "الأجزاء"، كما تطلق عليها حزمة الويب):
حزم JavaScript الموضحة في الشكل أعلاه هي إصدارات إنتاج، أي أنه يتم تحسينها من خلال التقسيم. فحجم 21.1 كيلوبايت لحزمة خاصة بالتطبيق ليس سيئًا، ولكن تجدر الإشارة إلى أنّه لا هزّ أي شجرة على الإطلاق. لنلقي نظرة على رمز التطبيق ونرى ما يمكن فعله لإصلاح ذلك.
في أي تطبيق، يتضمّن العثور على فرص اهتزاز الشجرة البحث عن عبارات import
الثابتة. بالقرب من أعلى ملف المكوّن الرئيسي، سيظهر لك سطر على النحو التالي:
import * as utils from "../../utils/utils";
يمكنك استيراد وحدات ES6 بعدة طرق، ولكن ستلفت انتباهك وحدات كهذه. يعرض هذا السطر تحديدًا "import
كل شيء من وحدة utils
، ويضعه في مساحة اسم تُسمى utils
". والسؤال الكبير الذي يجب طرحه هنا هو: "ما مقدار العناصر في تلك الوحدة؟"
إذا نظرت إلى رمز المصدر للوحدة utils
، ستجد حوالي 1,300 سطر من الرموز.
هل تحتاج إلى كل هذه الأشياء؟ لنتحقق مرة أخرى من خلال البحث عن ملف المكوِّن الرئيسي الذي يستورد وحدة 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);
}
من بين ملف 1300 سطر يتضمن مجموعة من عمليات التصدير، يتم استخدام واحد منها فقط. ينتج عن ذلك شحن الكثير من محتوى JavaScript غير المستخدَم.
على الرغم من أن هذا المثال من التطبيق مفتعل بعض الشيء، إلا أنه لا يغير حقيقة أن هذا النوع من السيناريو الاصطناعي يشبه فرص التحسين الفعلية التي قد تواجهها في تطبيق ويب للإنتاج. الآن بعد أن حددت فرصة لاهتزاز الشجرة لتكون مفيدة، كيف يتم ذلك بالفعل؟
حماية Babel من تحويل وحدات ES6 إلى وحدات CommonJS
تُعدّ أداة Babel أداة لا غنى عنها، ولكنّها قد تزيد من صعوبة ملاحظة آثار هزّ الأشجار. في حال استخدام @babel/preset-env
، قد تحوّل Babel وحدات ES6 إلى وحدات CommonJS أكثر توافقًا على نطاق واسع، أي الوحدات require
بدلاً من import
.
نظرًا إلى صعوبة تنفيذ اهتزاز الشجرة بالنسبة إلى وحدات 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 يعمل على النحو المطلوب، ما يسمح لحزمة الويب بتحليل شجرة التبعية والتخلّص من الاعتماديات غير المستخدَمة.
مع أخذ الآثار الجانبية في الاعتبار
هناك جانب آخر يجب مراعاته عند هز التبعيات من تطبيقك وهو ما إذا وحدات المشروع لديك آثار جانبية. مثال على الأثر الجانبي تعدّل عنصرًا خارج نطاقها الخاص، ما يُعدّ تأثيرًا جانبيًا. طريقة تنفيذها:
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، وهذا مهم في سياق اهتزاز الأشجار. الوحدات التي تأخذ مدخلات يمكن توقعها وتنتج مخرجات يمكن التنبؤ بها على نحوٍ متساوٍ بدون تعديل أي شيء خارج نطاقها هي تلك التبعيات التي يمكن إسقاطها بأمان في حالة عدم استخدامها. وهي عبارة عن قِطع من الرموز البرمجية مجمّعة مستقلة. ومن هنا جاءت عبارة "الوحدات".
بالنسبة إلى حزمة الويب، يمكن استخدام تلميح لتحديد أنّ الحزمة وتبعياتها خالية من الآثار الجانبية من خلال تحديد "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
، يمكنك أيضًا تحديد هذه العلامة في إعداد حزمة الويب من خلال 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% تقريبًا. ولا يؤدي ذلك إلى تقليل الوقت الذي يستغرقه النص البرمجي في عملية التنزيل فحسب، بل إلى تقليل وقت المعالجة أيضًا.
هزّ بعض الأشجار.
مهما كانت المسافة المقطوعة التي تخرجها من هزة الشجرة تعتمد على تطبيقك وتبعياته وبنيته. جرّب الآن إذا كنت تعلم أنك لم تُعِدّ أداة دمج الوحدات لإجراء هذا التحسين، فلا داعٍ من المحاولة ومعرفة كيف يفيد تطبيقك.
قد تدرك مكاسب كبيرة في الأداء بسبب هزّ الشجرة، أو ليس كثيرًا على الإطلاق. ولكن من خلال ضبط نظام التصميم للاستفادة من هذا التحسين في الإصدارات التجريبية والاستيراد بشكل انتقائي لما يحتاجه تطبيقك فقط، سيتم بشكل استباقي إبقاء حِزم التطبيقات صغيرة قدر الإمكان.
نشكر "كريستوف باكستر" وجيسون ميلر وأدي عثماني وجيف بوسنيك و"سام ساكون" وفيليب والتون على ملاحظاتهم القيّمة التي ساهمت في تحسين جودة هذه المقالة بشكل كبير.