ट्री शेकिंग की मदद से JavaScript पेलोड कम करें

आज के वेब ऐप्लिकेशन का साइज़ काफ़ी बड़ा हो सकता है. खास तौर पर, इनमें JavaScript का हिस्सा. साल 2018 के मध्य तक, एचटीटीपी आर्काइव के मुताबिक मोबाइल डिवाइसों पर JavaScript के ट्रांसफ़र का औसत साइज़ करीब 350 केबी था. यह सिर्फ़ ट्रांसफ़र का साइज़ है! नेटवर्क पर भेजे जाने पर, JavaScript को अक्सर कंप्रेस किया जाता है. इसका मतलब है कि ब्राउज़र के कंप्रेस किए जाने के बाद, JavaScript का असल साइज़ काफ़ी ज़्यादा हो जाता है. इस बात पर ध्यान देना ज़रूरी है, क्योंकि संसाधन प्रोसेसिंग के लिए, कॉम्प्रेस करने की ज़रूरत नहीं होती. डिकंप्रेस किए गए 900 केबी के JavaScript को पार्स करने और कंपाइल करने में, 900 केबी का ही इस्तेमाल होता है. भले ही, कंप्रेस करने पर यह 300 केबी हो जाए.

JavaScript को डाउनलोड करने, डिकंप्रेस करने, पार्स करने, कंपाइल करने, और एक्ज़ीक्यूट करने की प्रोसेस दिखाने वाला डायग्राम.
JavaScript डाउनलोड और चलाने की प्रोसेस. ध्यान दें कि भले ही स्क्रिप्ट का ट्रांसफ़र साइज़ 300 केबी का हो, लेकिन यह अब भी 900 केबी का JavaScript है. इसे पार्स, कंपाइल, और लागू करना होगा.

JavaScript को प्रोसेस करना महंगा होता है. इमेज को डाउनलोड करने के बाद, उसे डिकोड करने में ज़्यादा समय नहीं लगता. हालांकि, JavaScript को पार्स, कंपाइल, और फिर उसे एक्सीक्यूट करना पड़ता है. बाइट के हिसाब से, यह JavaScript को अन्य तरह के रिसॉर्स के मुकाबले ज़्यादा महंगा बनाता है.

इस डायग्राम में, 170 केबी के JavaScript और इसी साइज़ की JPEG इमेज को प्रोसेस करने में लगने वाले समय की तुलना की गई है. JPEG की तुलना में, JavaScript संसाधन ज़्यादा बाइट का होता है.
170 केबी के JavaScript को पार्स/कंपाइल करने की प्रोसेसिंग लागत बनाम उसी साइज़ के JPEG को डिकोड करने में लगने वाला समय. (source).

JavaScript इंजन की परफ़ॉर्मेंस को बेहतर बनाने के लिए, इसमें लगातार सुधार किए जा रहे हैं. हालांकि, JavaScript की परफ़ॉर्मेंस को बेहतर बनाना, हमेशा की तरह डेवलपर का काम है.

इसके लिए, JavaScript की परफ़ॉर्मेंस को बेहतर बनाने की तकनीकें उपलब्ध हैं. कोड को अलग-अलग करने की तकनीक से, ऐप्लिकेशन के JavaScript को अलग-अलग हिस्सों में बांटकर परफ़ॉर्मेंस को बेहतर बनाया जाता है. साथ ही, उन हिस्सों को सिर्फ़ उन ऐप्लिकेशन के रूट पर दिखाया जाता है जिनमें उनकी ज़रूरत होती है.

यह तकनीक काम करती है, लेकिन इससे JavaScript के ज़्यादा इस्तेमाल वाले ऐप्लिकेशन की एक आम समस्या हल नहीं होती. यह समस्या, ऐसे कोड को शामिल करने से जुड़ी है जिसका कभी इस्तेमाल नहीं किया जाता. ट्री शेकिंग की मदद से, इस समस्या को हल करने की कोशिश की जाती है.

ट्री शेकिंग क्या है?

ट्री शेकिंग, डेड कोड को हटाने का एक तरीका है. इस शब्द को Rollup ने लोकप्रिय बनाया, लेकिन डेड कोड को हटाने का कॉन्सेप्ट कुछ समय से मौजूद है. इस कॉन्सेप्ट को webpack में भी खरीदा गया है. इस बारे में इस लेख में, सैंपल ऐप्लिकेशन के ज़रिए बताया गया है.

"ट्री शेकिंग" शब्द, आपके ऐप्लिकेशन और उसकी डिपेंडेंसी को ट्री जैसे स्ट्रक्चर के तौर पर दिखाने वाले मॉडल से लिया गया है. ट्री में मौजूद हर नोड, एक ऐसी डिपेंडेंसी को दिखाता है जो आपके ऐप्लिकेशन के लिए अलग-अलग फ़ंक्शन उपलब्ध कराती है. आधुनिक ऐप्लिकेशन में, इन डिपेंडेंसी को स्टैटिक import स्टेटमेंट के ज़रिए शामिल किया जाता है. जैसे:

// Import all the array utilities!
import arrayUtils from "array-utils";

जब कोई ऐप्लिकेशन नया हो, तो हो सकता है कि उसमें कुछ ही डिपेंडेंसी हों. यह आपकी जोड़ी गई सभी डिपेंडेंसी का इस्तेमाल कर रहा है. अगर सभी डिपेंडेंसी का इस्तेमाल नहीं किया जा रहा है, तो ज़्यादातर डिपेंडेंसी का इस्तेमाल किया जा रहा है. हालांकि, जैसे-जैसे आपका ऐप्लिकेशन बेहतर होता जाता है, उसमें ज़्यादा डिपेंडेंसी जोड़ी जा सकती हैं. समस्या को और भी मुश्किल बनाते हुए, पुरानी डिपेंडेंसी का इस्तेमाल बंद हो जाता है. हालांकि, हो सकता है कि वे आपके कोडबेस से न हटें. इसका नतीजा यह होता है कि ऐप्लिकेशन में इस्तेमाल न होने वाली JavaScript की बहुत सारी फ़ाइलें शामिल हो जाती हैं. ट्री शेकिंग, इस समस्या को हल करती है. इसके लिए, यह इस बात का फ़ायदा लेती है कि स्टैटिक import स्टेटमेंट, ES6 मॉड्यूल के खास हिस्सों को कैसे खींचते हैं:

// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";

इस import उदाहरण और पिछले उदाहरण में अंतर यह है कि "array-utils" मॉड्यूल से पूरी चीज़ इंपोर्ट करने के बजाय, यह उदाहरण सिर्फ़ उसके कुछ हिस्सों को इंपोर्ट करता है. "array-utils" मॉड्यूल में बहुत ज़्यादा कोड हो सकता है. डेवलपर के लिए बनाए गए बिल्ड में, इससे कोई फ़र्क़ नहीं पड़ता, क्योंकि पूरा मॉड्यूल इंपोर्ट हो जाता है. प्रोडक्शन बिल्ड में, webpack को कॉन्फ़िगर किया जा सकता है, ताकि वह ES6 मॉड्यूल से एक्सपोर्ट को "हटाया" जा सके. ये ऐसे मॉड्यूल होते हैं जिन्हें साफ़ तौर पर इंपोर्ट नहीं किया गया था. इससे प्रोडक्शन बिल्ड छोटे हो जाते हैं. इस गाइड में, आपको ऐसा करने का तरीका पता चलेगा!

पेड़ को हिलाने के अवसर ढूंढना

उदाहरण के लिए, एक पेज वाले ऐप्लिकेशन का सैंपल उपलब्ध है. इससे यह पता चलता है कि ट्री शेकिंग कैसे काम करती है. अगर आप चाहें, तो इसे क्लोन करें और निर्देशों का पालन करें. हालांकि, हम इस गाइड में हर चरण को एक साथ कवर करेंगे. इसलिए, क्लोन करने की ज़रूरत नहीं है. ऐसा सिर्फ़ तब करें, जब आपको सीधे तौर पर सीखना हो.

सैंपल ऐप्लिकेशन, गिटार के इफ़ेक्ट वाले पेडल का ऐसा डेटाबेस है जिसमें खोज की सुविधा होती है. कोई क्वेरी डालने पर, आपको इफ़ेक्ट वाले पेडल की सूची दिखेगी.

गिटार इफ़ेक्ट पेडल के डेटाबेस को खोजने के लिए, एक पेज वाले सैंपल ऐप्लिकेशन का स्क्रीनशॉट.
सैंपल ऐप्लिकेशन का स्क्रीनशॉट.

इस ऐप्लिकेशन के काम करने के तरीके को वेंडर (यानी, Preact और Emotion) और ऐप्लिकेशन के हिसाब से कोड बंडल (या "चंक", जैसा कि वेबपैक उन्हें कहते हैं):

Chrome के DevTools के नेटवर्क पैनल में दिखाए गए दो ऐप्लिकेशन कोड बंडल (या चंक) का स्क्रीनशॉट.
ऐप्लिकेशन के दो JavaScript बंडल. ये साइज़, बिना कंप्रेस किए हुए हैं.

ऊपर दी गई इमेज में दिखाए गए JavaScript बंडल, प्रोडक्शन बिल्ड हैं. इसका मतलब है कि उन्हें uglification की मदद से ऑप्टिमाइज़ किया गया है. किसी ऐप्लिकेशन के लिए बने बंडल का साइज़ 21.1 केबी होना बुरा नहीं है. हालांकि, ध्यान रखें कि कोई ट्री शेकिंग नहीं हो रही है. चलिए, ऐप्लिकेशन कोड देखते हैं और देखते हैं कि इसे ठीक करने के लिए क्या किया जा सकता है.

किसी भी ऐप्लिकेशन में, ट्री शेकिंग के अवसरों को ढूंढने के लिए, स्टैटिक import स्टेटमेंट देखना होगा. मुख्य कॉम्पोनेंट फ़ाइल के सबसे ऊपर, आपको इस तरह की लाइन दिखेगी:

import * as utils from "../../utils/utils";

ES6 मॉड्यूल को कई तरीकों से इंपोर्ट किया जा सकता है. हालांकि, आपको इस तरह के मॉड्यूल पर ध्यान देना चाहिए. इस लाइन में कहा गया है कि "utils मॉड्यूल से import सब कुछ ले लो और उसे utils नाम के नेमस्पेस में डाल दो." यहां सबसे अहम सवाल यह है कि "उस मॉड्यूल में कितना सामान है?"

utils मॉड्यूल के सोर्स कोड को देखने पर, आपको पता चलेगा कि इसमें करीब 1,300 लाइनें हैं.

क्या आपको इन सभी चीज़ों की ज़रूरत है? आइए, utils मॉड्यूल को इंपोर्ट करने वाली मुख्य कॉम्पोनेंट फ़ाइल खोजकर दोबारा जांच करें. इससे यह पता चलेगा कि उस नेमस्पेस के कितने इंस्टेंस दिखते हैं.

टेक्स्ट एडिटर में 'utils' खोजने पर मिले नतीजों का स्क्रीनशॉट. इसमें सिर्फ़ तीन नतीजे दिख रहे हैं.
हमने utils नेमस्पेस से कई मॉड्यूल इंपोर्ट किए हैं. हालांकि, मुख्य कॉम्पोनेंट फ़ाइल में इसे सिर्फ़ तीन बार इंवोक किया गया है.

हमें पता चला है कि utils नेमस्पेस हमारे ऐप्लिकेशन में सिर्फ़ तीन जगहों पर दिखता है—लेकिन किन फ़ंक्शन के लिए? मुख्य कॉम्पोनेंट फ़ाइल को फिर से देखने पर, इसमें सिर्फ़ एक फ़ंक्शन दिखता है. यह utils.simpleSort फ़ंक्शन है. इसका इस्तेमाल, खोज के नतीजों की सूची को कई शर्तों के हिसाब से क्रम से लगाने के लिए किया जाता है. ऐसा तब किया जाता है, जब क्रम से लगाने के ड्रॉपडाउन में बदलाव किया जाता है:

if (this.state.sortBy === "model") {
  // `simpleSort` gets used here...
  json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  // ..and here...
  json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
  // ..and here.
  json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}

1,300 लाइनों वाली फ़ाइल में कई एक्सपोर्ट हैं, लेकिन उनमें से सिर्फ़ एक का इस्तेमाल किया गया है. इस वजह से, इस्तेमाल न किए गए बहुत सारे JavaScript को शिप किया जाता है.

यह उदाहरण थोड़ा बनावटी है, लेकिन इससे इस बात में कोई फ़र्क़ नहीं पड़ता कि इस तरह की स्थिति, किसी प्रोडक्शन वेब ऐप्लिकेशन में मिलने वाले ऑप्टिमाइज़ेशन के असली अवसरों से मिलती-जुलती है. अब जब आपको पता चल गया है कि ट्री शेकिंग का इस्तेमाल कैसे किया जा सकता है, तो इसे असल में कैसे किया जाता है?

Babel को ES6 मॉड्यूल को CommonJS मॉड्यूल में ट्रांसपाइल करने से रोकना

Babel एक ज़रूरी टूल है. हालांकि, इससे ट्री शेकिंग के असर को समझना थोड़ा मुश्किल हो सकता है. अगर @babel/preset-env का इस्तेमाल किया जा रहा है, तो Babel हो सकता है कि ES6 मॉड्यूल को ज़्यादा काम के CommonJS मॉड्यूल में बदल दे. ये वे मॉड्यूल होते हैं जिन्हें import के बजाय require किया जाता है.

CommonJS मॉड्यूल के लिए, ट्री शेकिंग करना ज़्यादा मुश्किल होता है. इसलिए, अगर आपने बंडल का इस्तेमाल करने का फ़ैसला किया, तो webpack को यह नहीं पता होगा कि बंडल से क्या हटाना है. इसका समाधान यह है कि @babel/preset-env को कॉन्फ़िगर करके, ES6 मॉड्यूल को छोड़ दिया जाए. Babel को babel.config.js या package.json में कहीं भी कॉन्फ़िगर करने पर, आपको कुछ और भी जोड़ना होगा:

// babel.config.js
export default {
  presets: [
    [
      "@babel/preset-env", {
        modules: false
      }
    ]
  ]
}

अपने @babel/preset-env कॉन्फ़िगरेशन में modules: false तय करने से, Babel अपनी ज़रूरत के मुताबिक काम करता है. इससे webpack को आपके डिपेंडेंसी ट्री का विश्लेषण करने और इस्तेमाल न किए गए डिपेंडेंसी को हटाने में मदद मिलती है.

साइड इफ़ेक्ट को ध्यान में रखना

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

let fruits = ["apple", "orange", "pear"];

console.log(fruits); // (3) ["apple", "orange", "pear"]

const addFruit = function(fruit) {
  fruits.push(fruit);
};

addFruit("kiwi");

console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]

इस उदाहरण में, addFruit अपने दायरे से बाहर के fruits कलेक्शन में बदलाव करने पर, साइड इफ़ेक्ट पैदा करता है.

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

webpack के मामले में, किसी प्रोजेक्ट की package.json फ़ाइल में "sideEffects": false की जानकारी देकर, यह बताया जा सकता है कि किसी पैकेज और उसकी डिपेंडेंसी पर कोई साइड इफ़ेक्ट नहीं पड़ता:

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": false
}

इसके अलावा, webpack को यह भी बताया जा सकता है कि किन फ़ाइलों पर साइड इफ़ेक्ट नहीं पड़ता:

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": [
    "./src/utils/utils.js"
  ]
}

दूसरे उदाहरण में, जिस फ़ाइल के बारे में नहीं बताया गया है उसे साइड इफ़ेक्ट से मुक्त माना जाएगा. अगर आपको इसे अपनी package.json फ़ाइल में नहीं जोड़ना है, तो module.rules की मदद से, अपने वेबपैक कॉन्फ़िगरेशन में भी इस फ़्लैग की जानकारी दी जा सकती है.

सिर्फ़ ज़रूरी डेटा इंपोर्ट करना

Babel को ES6 मॉड्यूल को छोड़ने का निर्देश देने के बाद, utils मॉड्यूल से सिर्फ़ ज़रूरी फ़ंक्शन लाने के लिए, हमारे import सिंटैक्स में थोड़ा बदलाव करना ज़रूरी है. इस गाइड के उदाहरण में, सिर्फ़ simpleSort फ़ंक्शन की ज़रूरत है:

import { simpleSort } from "../../utils/utils";

पूरे utils मॉड्यूल के बजाय, सिर्फ़ simpleSort को इंपोर्ट किया जा रहा है. इसलिए, utils.simpleSort के हर इंस्टेंस को simpleSort में बदलना होगा:

if (this.state.sortBy === "model") {
  json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  json = simpleSort(json, "type", this.state.sortOrder);
} else {
  json = simpleSort(json, "manufacturer", this.state.sortOrder);
}

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

                 Asset      Size  Chunks             Chunk Names
js/vendors.16262743.js  37.1 KiB       0  [emitted]  vendors
   js/main.797ebb8b.js  20.8 KiB       1  [emitted]  main

ट्री शेकिंग की प्रोसेस पूरी होने के बाद, यह आउटपुट दिखता है:

                 Asset      Size  Chunks             Chunk Names
js/vendors.45ce9b64.js  36.9 KiB       0  [emitted]  vendors
   js/main.559652be.js  8.46 KiB       1  [emitted]  main

दोनों बंडल में कमी आई, लेकिन main बंडल को सबसे ज़्यादा फ़ायदा हुआ. utils मॉड्यूल के इस्तेमाल न किए गए हिस्सों को हटाने पर, main बंडल का साइज़ करीब 60% तक कम हो जाता है. इससे स्क्रिप्ट को डाउनलोड होने में लगने वाला समय कम हो जाता है. साथ ही, प्रोसेसिंग में लगने वाला समय भी कम हो जाता है.

कुछ पेड़ों को हिलाएं!

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

हो सकता है कि ट्री शेकिंग की वजह से, आपको परफ़ॉर्मेंस में काफ़ी बढ़ोतरी दिखे या बिलकुल न दिखे. हालांकि, प्रोडक्शन बिल्ड में इस ऑप्टिमाइज़ेशन का फ़ायदा पाने के लिए, अपने बिल्ड सिस्टम को कॉन्फ़िगर करें. साथ ही, सिर्फ़ वही कॉम्पोनेंट इंपोर्ट करें जो आपके ऐप्लिकेशन के लिए ज़रूरी हैं. इससे, आपके ऐप्लिकेशन बंडल का साइज़ कम से कम रहेगा.

इस लेख को बेहतर बनाने के लिए, Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone, और Philip Walton का बहुत-बहुत धन्यवाद.