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));

node के साथ index.js को लागू करने पर, कंसोल में 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 की मदद से ट्री-शैकिंग

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

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

नतीजा

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

यहां कुछ ऐसी सलाह दी गई हैं जिन पर अमल करके, यह पक्का किया जा सकता है कि आप सही रास्ते पर हैं:

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