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 फ़ंक्शन के मुख्य भाग को इनलाइन करें

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

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

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

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

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

नतीजा

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

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

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