كيفية استفادة مؤسسة 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 في وحدة التحكّم.

نظرًا لعدم وجود نظام وحدات موحد في المتصفح في أوائل عام 2010، أصبح 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. بالإضافة إلى ذلك، تم تضمين الدالة add في console.log من قِبل terser (وهو أداة تصغير JavaScript التي يستخدمها webpack).

وأحد الأسئلة العادلة التي قد تطرحها هو: لماذا يؤدي استخدام 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));**

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

في الناتج أعلاه، توجد جميع الدوال داخل نفس مساحة الاسم. لمنع الاصطدامات، أعادت حزمة الويب تسمية الدالة subtract في index.js إلى index_subtract.

إذا عالجت أداة مصغّر رمز المصدر أعلاه، فستنفّذ ما يلي:

  • أزِل الدالتَين غير المستخدَمتَين subtract وindex_subtract.
  • إزالة كل التعليقات والمسافات البيضاء المكررة
  • أن يتم تضمين نص الدالة add في استدعاء console.log

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

الخلاصة

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

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

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