आजकल के वेब ऐप्लिकेशन काफ़ी बड़े हो सकते हैं. खास तौर पर, उनमें मौजूद JavaScript का हिस्सा. HTTP Archive के मुताबिक, 2018 के मध्य तक, मोबाइल डिवाइसों पर JavaScript का औसत ट्रांसफ़र साइज़ करीब 350 केबी था. यह सिर्फ़ ट्रांसफ़र साइज़ है! नेटवर्क पर JavaScript भेजते समय, अक्सर इसे कंप्रेस किया जाता है. इसका मतलब है कि ब्राउज़र से डीकंप्रेस करने के बाद, JavaScript की असल मात्रा काफ़ी ज़्यादा होती है. यह बात बताना ज़रूरी है, क्योंकि संसाधन प्रोसेस करने के मामले में, कंप्रेस करने से कोई फ़र्क़ नहीं पड़ता. डीकंप्रेस की गई 900 केबी की JavaScript, पार्सर और कंपाइलर के लिए अब भी 900 केबी की ही रहेगी. भले ही, कंप्रेस करने पर यह करीब 300 केबी की हो.
JavaScript को प्रोसेस करने में ज़्यादा समय लगता है. इमेज को डाउनलोड करने के बाद, उन्हें डीकोड करने में बहुत कम समय लगता है. वहीं, JavaScript को पहले पार्स, फिर कंपाइल, और आखिर में एक्ज़ीक्यूट करना पड़ता है. बाइट के हिसाब से, JavaScript को प्रोसेस करने में अन्य तरह के संसाधनों की तुलना में ज़्यादा समय लगता है.
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) और ऐप्लिकेशन के हिसाब से कोड बंडल (या "चंक", जैसा कि webpack इन्हें कहता है) में बांटा गया है:
ऊपर दी गई इमेज में दिखाए गए JavaScript बंडल, प्रोडक्शन बिल्ड हैं. इसका मतलब है कि इन्हें ऑप्टिमाइज़ करने के लिए, यूग्लिफ़िकेशन का इस्तेमाल किया गया है. ऐप्लिकेशन के हिसाब से बंडल के लिए 21.1 केबी साइज़ ठीक है. हालांकि, यह ध्यान रखना ज़रूरी है कि इसमें ट्री शेकिंग नहीं की गई है. आइए, ऐप्लिकेशन का कोड देखें और जानें कि इसे ठीक करने के लिए क्या किया जा सकता है.
किसी भी ऐप्लिकेशन में, ट्री शेकिंग के अवसर ढूंढने के लिए, स्टैटिक import स्टेटमेंट देखने होंगे. मुख्य कॉम्पोनेंट फ़ाइल में सबसे ऊपर, आपको इस तरह की लाइन दिखेगी:
import * as utils from "../../utils/utils";
ES6 मॉड्यूल को कई तरीकों से इंपोर्ट किया जा सकता है. हालांकि, इस तरह के मॉड्यूल पर ध्यान देना चाहिए. इस लाइन का मतलब है कि utils मॉड्यूल से सब कुछ import करें और इसे utils नाम के नेमस्पेस में रखें. यहां यह सवाल पूछना ज़रूरी है कि उस मॉड्यूल में कितना कॉन्टेंट है?
the utils मॉड्यूल सोर्स कोड को देखने पर, आपको करीब 1,300 लाइन का कोड दिखेगा.
क्या आपको उस पूरे कॉन्टेंट की ज़रूरत है? आइए, इसकी पुष्टि करने के लिए, मुख्य कॉम्पोनेंट फ़ाइल में 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 के ज़रिए, इसे अपने webpack कॉन्फ़िगरेशन में भी तय किया जा सकता है.
सिर्फ़ ज़रूरी चीज़ें इंपोर्ट करना
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);
}
इस उदाहरण में, ट्री शेकिंग को काम करने के लिए, इतना ही करना ज़रूरी है. यह डिपेंडेंसी ट्री को शेक करने से पहले, webpack का आउटपुट है:
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 का खास तौर पर धन्यवाद.