ट्री शेकिंग की मदद से 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 फ़ॉर्मैट के डिकोड करने में लगने वाला समय. (सोर्स).

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

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

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

पेड़ के हिलने का क्या मतलब होता है?

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

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

पेड़ हिलाने के मौके तलाशना

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

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

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

इस ऐप्लिकेशन को चलाने वाली हर कार्रवाई को वेंडर के हिसाब से अलग-अलग किया जाता है (यानी, प्रीऐक्ट और इमोशन) और ऐप्लिकेशन के लिए खास कोड बंडल (या "हिंक", क्योंकि Webpack इन्हें कहते हैं):

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

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

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

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

ES6 मॉड्यूल कई तरीकों से इंपोर्ट किए जा सकते हैं. हालांकि, इस तरह के मॉड्यूल पर आपको ध्यान मिलना चाहिए. इस लाइन में लिखा है "import utils मॉड्यूल से सब कुछ और उसे 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 को काफ़ी ज़्यादा शिप किया जाता है.

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

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

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

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

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

आपके @babel/preset-env कॉन्फ़िगरेशन में modules: false तय करने पर, बेबेल उम्मीद के मुताबिक काम कर पाता है. इससे 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 कॉन्फ़िगरेशन में इस फ़्लैग के बारे में भी बताया जा सकता है.

सिर्फ़ वही इंपोर्ट करें जो आपके लिए ज़रूरी है

बेबल को 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% तक छोटा हो जाता है. इससे न सिर्फ़ स्क्रिप्ट को डाउनलोड होने में लगने वाला समय कम होता है, बल्कि उसकी प्रोसेसिंग में भी कम समय लगता है.

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

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

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

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