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

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

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

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

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

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

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

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

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

ट्री शेकिंग, डेड कोड को हटाने का एक तरीका है. इस शब्द को रोलअप टूल से लोकप्रिय बनाया गया था. हालांकि, इस शब्द को बंद करने का सिद्धांत कुछ समय से मौजूद है. इस कॉन्सेप्ट को 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" मॉड्यूल से सब कुछ इंपोर्ट करने के बजाय, इस उदाहरण में इसके सिर्फ़ खास हिस्सों को इंपोर्ट किया जाता है. इसमें बहुत ज़्यादा कोड हो सकते हैं. डेवलपर के लिए बने बिल्ड में, इससे कोई फ़र्क़ नहीं पड़ता, क्योंकि पूरा मॉड्यूल इंपोर्ट हो जाता है. प्रोडक्शन बिल्ड में, वेबपैक को कॉन्फ़िगर किया जा सकता है, ताकि वह 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 फ़ाइल में नहीं जोड़ना है, तो अपने webpack कॉन्फ़िगरेशन में 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% तक कम हो जाता है. इससे स्क्रिप्ट को डाउनलोड होने में लगने वाला समय कम हो जाता है. साथ ही, प्रोसेसिंग में लगने वाला समय भी कम हो जाता है.

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

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

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

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