आपसे कहा गया है कि "मुख्य थ्रेड को ब्लॉक न करें" और "अपने लंबे टास्क को अलग-अलग करें". हालाँकि, कहा गया है कि ऐसे काम करने का क्या मतलब है?
अगर आपने वेब परफ़ॉर्मेंस के बारे में बहुत कुछ पढ़ा है, तो अपने JavaScript ऐप्लिकेशन को तेज़ बनाए रखने की सलाह में, नीचे दी गई कुछ बातें शामिल होती हैं:
- "मुख्य थ्रेड को ब्लॉक न करें."
- "अपने लंबे टास्क को हिस्सों में बांटें."
इसका क्या मतलब है? शिपिंग कम JavaScript अच्छा होता है, लेकिन क्या यह पेज की लाइफ़साइकल के दौरान अपने-आप ही स्नैपर यूज़र इंटरफ़ेस के बराबर हो जाता है? हो सकती है, लेकिन नहीं हो सकती.
JavaScript में टास्क को ऑप्टिमाइज़ करना क्यों ज़रूरी है, यह जानने के लिए आपको टास्क की भूमिका और ब्राउज़र उन्हें कैसे हैंडल करता है, यह समझना होगा. इसके बाद सबसे पहले यह समझना ज़रूरी है कि टास्क क्या है.
टास्क क्या होता है?
टास्क एक अलग तरह का काम है, जिसे ब्राउज़र करता है. टास्क में कई तरह के काम शामिल हैं. जैसे, रेंडरिंग, एचटीएमएल और सीएसएस को पार्स करना, आपके लिखे हुए JavaScript कोड को चलाना, और ऐसे दूसरे काम जिन पर आपका सीधा कंट्रोल न हो. इन सभी में से, जो JavaScript लिखा जाता है और वेब पर डिप्लॉय किया जाता है, वह टास्क का एक बड़ा सोर्स है.
टास्क, परफ़ॉर्मेंस पर कई तरह से असर डालते हैं. उदाहरण के लिए, जब कोई ब्राउज़र, ब्राउज़र खोलने के दौरान JavaScript फ़ाइल डाउनलोड करता है, तो वह JavaScript को पार्स और कंपाइल करने के लिए टास्क को सूची में जोड़ देता है, ताकि उसे चलाया जा सके. बाद में, पेज के लाइफ़साइकल में, जब आपका JavaScript काम करता है, तब टास्क शुरू हो जाते हैं. जैसे, इवेंट हैंडलर, JavaScript से चलने वाले ऐनिमेशन, और Analytics कलेक्शन जैसी बैकग्राउंड गतिविधि. यह सभी चीज़ें—वेब वर्कर और मिलते-जुलते एपीआई को छोड़कर—मुख्य थ्रेड पर होती हैं.
मुख्य थ्रेड क्या है?
मुख्य थ्रेड वह जगह है जहां ब्राउज़र में ज़्यादातर टास्क चलते हैं. इसे मुख्य थ्रेड कहा जाता है. यह एक ऐसा थ्रेड है जहां आपके लिखे गए करीब-करीब सभी JavaScript काम करते हैं.
मुख्य थ्रेड एक बार में सिर्फ़ एक ही टास्क प्रोसेस कर सकती है. जब टास्क एक तय समय से ज़्यादा बड़े हो जाते हैं, यानी कि 50 मिलीसेकंड ही, तो उन्हें लंबे टास्क की कैटगरी में रखा जाता है. अगर उपयोगकर्ता, टास्क के लंबे समय तक चलने के दौरान पेज से इंटरैक्ट करने की कोशिश कर रहा है—या रेंडरिंग से जुड़ा कोई अहम अपडेट ज़रूरी है, तो ब्राउज़र को देर से वह काम पूरा करना होगा. इसकी वजह से, इंटरैक्शन या रेंडरिंग होने में लगने वाला समय बढ़ जाता है.
आपको टास्क को अलग-अलग हिस्सों में बांटना होगा. इसका मतलब है कि एक लंबे टास्क को छोटे-छोटे टास्क में बांटना, जिन्हें अलग-अलग पूरा करने में कम समय लगता हो.
यह इसलिए अहम है, क्योंकि टास्क का बंटवारा करने पर, ब्राउज़र के पास ज़्यादा प्राथमिकता वाले कामों का जवाब देने के ज़्यादा अवसर होते हैं. इसमें उपयोगकर्ता के इंटरैक्शन भी शामिल होते हैं.
पिछले आंकड़े में सबसे ऊपर, यूज़र इंटरैक्शन के दौरान लाइन में लगाए गए इवेंट हैंडलर को एक टास्क पूरा करने के लिए इंतज़ार करना पड़ा. इस वजह से, इंटरैक्शन होने में देरी हो गई. सबसे नीचे, इवेंट हैंडलर के जल्दी चलने की संभावना है. इवेंट हैंडलर छोटे-छोटे टास्क के बीच में चल सकता था. इसलिए, लंबा टास्क पूरा होने का इंतज़ार करने पर, इवेंट जल्दी चलता है. सबसे ऊपर दिए गए उदाहरण में, हो सकता है कि उपयोगकर्ता को अंतर दिख रहा हो. सबसे नीचे, ऐसा हो सकता है कि इंटरैक्शन तुरंत महसूस हुआ हो.
हालांकि, समस्या यह है कि "अपने लंबे टास्क को अलग-अलग हिस्सों में बांटें" और "मुख्य थ्रेड को ब्लॉक न करें" की सलाह तब तक सटीक नहीं होती, जब तक आप पहले से यह न जानते हों कि इन कामों को कैसे किया जाता है. इस गाइड में बताया गया है कि आपको क्या करना है.
टास्क मैनेजमेंट से जुड़ी रणनीतियां
सॉफ़्टवेयर आर्किटेक्चर में आम तौर पर दी जाने वाली सलाह यह है कि आप अपने काम को छोटे-छोटे हिस्सों में बांट लें. इससे आपको कोड को आसानी से पढ़ने और प्रोजेक्ट के रखरखाव के फ़ायदे मिलते हैं. इससे टेस्ट को लिखना भी आसान हो जाता है.
function saveSettings () {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
इस उदाहरण में, saveSettings()
नाम का एक फ़ंक्शन है, जो इसके अंदर पांच फ़ंक्शन को कॉल करता है. उदाहरण के लिए, किसी फ़ॉर्म की पुष्टि करना, स्पिनर दिखाना, डेटा भेजना वगैरह. सैद्धान्तिक तौर पर, इसे अच्छी तरह से डिज़ाइन किया गया है. अगर आपको इनमें से किसी एक फ़ंक्शन को डीबग करना है, तो प्रोजेक्ट ट्री को पार करके यह पता लगाया जा सकता है कि हर फ़ंक्शन क्या करता है.
हालांकि, समस्या यह है कि JavaScript इनमें से हर फ़ंक्शन को अलग टास्क के रूप में नहीं चलाता, क्योंकि वे saveSettings()
फ़ंक्शन के अंदर ही एक्ज़ीक्यूट किए जाते हैं. इसका मतलब है कि सभी पांच फ़ंक्शन एक ही टास्क के तौर पर काम करते हैं.
सबसे अच्छी स्थिति में, इनमें से किसी एक फ़ंक्शन से भी टास्क को पूरा होने में 50 मिलीसेकंड या उससे ज़्यादा समय लग सकता है. सबसे खराब स्थिति में, इनमें से ज़्यादातर टास्क, खास तौर पर सीमित संसाधनों वाले डिवाइसों पर, ज़्यादा समय तक चल सकते हैं. इसके बाद, कुछ रणनीतियों का इस्तेमाल किया जा सकता है. इनका इस्तेमाल, टास्क को अलग-अलग हिस्सों में बांटने और प्राथमिकता तय करने के लिए किया जा सकता है.
मैन्युअल तरीके से कोड एक्ज़ीक्यूट होने से रोकें
टास्क को छोटे-छोटे हिस्सों में बांटने के लिए, डेवलपर setTimeout()
का एक तरीका इस्तेमाल करते हैं. इस तकनीक का इस्तेमाल करके, आप फ़ंक्शन को setTimeout()
को पास करते हैं. इससे, कॉलबैक के लागू होने को एक अलग टास्क में आगे बढ़ा दिया जाता है, भले ही आपने टाइम आउट 0
तय कर दिया हो.
function saveSettings () {
// Do critical work that is user-visible:
validateForm();
showSpinner();
updateUI();
// Defer work that isn't user-visible to a separate task:
setTimeout(() => {
saveToDatabase();
sendAnalytics();
}, 0);
}
अगर आपके पास कई ऐसे फ़ंक्शन हैं जिन्हें क्रम से चलाने की ज़रूरत है, तो यह तब बढ़िया काम करता है, लेकिन हो सकता है कि आपका कोड हमेशा इस तरह से व्यवस्थित न हो. उदाहरण के लिए, हो सकता है कि आपके पास काफ़ी ज़्यादा डेटा हो, जिसे लूप में प्रोसेस किया जा सके. साथ ही, अगर आपके पास लाखों आइटम हैं, तो इस टास्क को प्रोसेस होने में बहुत समय लग सकता है.
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
यहां setTimeout()
का इस्तेमाल करने में मुश्किल होती है, क्योंकि इसके एर्गोनॉमिक्स की वजह से इसे लागू करना मुश्किल होता है. साथ ही, पूरे डेटा कलेक्शन को प्रोसेस होने में बहुत ज़्यादा समय लग सकता है. भले ही, हर आइटम को बहुत तेज़ी से प्रोसेस किया जा सकता हो. इस तरीके से यह रोज़मर्रा की ज़रूरतों को पूरा कर सकता है और setTimeout()
इस काम के लिए सही टूल नहीं है. कम से कम ऐसा तब नहीं किया जा सकता, जब इसे इस तरह इस्तेमाल किया गया हो.
setTimeout()
के अलावा, कुछ और एपीआई भी हैं जिनकी मदद से, कोड को आगे के किसी टास्क के लिए लागू होने से रोका जा सकता है. इनमें से एक में, तेज़ी से टाइम आउट पाने के लिए postMessage()
का इस्तेमाल किया जाता है. requestIdleCallback()
का इस्तेमाल करके, काम को अलग-अलग हिस्सों में बांटा जा सकता है, लेकिन ध्यान रखें!—requestIdleCallback()
, टास्क को सबसे कम प्राथमिकता पर शेड्यूल करता है. साथ ही, ऐसा सिर्फ़ तब किया जाता है, जब ब्राउज़र कुछ समय इस्तेमाल न कर रहा हो. मुख्य थ्रेड में भीड़-भाड़ होने पर, हो सकता है कि requestIdleCallback()
के साथ शेड्यूल किए गए टास्क कभी न चल पाएं.
ईल्ड पॉइंट बनाने के लिए, async
/await
का इस्तेमाल करें
इस गाइड के बाकी हिस्से में आपको एक ऐसा वाक्यांश दिखेगा जो "मुख्य थ्रेड में शामिल होने वाला है"—लेकिन इसका क्या मतलब है? आपको ऐसा क्यों करना चाहिए? आपको इसे कब करना चाहिए?
जब टास्क अलग-अलग किए जाते हैं, तो ब्राउज़र की इंटरनल प्राथमिकता स्कीम के ज़रिए, दूसरे टास्क को बेहतर तरीके से प्राथमिकता दी जा सकती है. मुख्य थ्रेड में शामिल होने का एक तरीका है. इसके लिए, Promise
को एक साथ इस्तेमाल करके setTimeout()
को कॉल किया जाना चाहिए:
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
saveSettings()
फ़ंक्शन में, हर फ़ंक्शन के बाद yieldToMain()
फ़ंक्शन को await
करने पर, हर काम के बाद मुख्य थ्रेड में नतीजे पाए जा सकते हैं:
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread:
await yieldToMain();
}
}
इससे, एक बार मोनोलिथिक टास्क को अलग-अलग टास्क में बांट दिया जाता है.
setTimeout()
को मैन्युअल तौर पर इस्तेमाल करने के बजाय, वादे पर आधारित तरीके का इस्तेमाल करने से बेहतर एर्गोनॉमिक्स मिलता है. यील्ड पॉइंट डिक्लेरेटिव होते हैं. इस वजह से उन्हें लिखना, पढ़ना, और समझना आसान हो जाता है.
ज़रूरत होने पर ही पाएं
अगर आपके पास कई टास्क हैं, लेकिन आपको लगता है कि कोई उपयोगकर्ता उस पेज से इंटरैक्ट करे, तो क्या करना चाहिए? isInputPending()
इसी तरह के लोगों के लिए बना है.
isInputPending()
एक ऐसा फ़ंक्शन है जिसे किसी भी समय चलाकर यह पता लगाया जा सकता है कि उपयोगकर्ता, पेज एलिमेंट से इंटरैक्ट करने की कोशिश कर रहा है या नहीं. isInputPending()
को कॉल करने पर true
दिखेगा. अगर ऐसा नहीं है, तो यह false
दिखाता है.
मान लें कि आपके पास उन टास्क की सूची है जिन्हें आपको चलाना है, लेकिन आपको बार-बार इनपुट में रुकावट नहीं डालनी पड़ती. यह कोड—जो isInputPending()
और हमारे कस्टम yieldToMain()
फ़ंक्शन, दोनों का इस्तेमाल करता है—इससे यह पक्का किया जाता है कि उपयोगकर्ता के पेज से इंटरैक्ट करने की कोशिश के दौरान, इनपुट में देरी नहीं होगी:
async function saveSettings () {
// A task queue of functions
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
];
while (tasks.length > 0) {
// Yield to a pending user input:
if (navigator.scheduling.isInputPending()) {
// There's a pending user input. Yield here:
await yieldToMain();
} else {
// Shift the task out of the queue:
const task = tasks.shift();
// Run the task:
task();
}
}
}
saveSettings()
चलने पर, यह सूची में मौजूद टास्क पर लूप करेगा. अगर isInputPending()
, लूप के दौरान true
दिखाता है, तो saveSettings()
yieldToMain()
को कॉल करेगा, ताकि उपयोगकर्ता का इनपुट मैनेज किया जा सके. ऐसा न करने पर, यह अगले टास्क को सूची में मौजूद नहीं दिखाएगा और लगातार चलाता रहेगा. ऐसा तब तक किया जाएगा, जब तक कोई टास्क पूरा नहीं हो जाता.
ईल्डिंग मैकेनिज़्म के साथ isInputPending()
का इस्तेमाल करना, ब्राउज़र से प्रोसेस किए जा रहे किसी भी टास्क को रोकने का बढ़िया तरीका है. इससे ब्राउज़र को उपयोगकर्ता के अहम इंटरैक्शन का जवाब देने में मदद मिलती है. इससे कई स्थितियों में उपयोगकर्ता को जवाब देने में आपके पेज की क्षमता बेहतर हो सकती है. ऐसा तब होगा, जब बहुत से टास्क चल रहे हों.
isInputPending()
इस्तेमाल करने का एक और तरीका—अगर आपको इसके साथ काम न करने वाले ब्राउज़र के लिए फ़ॉलबैक देने को लेकर परेशान हैं, तो आप चेन ऑपरेटर के वैकल्पिक ऑपरेटर के साथ समय-आधारित तरीके का इस्तेमाल करें:
async function saveSettings () {
// A task queue of functions
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
];
let deadline = performance.now() + 50;
while (tasks.length > 0) {
// Optional chaining operator used here helps to avoid
// errors in browsers that don't support `isInputPending`:
if (navigator.scheduling?.isInputPending() || performance.now() >= deadline) {
// There's a pending user input, or the
// deadline has been reached. Yield here:
await yieldToMain();
// Extend the deadline:
deadline = performance.now() + 50;
// Stop the execution of the current loop and
// move onto the next iteration:
continue;
}
// Shift the task out of the queue:
const task = tasks.shift();
// Run the task:
task();
}
}
यह तरीका अपनाकर, आपको ऐसे ब्राउज़र के लिए फ़ॉलबैक मिलता है जो isInputPending()
के साथ काम नहीं करते. इसके लिए, समय पर आधारित तरीके का इस्तेमाल किया जाता है. इसमें, समय पर आधारित तरीका इस्तेमाल किया जाता है, ताकि ज़रूरत पड़ने पर काम को अलग-अलग किया जा सके. भले ही, उपयोगकर्ता के इनपुट के आधार पर या किसी तय समय में वह काम कर रहा हो.
मौजूदा एपीआई में गैप
अब तक बताए गए एपीआई, टास्क को छोटे-छोटे टास्क में बांटने में आपकी मदद कर सकते हैं, लेकिन इसका एक बड़ा नुकसान है: जब किसी टास्क को आगे के टास्क में चलाने के लिए कोड को रोककर मुख्य थ्रेड में भेजा जाता है, तो वह कोड टास्क की सूची में सबसे आखिर में जुड़ जाता है.
अगर आपके पेज पर मौजूद सभी कोड कंट्रोल किए जाते हैं, तो अपना खुद का शेड्यूलर बनाया जा सकता है. साथ ही, टास्क को प्राथमिकता दी जा सकती है. हालांकि, तीसरे पक्ष की स्क्रिप्ट आपके शेड्यूलर का इस्तेमाल नहीं करेंगी. इसलिए, ऐसे माहौल में काम को प्राथमिकता नहीं दी जा सकती. इसे सिर्फ़ अलग-अलग ग्रुप में बांटा जा सकता है या साफ़ तौर पर उपयोगकर्ता के इंटरैक्शन के तौर पर दिखाया जा सकता है.
अच्छी बात यह है कि एक खास शेड्यूलर एपीआई मौजूद है जो फ़िलहाल इन समस्याओं को हल करने के लिए काम कर रहा है.
एक खास शेड्यूलर एपीआई
फ़िलहाल, शेड्यूलर एपीआई postTask()
फ़ंक्शन ऑफ़र करता है. यह फ़ंक्शन लिखते समय, यह Chromium ब्राउज़र में और Firefox में फ़्लैग के पीछे उपलब्ध होता है. postTask()
, टास्क को सटीक तरीके से शेड्यूल करने की सुविधा देता है. साथ ही, यह ब्राउज़र को प्राथमिकता से काम करने में मदद करता है, ताकि कम प्राथमिकता वाले टास्क मुख्य थ्रेड में आएं. postTask()
, प्रॉमिस का इस्तेमाल करता है और priority
सेटिंग को स्वीकार करता है.
postTask()
API में तीन प्राथमिकताएं हैं जिनका इस्तेमाल किया जा सकता है:
- सबसे कम प्राथमिकता वाले टास्क के लिए
'background'
. - सामान्य प्राथमिकता वाले टास्क के लिए,
'user-visible'
. अगर कोईpriority
सेट नहीं है, तो यह डिफ़ॉल्ट तौर पर लागू होता है. - ज़्यादा प्राथमिकता पर चलाए जाने वाले ज़रूरी टास्क के लिए,
'user-blocking'
.
नीचे दिए गए कोड को उदाहरण के तौर पर लें, जहां postTask()
एपीआई का इस्तेमाल सबसे ज़्यादा प्राथमिकता पर तीन टास्क चलाने के लिए किया जाता है और बाकी दो टास्क को सबसे कम प्राथमिकता पर चलाने के लिए.
function saveSettings () {
// Validate the form at high priority
scheduler.postTask(validateForm, {priority: 'user-blocking'});
// Show the spinner at high priority:
scheduler.postTask(showSpinner, {priority: 'user-blocking'});
// Update the database in the background:
scheduler.postTask(saveToDatabase, {priority: 'background'});
// Update the user interface at high priority:
scheduler.postTask(updateUI, {priority: 'user-blocking'});
// Send analytics data in the background:
scheduler.postTask(sendAnalytics, {priority: 'background'});
};
यहां, टास्क की प्राथमिकता इस तरह से शेड्यूल की जाती है कि ब्राउज़र की प्राथमिकता वाले टास्क—जैसे कि उपयोगकर्ता के इंटरैक्शन—अपने हिसाब से काम कर सकें.
यह, postTask()
को इस्तेमाल करने के तरीके का आसान उदाहरण है. अलग-अलग TaskController
ऑब्जेक्ट को इंस्टैंशिएट किया जा सकता है, जो टास्क के बीच प्राथमिकताएं शेयर कर सकता है. इसमें, ज़रूरत के हिसाब से अलग-अलग TaskController
इंस्टेंस के लिए प्राथमिकताएं बदलने की सुविधा भी शामिल है.
scheduler.yield
से, बिना रुकावट के मिलने वाला फ़ायदा
शेड्यूलर API का एक प्रस्तावित हिस्सा scheduler.yield
है. यह एक एपीआई है. इसे खास तौर पर, ब्राउज़र के मुख्य थ्रेड में फ़ेच करने के लिए डिज़ाइन किया गया है. फ़िलहाल, यह ऑरिजिन ट्रायल के तौर पर उपलब्ध है. इसका इस्तेमाल, इस लेख में पहले दिखाए गए yieldToMain()
फ़ंक्शन से मिलता-जुलता है:
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread with the scheduler
// API's own yielding mechanism:
await scheduler.yield();
}
}
यह ध्यान रखा जाएगा कि ऊपर दिया गया कोड काफ़ी हद तक जाना-पहचाना है, लेकिन yieldToMain()
का इस्तेमाल करने के बजाय, आप कॉल और await scheduler.yield()
का इस्तेमाल कर सकते हैं.
scheduler.yield()
का फ़ायदा लगातार हासिल करने पर होता है. इसका मतलब है कि अगर टास्क के सेट के बीच में ही कोई टास्क हासिल किया जाता है, तो शेड्यूल किए गए अन्य टास्क भी उसी क्रम में अपने-आप चलते रहेंगे. इससे तीसरे पक्ष की स्क्रिप्ट के कोड को आपके कोड के काम करने के क्रम को कम नहीं किया जा सकता.
नतीजा
टास्क को मैनेज करना चुनौती भरा काम है. हालांकि, ऐसा करने से आपके पेज को उपयोगकर्ता के इंटरैक्शन के लिए, तेज़ी से जवाब देने में मदद मिलती है. किसी टास्क को मैनेज करने और उसकी प्राथमिकता तय करने के लिए कोई एक सलाह नहीं है. दरअसल, इसमें कई अलग-अलग तकनीकें होती हैं. एक बार फिर से बताने के लिए, टास्क मैनेज करते समय आपको इन मुख्य बातों पर ध्यान देना चाहिए:
- लोगों को मिलने वाले ज़रूरी टास्क के लिए, मुख्य थ्रेड पर वापस आएं.
- जब लोग पेज से इंटरैक्ट करने की कोशिश कर रहे हों, तब मुख्य थ्रेड में कॉल पाने के लिए
isInputPending()
का इस्तेमाल करें. postTask()
की मदद से टास्क को प्राथमिकता दें.- आखिर में, अपने फ़ंक्शन में जितना हो सके उतना कम काम करें.
इनमें से एक या उससे ज़्यादा टूल का इस्तेमाल करके, किसी काम को ऐप्लिकेशन में व्यवस्थित किया जा सकता है. इससे उपयोगकर्ता की ज़रूरतों को प्राथमिकता दी जा सकेगी. साथ ही, यह भी पक्का किया जा सकेगा कि कम मुश्किल काम अब भी पूरा हो सके. इससे उपयोगकर्ताओं को बेहतर अनुभव मिलेगा, जो ज़्यादा रिस्पॉन्सिव और इस्तेमाल में ज़्यादा मज़ेदार होगा.
इस लेख की तकनीकी जांच के लिए, फ़िलिप वॉल्टन का विशेष आभार.
हीरो इमेज को अमीराली मिराहाश्मिअन के सौजन्य से Unस्प्लैश से लिया गया है.