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

नतीजा

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

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

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