التعرّف على كيفية تأثير وحدات CommonJS في عملية إزالة المحتوى غير المُستخدَم من تطبيقك
في هذه المشاركة، سنلقي نظرة على CommonJS وسبب جعله حِزم JavaScript أكبر من اللازم.
الملخّص: لضمان أن يتمكّن أداة تجميع الحِزم من تحسين تطبيقك بنجاح، تجنَّب الاعتماد على وحدات CommonJS واستخدِم بنية وحدات ECMAScript في تطبيقك بالكامل.
ما هو CommonJS؟
CommonJS هو معيار يعود إلى عام 2009 وضع اصطلاحات لمكوّنات JavaScript. وكان الغرض منها في البداية الاستخدام خارج متصفّح الويب، وبشكل أساسي للتطبيقات من جهة الخادم.
باستخدام CommonJS، يمكنك تحديد الوحدات وتصدير الوظائف منها واستيرادها في وحدات أخرى. على سبيل المثال، يحدِّد المقتطف أدناه وحدة تصدِّر خمس دوال: add
وsubtract
وmultiply
وdivide
وmax
:
// utils.js
const { maxBy } = require('lodash-es');
const fns = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
max: arr => maxBy(arr)
};
Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);
لاحقًا، يمكن أن تستورد وحدة أخرى بعض هذه الدوال أو جميعها وتستخدمها:
// index.js
const { add } = require('./utils.js');
console.log(add(1, 2));
سيؤدي استدعاء index.js
باستخدام node
إلى عرض الرقم 3
في وحدة التحكّم.
بسبب عدم توفّر نظام وحدات موحّد في المتصفّح في أوائل عام 2010، أصبح CommonJS تنسيقًا شائعًا للوحدات في مكتبات JavaScript من جهة العميل أيضًا.
كيف يؤثر CommonJS في حجم الحِزمة النهائي؟
لا يُعدّ حجم تطبيق JavaScript من جهة الخادم مهمًا بقدر ما هو مهم في المتصفّح، ولهذا السبب لم يتم تصميم CommonJS مع الأخذ في الاعتبار تقليل حجم حِزمة الإنتاج. في الوقت نفسه، يُظهر التحليل أنّ حجم حِزم JavaScript لا يزال السبب الأول في إبطاء تطبيقات المتصفّحات.
تُجري أدوات تجميع JavaScript وأدوات تصغير JavaScript، مثل webpack
وterser
، تحسينات مختلفة لتقليل حجم تطبيقك. وعند تحليل تطبيقك في وقت الإنشاء، تحاول هذه الأدوات إزالة أكبر قدر ممكن من رمز المصدر الذي لا تستخدمه.
على سبيل المثال، في المقتطف أعلاه، يجب أن تتضمّن الحِزمة النهائية دالة add
فقط لأنّ هذا هو الرمز الوحيد من utils.js
الذي تستورده في index.js
.
لننشئ التطبيق باستخدام إعدادات webpack
التالية:
const path = require('path');
module.exports = {
entry: 'index.js',
output: {
filename: 'out.js',
path: path.resolve(__dirname, 'dist'),
},
mode: 'production',
};
في ما يلي نحدّد أنّنا نريد استخدام تحسينات وضع الإنتاج واستخدام index.js
كنقطة دخول. بعد استدعاء webpack
، إذا اطّلعنا على حجم الإخراج، سنرى شيئًا على النحو التالي:
$ cd dist && ls -lah
625K Apr 13 13:04 out.js
يُرجى العلم أنّ حجم الحِزمة هو 625 كيلوبايت. إذا اطّلعنا على الإخراج، سنجد جميع الدوال من utils.js
بالإضافة إلى الكثير من الوحدات من lodash
. على الرغم من أنّنا لا نستخدم lodash
في index.js
، إلا أنّه جزء من الإخراج، ما يضيف الكثير من الأهمية إلى مواد العرض الخاصة بالإنتاج.
لنغيّر الآن تنسيق الوحدة إلى وحدات ECMAScript ونحاول مرة أخرى. سيظهر الرمز utils.js
هذه المرة على النحو التالي:
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;
import { maxBy } from 'lodash-es';
export const max = arr => maxBy(arr);
وسيستورد index.js
من utils.js
باستخدام بنية وحدة ECMAScript:
import { add } from './utils.js';
console.log(add(1, 2));
باستخدام إعدادات webpack
نفسها، يمكننا إنشاء تطبيقنا وفتح ملف الإخراج. أصبح حجمها الآن 40 بايت مع الإخراج التالي:
(()=>{"use strict";console.log(1+2)})();
يُرجى العِلم أنّ الحِزمة النهائية لا تحتوي على أيّ من الدوالّ من utils.js
التي لا نستخدمها، ولا تتضمّن أيّ أثر من lodash
. علاوةً على ذلك، تضمّن terser
(أداة تصغير JavaScript التي يستخدمها webpack
) دالة add
في console.log
.
قد تطرح سؤالاً معقولاً وهو لماذا يؤدي استخدام CommonJS إلى زيادة حجم حِزمة الإخراج بمقدار 16,000 مرة تقريبًا؟ بالطبع، هذا مثال بسيط، وقد لا يكون الفرق في الحجم كبيرًا في الواقع، ولكن من المحتمل أن تضيف CommonJS وزنًا كبيرًا إلى إصدار الإنتاج.
يصعب تحسين وحدات CommonJS بشكل عام لأنّها أكثر ديناميكية من وحدات ES. لضمان أن يتمكّن أداة تجميع الرموز البرمجية وأداة تصغيرها من تحسين تطبيقك بنجاح، تجنَّب الاعتماد على وحدات CommonJS واستخدِم بنية وحدات ECMAScript في تطبيقك بأكمله.
يُرجى العِلم أنّه حتى إذا كنت تستخدم وحدات ECMAScript في index.js
، سيتأثّر حجم حِزمة تطبيقك إذا كانت الوحدة التي تستخدمها هي وحدة CommonJS.
لماذا يجعل CommonJS تطبيقك أكبر حجمًا؟
للإجابة عن هذا السؤال، سننظر في سلوك ModuleConcatenationPlugin
في webpack
، وبعد ذلك سنناقش قابلية التحليل الثابت. يُجمِّع هذا المكوّن الإضافي نطاق جميع وحداتك في إغلاق واحد ويسمح لرمزك البرمجي بوقت تنفيذ أسرع في المتصفّح. لنلقِ نظرة على المثال التالي:
// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// index.js
import { add } from './utils.js';
const subtract = (a, b) => a - b;
console.log(add(1, 2));
في ما يلي وحدة ECMAScript التي نستوردها في index.js
. نحدِّد أيضًا دالة subtract
. يمكننا إنشاء المشروع باستخدام إعدادات webpack
نفسها المذكورة أعلاه، ولكن هذه المرة، سنوقف ميزة التصغير:
const path = require('path');
module.exports = {
entry: 'index.js',
output: {
filename: 'out.js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
minimize: false
},
mode: 'production',
};
لنلقِ نظرة على النتيجة التي تمّ إنشاؤها:
/******/ (() => { // webpackBootstrap
/******/ "use strict";
// CONCATENATED MODULE: ./utils.js**
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// CONCATENATED MODULE: ./index.js**
const index_subtract = (a, b) => a - b;**
console.log(add(1, 2));**
/******/ })();
في الإخراج أعلاه، تكون جميع الدوال داخل مساحة الاسم نفسها. لمنع حدوث تعارضات، أعاد Webpack تسمية الدالة subtract
في index.js
إلى index_subtract
.
إذا عالجت أداة تصغير الحجم رمز المصدر أعلاه، ستتم تنفيذ ما يلي:
- أزِل الدالتَين
subtract
وindex_subtract
غير المستخدَمتين. - إزالة جميع التعليقات والمسافات البيضاء المتكرّرة
- تضمين نص الدالة
add
في استدعاءconsole.log
يُشار إلى إزالة عمليات الاستيراد غير المستخدَمة باسم "إزالة العناصر غير الضرورية". لم يكن من الممكن إجراء عملية "إزالة المحتوى غير الضروري" إلا لأنّ webpack تمكّن من فهم الرموز التي نستوردها من utils.js
والرموز التي تصدّرها بشكل ثابت (أثناء وقت التصميم).
يكون هذا السلوك مفعّلاً تلقائيًا في وحدات ES لأنّها يسهل تحليلها بشكلٍ ثابت مقارنةً بـ CommonJS.
لنلقِ نظرة على المثال نفسه، ولكن هذه المرة سنغيّر utils.js
لاستخدام CommonJS بدلاً من وحدات ES:
// utils.js
const { maxBy } = require('lodash-es');
const fns = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
max: arr => maxBy(arr)
};
Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);
سيؤدي هذا التعديل البسيط إلى تغيير النتيجة بشكل كبير. بما أنّه طويل جدًا ولا يمكن تضمينه في هذه الصفحة، شاركنا جزءًا صغيرًا منه فقط:
...
(() => {
"use strict";
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(288);
const subtract = (a, b) => a - b;
console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .add */ .IH)(1, 2));
})();
لاحظ أنّ الحزمة النهائية تحتوي على بعض webpack
"وقت التشغيل": رمز مُدرَج مسؤول عن استيراد/تصدير الوظائف من الوحدات المجمّعة. هذه المرة، بدلاً من وضع كل الرموز من utils.js
وindex.js
ضمن مساحة الاسم نفسها، نطلب ديناميكيًا، أثناء التشغيل، دالة add
باستخدام __webpack_require__
.
هذا الإجراء ضروري لأنّه باستخدام CommonJS، يمكننا الحصول على اسم التصدير من تعبير عشوائي. على سبيل المثال، الرمز البرمجي أدناه هو بنية صالحة تمامًا:
module.exports[localStorage.getItem(Math.random())] = () => { … };
لا يمكن لأداة تجميع الرموز معرفة اسم الرمز الذي تم تصديره في وقت الإنشاء، لأنّ ذلك يتطلب معلومات لا تتوفّر إلا في وقت التشغيل، في سياق متصفّح المستخدم.
وبالتالي، لا يمكن لأداة التصغير فهم ما يستخدمه index.js
بالضبط من مكوّنات الاعتماد، وبالتالي لا يمكنها إزالة هذه المكوّنات. سنلاحظ السلوك نفسه بالضبط في الوحدات التابعة لجهات خارجية أيضًا. إذا استورَدنا وحدة CommonJS من node_modules
، لن تتمكّن سلسلة أدوات الإنشاء من تحسينها بشكل صحيح.
إزالة المحتوى غير المُستخدَم باستخدام CommonJS
من الصعب جدًا تحليل وحدات CommonJS لأنّها ديناميكية بطبيعتها. على سبيل المثال، يكون موقع الاستيراد في وحدات ES دائمًا سلسلة حرفية، مقارنةً بـ CommonJS، حيث يكون تعبيرًا.
في بعض الحالات، إذا كانت المكتبة التي تستخدمها تتّبع اصطلاحات محدّدة حول كيفية استخدامها لـ CommonJS، من الممكن إزالة عمليات التصدير غير المستخدَمة في وقت الإنشاء باستخدام webpack
plugin تابع لجهة خارجية. على الرغم من أنّ هذا المكوّن الإضافي يضيف ميزة تتبُّع مسار استدعاء الدوال البرمجية، إلا أنّه لا يشمل جميع الطرق المختلفة التي يمكن أن تستخدم بها التبعيات CommonJS. وهذا يعني أنّك لن تحصل على الضمانات نفسها التي تحصل عليها مع وحدات ES. بالإضافة إلى ذلك، تضيف هذه الميزة تكلفة إضافية كجزء من عملية الإنشاء بالإضافة إلى السلوك التلقائي webpack
.
الخاتمة
لضمان أن يتمكّن أداة تجميع الحِزم من تحسين تطبيقك بنجاح، تجنَّب الاعتماد على وحدات CommonJS واستخدِم بنية وحدات ECMAScript في تطبيقك بالكامل.
في ما يلي بعض النصائح التي يمكن تنفيذها للتأكّد من اتّباع المسار الأمثل:
- استخدِم المكوّن الإضافي node-resolve
في Rollup.js واضبط العلامة
modulesOnly
لتحديد أنّك تريد الاعتماد على وحدات ECMAScript فقط. - استخدِم الحزمة
is-esm
للتحقّق من أنّ حزمة npm تستخدم وحدات ECMAScript. - إذا كنت تستخدم Angular، ستتلقّى تلقائيًا تحذيرًا في حال الاعتماد على وحدات لا يمكن إزالة محتوى غير المُستخدَم منها.