كيفية استفادة مؤسسة CommonJS من حِزمك

التعرّف على كيفية تأثير وحدات CommonJS في عملية إزالة المحتوى غير المُستخدَم من تطبيقك

في هذه المشاركة، سنلقي نظرة على CommonJS وسبب جعله حِزم JavaScript أكبر من اللازم.

الملخّص: لضمان أن يتمكّن أداة تجميع الحِزم من تحسين تطبيقك بنجاح، تجنَّب الاعتماد على وحدات CommonJS واستخدِم بنية وحدات ECMAScript في تطبيقك بالكامل.

‫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، ستتلقّى تلقائيًا تحذيرًا في حال الاعتماد على وحدات لا يمكن إزالة أجزاء منها.