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 बंडल का साइज़, ब्राउज़र ऐप्लिकेशन के धीमे होने की बड़ी वजह है.

webpack और terser जैसे JavaScript बंडलर और मिनीफ़ायर, आपके ऐप्लिकेशन का साइज़ कम करने के लिए अलग-अलग ऑप्टिमाइज़ेशन करते हैं. बिल्ड के समय आपके ऐप्लिकेशन का विश्लेषण करते समय, वे उस सोर्स कोड से ज़्यादा से ज़्यादा डेटा हटाने की कोशिश करते हैं जिसका इस्तेमाल नहीं किया जा रहा है.

उदाहरण के लिए, ऊपर दिए गए स्निपेट में, आपके फ़ाइनल बंडल में सिर्फ़ 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 के कई मॉड्यूल मिलेंगे. हम index.js में lodash का इस्तेमाल नहीं करते हैं, लेकिन यह आउटपुट का हिस्सा है. इसलिए, हमारे प्रोडक्शन ऐसेट को बहुत ज़्यादा ध्यान में रखा जाता है.

अब हमें मॉड्यूल फ़ॉर्मैट को 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 ECMAScript मॉड्यूल सिंटैक्स का इस्तेमाल करके utils.js से इंपोर्ट करेगा:

import { add } from './utils.js';

console.log(add(1, 2));

webpack के उसी कॉन्फ़िगरेशन का इस्तेमाल करके, हम अपना ऐप्लिकेशन बना सकते हैं और आउटपुट फ़ाइल खोल सकते हैं. अब यह 40 बाइट हो गया है, जिसमें ये आउटपुट हैं:

(()=>{"use strict";console.log(1+2)})();

ध्यान दें कि फ़ाइनल बंडल में, utils.js का ऐसा कोई भी फ़ंक्शन नहीं है जिसका हम इस्तेमाल नहीं करते. साथ ही, lodash का कोई ट्रेस भी मौजूद नहीं है! इससे भी आगे जाकर, terser (webpack, JavaScript छोटा करने की सुविधा का इस्तेमाल करके) ने console.log में add फ़ंक्शन को इनलाइन किया.

आप एक सही सवाल पूछ सकते हैं कि CommonJS का इस्तेमाल करने से आउटपुट बंडल 16,000 गुना बड़ा क्यों हो गया है? बेशक, यह खिलौने का एक उदाहरण है, लेकिन हो सकता है कि साइज़ का अंतर इतना ज़्यादा न हो, लेकिन इस बात की संभावना है कि CommonJS आपके प्रोडक्शन बिल्ड को ज़्यादा अहमियत देगा.

CommonJS मॉड्यूल को सामान्य मामले में ऑप्टिमाइज़ करना मुश्किल है, क्योंकि वे ES मॉड्यूल की तुलना में ज़्यादा डाइनैमिक होते हैं. यह पक्का करने के लिए कि बंडलर और मिनीफ़ायर आपके ऐप्लिकेशन को सही से ऑप्टिमाइज़ कर सके, CommonJS मॉड्यूल पर निर्भर होने से बचें और अपने पूरे ऐप्लिकेशन में ECMAScript मॉड्यूल सिंटैक्स का इस्तेमाल करें.

ध्यान दें कि index.js में ECMAScript मॉड्यूल का इस्तेमाल करने के बावजूद, अगर आपका इस्तेमाल किया जा रहा मॉड्यूल एक CommonJS मॉड्यूल है, तो आपके ऐप्लिकेशन के बंडल के साइज़ पर असर पड़ेगा.

CommonJS आपके ऐप्लिकेशन को बड़ा क्यों करता है?

इस सवाल का जवाब देने के लिए, हम webpack में ModuleConcatenationPlugin के काम करने के तरीके को देखेंगे. इसके बाद, हम स्टैटिक विश्लेषण पर चर्चा करेंगे. यह प्लग इन आपके सभी मॉड्यूल के दायरे को एक साथ बंद कर देता है. साथ ही, इससे आपके कोड को ब्राउज़र में तेज़ी से काम करने में मदद मिलती है. आइए एक उदाहरण देखें:

// 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 ने index.js में मौजूद subtract फ़ंक्शन का नाम बदलकर index_subtract कर दिया है.

अगर छोटा करने वाला टूल, ऊपर दिए गए सोर्स कोड को प्रोसेस करता है, तो यह:

  • इस्तेमाल नहीं किए गए फ़ंक्शन subtract और index_subtract हटाएं
  • सभी टिप्पणियां और गै़र-ज़रूरी खाली सफ़ेद जगह हटाएं
  • console.log कॉल में add फ़ंक्शन के मुख्य भाग को इनलाइन करें

अक्सर डेवलपर इस्तेमाल नहीं किए गए इंपोर्ट को हटाने के तरीके को ट्री-शेकिंग मानते हैं. ट्री-शेकिंग सिर्फ़ इसलिए मुमकिन थी, क्योंकि वेबपैक को स्टैटिक तरीके (बनाने के समय) यह समझने में मदद मिली कि हम utils.js से कौनसे सिंबल इंपोर्ट कर रहे हैं और यह किन सिंबल को एक्सपोर्ट करता है.

यह व्यवहार ES मॉड्यूल के लिए डिफ़ॉल्ट रूप से चालू होता है, क्योंकि CommonJS की तुलना में उनका स्टैटिक तरीके से विश्लेषण किया जा सकता है.

चलिए, इसी तरह के उदाहरण को देखते हैं, लेकिन इस बार ES मॉड्यूल के बजाय CommonJS का इस्तेमाल करने के लिए, utils.js को बदलें:

// 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 के सभी सिंबल को एक ही नेमस्पेस में रखने के बजाय, हमें डाइनैमिक तौर पर, रनटाइम के दौरान __webpack_require__ का इस्तेमाल करने वाले add फ़ंक्शन की ज़रूरत होती है.

ऐसा करना ज़रूरी है, क्योंकि CommonJS की मदद से हम किसी आर्बिट्रेरी एक्सप्रेशन से, एक्सपोर्ट का नाम हासिल कर सकते हैं. उदाहरण के लिए, नीचे दिया गया कोड एक पूर्ण रूप से मान्य निर्माण है:

module.exports[localStorage.getItem(Math.random())] = () => { … };

बंडलर को बिल्ड के समय यह जानने का कोई तरीका नहीं होता कि एक्सपोर्ट किए गए सिंबल का नाम क्या है, क्योंकि इसके लिए ऐसी जानकारी की ज़रूरत होती है जो उपयोगकर्ता के ब्राउज़र के संदर्भ में, सिर्फ़ रनटाइम के दौरान उपलब्ध होती है.

इस तरह, मिनीफ़ायर यह नहीं समझ पाता कि index.js अपनी डिपेंडेंसी से असल में क्या इस्तेमाल करता है, ताकि यह उसे ट्री-शेक न कर सके. हम तीसरे पक्ष के मॉड्यूल के लिए भी यही तरीका देखेंगे. अगर हम node_modules से CommonJS मॉड्यूल इंपोर्ट करते हैं, तो आपका बिल्ड टूलचेन उसे सही तरीके से ऑप्टिमाइज़ नहीं कर पाएगा.

CommonJS के साथ ट्री-शेकिंग

CommonJS मॉड्यूल का विश्लेषण करना बहुत मुश्किल है, क्योंकि वे परिभाषा के हिसाब से डाइनैमिक होते हैं. उदाहरण के लिए, ES मॉड्यूल में इंपोर्ट करने की जगह, CommonJS की तुलना में, हमेशा एक स्ट्रिंग लिटरल होती है. यहां यह एक एक्सप्रेशन होता है.

कुछ मामलों में, अगर आपकी इस्तेमाल की जाने वाली लाइब्रेरी, CommonJS का इस्तेमाल करने के तरीके से जुड़े खास तौर-तरीकों का पालन करती है, तो किसी तीसरे पक्ष के webpack plugin का इस्तेमाल करके, बिल्ड के समय इस्तेमाल नहीं किए गए एक्सपोर्ट को हटाया जा सकता है. हालांकि, यह प्लग इन ट्री-शेकिंग के लिए सहायता जोड़ता है, लेकिन इसमें वे सभी अलग-अलग तरीके शामिल नहीं हैं जिनसे आपकी डिपेंडेंसी CommonJS का इस्तेमाल करती है. इसका मतलब है कि आपको वही गारंटी नहीं मिल रही हैं जो ES मॉड्यूल में मिलती हैं. इसके अलावा, यह आपके बिल्ड प्रोसेस के तहत, डिफ़ॉल्ट webpack व्यवहार के अलावा अतिरिक्त शुल्क भी जोड़ता है.

नतीजा

यह पक्का करने के लिए कि बंडलर आपके ऐप्लिकेशन को सही तरीके से ऑप्टिमाइज़ कर सके, CommonJS मॉड्यूल पर निर्भर होने से बचें और अपने पूरे ऐप्लिकेशन में ECMAScript मॉड्यूल सिंटैक्स का इस्तेमाल करें.

यह पुष्टि करने के लिए कि आप सबसे सही पाथ पर हैं, यहां कुछ कार्रवाई करने लायक सुझाव दिए गए हैं:

  • रोलअप.js के node-resolve प् लग इन का इस्तेमाल करें और modulesOnly फ़्लैग सेट करके बताएं कि आप सिर्फ़ ECMAScript मॉड्यूल पर निर्भर हैं.
  • पैकेज is-esm का इस्तेमाल करके पुष्टि करें कि एनपीएम पैकेज ECMAScript मॉड्यूल का इस्तेमाल करता है.
  • अगर आपने Angular का इस्तेमाल किया है, तो नॉन-ट्री-शेकेबल मॉड्यूल का इस्तेमाल करने पर, डिफ़ॉल्ट रूप से आपको एक चेतावनी मिलेगी.