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

التعرّف على كيفية تأثير وحدات 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 في وحدة التحكّم.

نظرًا لعدم وجود نظام وحدات موحَّدة في المتصفِّح في بدايات العقد الثاني من القرن الحادي والعشرين، أصبح CommonJS تنسيق وحدات شائعًا لمكتبات JavaScript من جهة العميل أيضًا.

كيف تؤثر لغة CommonJS في الحجم النهائي لحزمتك؟

حجم تطبيق JavaScript من جهة الخادم ليس بالغ الأهمية كما هو الحال في المتصفح، ولهذا لم يتم تصميم CommonJS مع وضع تقليل حجم حزمة الإنتاج في الاعتبار. وفي الوقت نفسه، يوضِّح التحليل أن حجم حزمة 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 إلى زيادة حجم حزمة الناتج بمقدار 16000 مرة تقريبًا؟ بالطبع، هذا مثال لعبة، في الواقع، قد لا يكون فرق الحجم كبيرًا، ولكن من المحتمل أن تضيف 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));**

/******/ })();

في الإخراج أعلاه، توجد جميع الدوال داخل مساحة الاسم نفسها. لمنع التضاربات، أعادت حزمة الويب تسمية الدالة 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 إضافي تابع لجهة خارجية. على الرغم من أن هذا المكون الإضافي يدعم اهتزاز الشجرة، إلا أنه لا يغطي جميع الطرق المختلفة التي يمكن أن تستخدم بها تبعياتك CommonJS. وهذا يعني أنّك لا تحصل على الضمانات نفسها التي تحصل عليها في وحدات ES. بالإضافة إلى ذلك، تضيف هذه الميزة تكلفة إضافية كجزء من عملية التصميم بالإضافة إلى سلوك webpack التلقائي.

الخاتمة

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

إليك بعض النصائح القابلة للتنفيذ للتأكد من أنك على المسار الأمثل:

  • استخدام node-resolve في Rollup.js وتعيين علامة modulesOnly لتحديد أنك تريد الاعتماد على وحدات ECMAScript فقط.
  • استخدام الحزمة is-esm للتحقق من أن حزمة npm تستخدم وحدات ECMAScript.
  • إذا كنت تستخدم Angular، ستتلقّى تلقائيًا تحذيرًا إذا كنت تعتمد على وحدات لا يمكن اهتزازها على شكل شجرة.