يمكن أن تصبح تطبيقات الويب الحالية كبيرة جدًا، لا سيما الجزء الذي يستخدم JavaScript. اعتبارًا من منتصف عام 2018، يضع HTTP Archive متوسط حجم نقل JavaScript على الأجهزة الجوّالة عند 350 كيلوبايت تقريبًا. وهذا مجرد حجم عملية النقل. غالبًا ما يتم ضغط JavaScript عند إرساله عبر الشبكة، ما يعني أنّ الحجم الحقيقي لـ JavaScript يكون أكبر بكثير بعد أن يزيل المتصفّح ضغطه. من المهم الإشارة إلى ذلك، لأنّ عملية معالجة الموارد لا ترتبط بعملية الضغط. إنّ 900 كيلوبايت من JavaScript غير المضغوطة تظل 900 كيلوبايت في المُحلِّل والمُجمِّع، على الرغم من أنّ حجمها قد يكون 300 كيلوبايت تقريبًا عند ضغطها.
معالجة لغة JavaScript هي عملية مُكلفة. على عكس الصور التي لا تتطلّب سوى وقت فك ترميز بسيط نسبيًا بعد تنزيلها، يجب تحليل JavaScript وتجميعه ثم تنفيذه أخيرًا. وبذلك، يكون رمز JavaScript أكثر تكلفةً من أنواع الموارد الأخرى.
مع أنّه يتم إجراء تحسينات باستمرار لتحسين كفاءة محركات JavaScript، يبقى تحسين أداء JavaScript من مهام المطوّرين كالعادة.
ولهذا الغرض، هناك تقنيات لتحسين أداء JavaScript. تقسيم الرموز البرمجية هو إحدى هذه التقنيات التي تحسِّن الأداء من خلال تقسيم JavaScript للتطبيق إلى أجزاء، وعرض هذه الأجزاء على مسارات التطبيق التي تحتاج إليها فقط.
على الرغم من أنّ هذه الطريقة فعّالة، إلا أنّها لا تعالج مشكلة شائعة في التطبيقات التي تستخدم JavaScript بشكل كبير، وهي تضمين رمز لم يتم استخدامه مطلقًا. يهدف "هَزّ الشجرة" إلى حلّ هذه المشكلة.
ما هو "هَزّ الشجرة"؟
إزالة الأشجار هي شكل من أشكال إزالة الرموز غير الصالحة. انتشرت هذه العبارة من خلال أداة Rollup، ولكن كان مفهوم إزالة الرموز غير الصالحة متوفّرًا منذ بعض الوقت. وقد تم استخدام هذا المفهوم أيضًا في 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"
، التي قد تتضمّن الكثير من الرموز البرمجية، يستورد هذا المثال أجزاء معيّنة منها فقط. في إصدارات المطوّرين، لا يؤدي ذلك إلى تغيير أي شيء، لأنّه يتم استيراد الوحدة بأكملها بغض النظر عن ذلك. في عمليات الإنشاء المخصّصة للإصدار العلني، يمكن ضبط 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.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
.
بما أنّه من الصعب إجراء عملية "إزالة المحتوى غير المُستخدَم" في وحدات 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
، وهو خارج نطاق addFruit
.
تنطبق الآثار الجانبية أيضًا على وحدات 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
، يمكنك أيضًا تحديد هذه العلامة في إعداد حزمة الويب من خلال 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 تقريبًا. لا يقتصر هذا الإجراء على تقليل الوقت الذي يستغرقه النص البرمجي للتنزيل، بل يقلل أيضًا من وقت المعالجة.
طاب يومك.
تعتمد الاستفادة التي تحقّقها من ميزة "إزالة العناصر غير الضرورية" على تطبيقك وعناصره المُستخدَمة وبنيته. جرّب الآن إذا كنت متأكدًا من أنّك لم تُعدّ حِزمة الوحدات لتنفيذ هذا التحسين، لا بأس من تجربته ومعرفة مدى فائدته لتطبيقك.
قد تحقّق زيادة كبيرة في الأداء من خلال ميزة "تغيير الترتيب العشوائي"، أو قد لا تحقّق أيّ زيادة على الإطلاق. ولكن من خلال ضبط نظام الإنشاء للاستفادة من هذا التحسين في عمليات الإنشاء العلنية واستيراد ما يحتاجه تطبيقك فقط بشكل انتقائي، ستحافظ بشكل استباقي على حِزم تطبيقك صغيرة قدر الإمكان.
نشكر بشكل خاص "كريستوفير باكستر" وجيسون ميلر وأدي عثماني وجيف بوسنيك وسام ساكوني وفيليب والتون على ملاحظاتهم القيّمة التي أدّت إلى تحسين جودة هذه المقالة بشكل كبير.