आपको "मुख्य थ्रेड को ब्लॉक न करें" और "लंबे टास्क को छोटे-छोटे टास्क में बांटें" के बारे में बताया गया है. हालांकि, इन बातों का क्या मतलब है?
पब्लिश किया गया: 30 सितंबर, 2022, पिछली बार अपडेट किया गया: 19 दिसंबर, 2024
JavaScript ऐप्लिकेशन को तेज़ बनाए रखने के लिए, आम तौर पर ये सलाह दी जाती है:
- "मुख्य थ्रेड को ब्लॉक न करें."
- "बड़े टास्क को छोटे-छोटे टास्क में बांटें."
यह एक बेहतरीन सलाह है, लेकिन इसमें क्या काम करना होगा? कम JavaScript का इस्तेमाल करना अच्छा है, लेकिन क्या इससे उपयोगकर्ता इंटरफ़ेस अपने-आप ज़्यादा तेज़ी से काम करने लगते हैं? हो सकता है, नहीं भी हो सकता है.
JavaScript में टास्क को ऑप्टिमाइज़ करने का तरीका समझने के लिए, आपको सबसे पहले यह जानने की ज़रूरत है कि टास्क क्या होते हैं और ब्राउज़र उन्हें कैसे हैंडल करता है.
टास्क क्या होता है?
टास्क, ब्राउज़र के ज़रिए किया जाने वाला कोई भी काम होता है. इसमें रेंडरिंग, एचटीएमएल और सीएसएस को पार्स करना, JavaScript चलाना, और अन्य तरह के काम शामिल हैं. इन पर आपका सीधा कंट्रोल नहीं होता. इन सभी में, लिखी गई JavaScript शायद टास्क का सबसे बड़ा सोर्स है.
click इवेंट हैंडलर से शुरू किया गया टास्क.
JavaScript से जुड़े टास्क, परफ़ॉर्मेंस पर कई तरह से असर डालते हैं:
- जब कोई ब्राउज़र स्टार्टअप के दौरान JavaScript फ़ाइल डाउनलोड करता है, तो वह उस JavaScript को पार्स और कंपाइल करने के लिए टास्क को क्रम में लगाता है, ताकि उसे बाद में लागू किया जा सके.
- पेज के चालू रहने के दौरान, अन्य समय में टास्क तब लाइन में लग जाते हैं, जब JavaScript कोई काम करती है. जैसे, इवेंट हैंडलर के ज़रिए इंटरैक्शन का जवाब देना, JavaScript की मदद से ऐनिमेशन बनाना, और बैकग्राउंड में होने वाली गतिविधि, जैसे कि आंकड़ों को इकट्ठा करना.
वेब वर्कर और मिलते-जुलते एपीआई को छोड़कर, यह सब मुख्य थ्रेड पर होता है.
मुख्य थ्रेड क्या है?
मुख्य थ्रेड में ब्राउज़र के ज़्यादातर टास्क चलते हैं. साथ ही, इसमें आपके लिखे गए लगभग सभी JavaScript कोड को एक्ज़ीक्यूट किया जाता है.
मुख्य थ्रेड, एक बार में सिर्फ़ एक टास्क प्रोसेस कर सकता है. अगर किसी टास्क को पूरा होने में 50 मि॰से॰ से ज़्यादा समय लगता है, तो उसे ज़्यादा समय लेने वाला काम कहा जाता है. अगर किसी टास्क को पूरा होने में 50 मिलीसेकंड से ज़्यादा समय लगता है, तो टास्क को पूरा होने में लगे कुल समय में से 50 मिलीसेकंड घटाने पर, टास्क का ब्लॉकिंग पीरियड मिलता है.
जब कोई टास्क चल रहा होता है, तब ब्राउज़र इंटरैक्शन को ब्लॉक कर देता है. हालांकि, जब तक टास्क बहुत ज़्यादा समय तक नहीं चलते, तब तक उपयोगकर्ता को इसकी जानकारी नहीं मिलती. जब कोई उपयोगकर्ता, कई लंबे टास्क वाले पेज से इंटरैक्ट करने की कोशिश करता है, तो यूज़र इंटरफ़ेस काम नहीं करता. अगर मुख्य थ्रेड लंबे समय तक ब्लॉक रहती है, तो हो सकता है कि यूज़र इंटरफ़ेस ठीक से काम न करे.
मुख्य थ्रेड को ज़्यादा देर तक ब्लॉक होने से रोकने के लिए, ज़्यादा समय लेने वाले काम को कई छोटे-छोटे कामों में बांटा जा सकता है.
टास्क को छोटे-छोटे हिस्सों में बांटने से, ब्राउज़र ज़्यादा प्राथमिकता वाले काम को बहुत जल्दी पूरा कर सकता है. इसमें उपयोगकर्ता की कार्रवाइयां भी शामिल हैं. इसलिए, यह तरीका बहुत अहम है. इसके बाद, बचे हुए टास्क पूरे किए जाते हैं. इससे यह पक्का होता है कि आपने जिन टास्क को शुरू में लाइन में लगाया था वे पूरे हो जाएं.
ऊपर दी गई इमेज में, उपयोगकर्ता के इंटरैक्शन से क्यू किया गया इवेंट हैंडलर, शुरू होने से पहले एक ज़्यादा समय लेने वाले काम का इंतज़ार करता है. इससे इंटरैक्शन में देरी होती है. इस स्थिति में, उपयोगकर्ता को लैग का पता चल सकता है. सबसे नीचे, इवेंट हैंडलर तुरंत शुरू हो सकता है. साथ ही, इंटरैक्शन तुरंत हो सकता है.
अब आपको पता चल गया है कि टास्क को छोटे-छोटे हिस्सों में बांटना क्यों ज़रूरी है. अब JavaScript में ऐसा करने का तरीका जानें.
टास्क मैनेज करने की रणनीतियां
सॉफ़्टवेयर आर्किटेक्चर में, काम को छोटे-छोटे फ़ंक्शन में बाँटने का सुझाव दिया जाता है:
function saveSettings () {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
इस उदाहरण में, saveSettings() नाम का एक फ़ंक्शन है. यह फ़ंक्शन, पांच फ़ंक्शन को कॉल करता है. ये फ़ंक्शन, फ़ॉर्म की पुष्टि करने, स्पिनर दिखाने, ऐप्लिकेशन के बैकएंड को डेटा भेजने, यूज़र इंटरफ़ेस को अपडेट करने, और आंकड़ों को भेजने के लिए इस्तेमाल किए जाते हैं.
saveSettings() को कॉन्सेप्ट के तौर पर अच्छी तरह से डिज़ाइन किया गया है. अगर आपको इनमें से किसी फ़ंक्शन को डीबग करना है, तो प्रोजेक्ट ट्री पर जाकर यह पता लगाया जा सकता है कि हर फ़ंक्शन क्या करता है. इस तरह से काम को बांटने से, प्रोजेक्ट को नेविगेट करना और मैनेज करना आसान हो जाता है.
हालांकि, यहां एक संभावित समस्या यह है कि JavaScript इन सभी फ़ंक्शन को अलग-अलग टास्क के तौर पर नहीं चलाता है, क्योंकि इन्हें saveSettings() फ़ंक्शन के अंदर एक्ज़ीक्यूट किया जाता है. इसका मतलब है कि ये पांचों फ़ंक्शन एक ही टास्क के तौर पर काम करेंगे.
saveSettings() जो पांच फ़ंक्शन को कॉल करता है. इस काम को एक बड़े टास्क के तौर पर किया जाता है. इसलिए, जब तक सभी पांच फ़ंक्शन पूरे नहीं हो जाते, तब तक कोई विज़ुअल जवाब नहीं मिलता.
सबसे अच्छे मामले में, इनमें से सिर्फ़ एक फ़ंक्शन, टास्क की कुल अवधि में 50 मिलीसेकंड या उससे ज़्यादा का समय ले सकता है. सबसे खराब स्थिति में, इनमें से ज़्यादातर टास्क बहुत ज़्यादा समय तक चल सकते हैं. ऐसा खास तौर पर उन डिवाइसों पर होता है जिनमें संसाधन सीमित होते हैं.
इस मामले में, saveSettings() को उपयोगकर्ता के क्लिक से ट्रिगर किया जाता है. साथ ही, जब तक पूरा फ़ंक्शन नहीं चल जाता, तब तक ब्राउज़र कोई जवाब नहीं दिखा पाता. इसलिए, इस ज़्यादा समय लेने वाले काम का नतीजा यह होता है कि यूज़र इंटरफ़ेस (यूआई) धीमा हो जाता है और काम नहीं करता. इसे खराब पेज के रिस्पॉन्स में लगने वाला समय (आईएनपी) के तौर पर मेज़र किया जाएगा.
कोड एक्ज़ीक्यूशन को मैन्युअल तरीके से चलाने में देरी करना
यह पक्का करने के लिए कि उपयोगकर्ता के लिए ज़रूरी टास्क और यूज़र इंटरफ़ेस (यूआई) के जवाब, कम प्राथमिकता वाले टास्क से पहले पूरे हो जाएं, मुख्य थ्रेड को कुछ समय के लिए रोकें. इससे ब्राउज़र को ज़्यादा ज़रूरी टास्क चलाने का मौका मिलेगा.
डेवलपर, टास्क को छोटे-छोटे हिस्सों में बांटने के लिए 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() के लिए कम से कम पाँच मिलीसेकंड की देरी लागू करना शुरू कर देगा.
setTimeout का एक और नुकसान यह है कि जब मुख्य थ्रेड को कोड चलाने की अनुमति दी जाती है, तब setTimeout का इस्तेमाल करके कोड को बाद के टास्क में चलाने के लिए टाला जाता है. इससे वह टास्क, क्यू के आखिर में जुड़ जाता है. अगर कोई अन्य टास्क पूरा होने के लिए इंतज़ार कर रहा है, तो वह टाले गए कोड से पहले चलेगा.
एक खास यिल्डिंग एपीआई: scheduler.yield()
scheduler.yield() एक ऐसा एपीआई है जिसे खास तौर पर ब्राउज़र में मुख्य थ्रेड को मैनेज करने के लिए डिज़ाइन किया गया है.
यह भाषा-लेवल का सिंटैक्स या कोई खास कंस्ट्रक्ट नहीं है. scheduler.yield() सिर्फ़ एक ऐसा फ़ंक्शन है जो Promise दिखाता है. इसे आने वाले समय में हल किया जाएगा. Promise के हल हो जाने के बाद, उससे जुड़ा कोई भी कोड (चाहे वह साफ़ तौर पर .then() चेन में हो या एसिंक फ़ंक्शन में await करने के बाद) उस फ़्यूचर टास्क में चलेगा.
असल में: await scheduler.yield() डालें. इससे फ़ंक्शन उस पॉइंट पर रुक जाएगा और मुख्य थ्रेड को कंट्रोल दे देगा. फ़ंक्शन के बाकी हिस्से को लागू करने के लिए, एक नया इवेंट-लूप टास्क शेड्यूल किया जाएगा. इसे फ़ंक्शन का जारी रहना कहा जाता है. जब वह टास्क शुरू होगा, तब इंतज़ार किया जा रहा प्रॉमिस पूरा हो जाएगा. इसके बाद, फ़ंक्शन वहीं से काम करना शुरू कर देगा जहाँ उसे छोड़ा गया था.
async function saveSettings () {
// Do critical work that is user-visible:
validateForm();
showSpinner();
updateUI();
// Yield to the main thread:
await scheduler.yield()
// Work that isn't user-visible, continued in a separate task:
saveToDatabase();
sendAnalytics();
}
saveSettings() को अब दो टास्क में बांट दिया गया है. इस वजह से, लेआउट और पेंटिंग को टास्क के बीच में चलाया जा सकता है. इससे उपयोगकर्ता को विज़ुअल रिस्पॉन्स तेज़ी से मिलता है. इसे अब बहुत कम समय में पॉइंटर इंटरैक्शन से मापा जाता है.
हालांकि, scheduler.yield() का फ़ायदा यह है कि यह अन्य यिल्डिंग अप्रोच की तुलना में, टास्क को जारी रखने को प्राथमिकता देता है. इसका मतलब है कि अगर किसी टास्क के बीच में यिल्ड किया जाता है, तो मौजूदा टास्क को जारी रखने की प्रोसेस, पहले चलेगी. इसके बाद, अन्य मिलते-जुलते टास्क शुरू किए जाएंगे.
इससे, टास्क के अन्य सोर्स से मिले कोड को आपके कोड के एक्ज़ीक्यूशन के क्रम में रुकावट डालने से रोका जा सकता है. जैसे, तीसरे पक्ष की स्क्रिप्ट से मिले टास्क.
scheduler.yield() का इस्तेमाल करने पर, बातचीत वहीं से शुरू होती है जहां आपने छोड़ी थी. इसके बाद, अन्य टास्क पर स्विच किया जाता है.
अलग-अलग ब्राउज़र पर काम करने की सुविधा
scheduler.yield() अभी सभी ब्राउज़र पर काम नहीं करता है. इसलिए, फ़ॉलबैक की ज़रूरत होती है.
एक तरीका यह है कि scheduler-polyfill को अपने बिल्ड में शामिल करें. इसके बाद, scheduler.yield() का सीधे तौर पर इस्तेमाल किया जा सकता है. पॉलीफ़िल, टास्क शेड्यूल करने वाले अन्य फ़ंक्शन पर वापस जाने की सुविधा को मैनेज करेगा, ताकि यह अलग-अलग ब्राउज़र पर एक जैसा काम करे.
इसके अलावा, कम जटिल वर्शन को कुछ लाइनों में लिखा जा सकता है. इसमें सिर्फ़ setTimeout का इस्तेमाल किया जाता है. अगर scheduler.yield() उपलब्ध नहीं है, तो इसे फ़ॉलबैक के तौर पर Promise में रैप किया जाता है.
function yieldToMain () {
if (globalThis.scheduler?.yield) {
return scheduler.yield();
}
// Fall back to yielding with setTimeout.
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
जिन ब्राउज़र पर scheduler.yield() काम नहीं करता उन्हें प्राथमिकता नहीं दी जाएगी. हालांकि, ब्राउज़र को चालू रखने के लिए, उन्हें अब भी प्राथमिकता दी जाएगी.
आखिर में, ऐसे मामले भी हो सकते हैं जहां आपके कोड को मुख्य थ्रेड को नहीं रोकना चाहिए. ऐसा तब होता है, जब उसके काम को प्राथमिकता नहीं दी जाती. उदाहरण के लिए, कोई ऐसा पेज जिस पर पहले से ही काम चल रहा है. ऐसे में, मुख्य थ्रेड को रोकने से, कुछ समय के लिए काम पूरा नहीं हो पाएगा. ऐसे में, scheduler.yield() को एक तरह का प्रोग्रेसिव एन्हांसमेंट माना जा सकता है: जिन ब्राउज़र में scheduler.yield() उपलब्ध है उनमें इसका इस्तेमाल किया जा सकता है. अगर यह उपलब्ध नहीं है, तो इसे जारी रखा जा सकता है.
इसे सुविधा का पता लगाकर और एक लाइन में लिखे गए कोड में एक माइक्रोटास्क के पूरा होने का इंतज़ार करके किया जा सकता है:
// Yield to the main thread if scheduler.yield() is available.
await globalThis.scheduler?.yield?.();
लंबे समय तक चलने वाले काम को scheduler.yield() की मदद से छोटे-छोटे हिस्सों में बांटना
scheduler.yield() का इस्तेमाल करने के इन तरीकों का फ़ायदा यह है कि इसे किसी भी async फ़ंक्शन में इस्तेमाल किया जा सकता है.await
उदाहरण के लिए, अगर आपको कई ऐसे जॉब चलाने हैं जो अक्सर एक लंबे टास्क में बदल जाते हैं, तो टास्क को छोटे-छोटे हिस्सों में बांटने के लिए, यिल्ड इंसर्ट किए जा सकते हैं.
async function runJobs(jobQueue) {
for (const job of jobQueue) {
// Run the job:
job();
// Yield to the main thread:
await yieldToMain();
}
}
runJobs() को जारी रखने को प्राथमिकता दी जाएगी. हालांकि, इससे ज़्यादा प्राथमिकता वाले काम भी किए जा सकेंगे. जैसे, उपयोगकर्ता के इनपुट के हिसाब से विज़ुअल जवाब देना. इसके लिए, नौकरियों की लंबी सूची के पूरा होने का इंतज़ार नहीं करना पड़ेगा.
हालांकि, इससे बेहतर तरीके से यिल्डिंग नहीं हो पाती. scheduler.yield() तेज़ और असरदार है, लेकिन इसमें कुछ ओवरहेड होता है. अगर jobQueue में कुछ जॉब बहुत कम समय के लिए हैं, तो ओवरहेड की वजह से, असल काम को पूरा करने के मुकाबले, यिल्डिंग और फिर से शुरू करने में ज़्यादा समय लग सकता है.
एक तरीका यह है कि जॉब को बैच में बांटा जाए. साथ ही, अगर पिछले यिल्ड को काफ़ी समय हो गया है, तो ही उनके बीच यिल्ड किया जाए. टास्क को लंबे समय तक चलने वाले टास्क बनने से रोकने के लिए, सामान्य समयसीमा 50 मिलीसेकंड होती है. हालांकि, रिस्पॉन्स देने की क्षमता और जॉब क्यू को पूरा करने में लगने वाले समय के बीच समझौता करके, इसे अडजस्ट किया जा सकता है.
async function runJobs(jobQueue, deadline=50) {
let lastYield = performance.now();
for (const job of jobQueue) {
// Run the job:
job();
// If it's been longer than the deadline, yield to the main thread:
if (performance.now() - lastYield > deadline) {
await yieldToMain();
lastYield = performance.now();
}
}
}
इस वजह से, जॉब को इस तरह से बांटा जाता है कि उन्हें पूरा होने में ज़्यादा समय न लगे. हालांकि, रनर सिर्फ़ हर 50 मिलीसेकंड में मुख्य थ्रेड को प्रोसेस करता है.
isInputPending() का इस्तेमाल न करें
isInputPending() एपीआई की मदद से यह पता लगाया जा सकता है कि किसी उपयोगकर्ता ने पेज से इंटरैक्ट करने की कोशिश की है या नहीं. साथ ही, यह सिर्फ़ तब नतीजे दिखाता है, जब कोई इनपुट लंबित हो.
इससे JavaScript को तब तक जारी रखने की अनुमति मिलती है, जब तक कोई इनपुट बाकी न हो. ऐसा टास्क कतार के आखिर में पहुंचने और रुकने के बजाय किया जाता है. इससे परफ़ॉर्मेंस में काफ़ी सुधार हो सकता है. इसके बारे में Intent to Ship में बताया गया है. यह उन साइटों के लिए फ़ायदेमंद है जो मुख्य थ्रेड पर वापस नहीं आ पाती हैं.
हालांकि, उस एपीआई के लॉन्च होने के बाद से, हमें यिल्डिंग के बारे में ज़्यादा जानकारी मिली है. खास तौर पर, आईएनपी के लॉन्च होने के बाद. हम अब इस एपीआई का इस्तेमाल करने का सुझाव नहीं देते हैं. इसके बजाय, हम इनपुट के लंबित होने या न होने से कोई फ़र्क़ नहीं पड़ता. इसके कई कारण हैं:
- ऐसा हो सकता है कि
isInputPending(), कुछ मामलों में उपयोगकर्ता के इंटरैक्ट करने के बावजूदfalseको गलत तरीके से वापस कर दे. - टास्क को सिर्फ़ इनपुट के दौरान नहीं, बल्कि अन्य स्थितियों में भी रोकना चाहिए. रिस्पॉन्सिव वेब पेज उपलब्ध कराने के लिए, ऐनिमेशन और अन्य सामान्य यूज़र इंटरफ़ेस अपडेट भी उतने ही ज़रूरी हो सकते हैं.
- इसके बाद, ज़्यादा बेहतर यिल्डिंग एपीआई लॉन्च किए गए हैं. ये यिल्डिंग से जुड़ी समस्याओं को हल करते हैं. जैसे,
scheduler.postTask()औरscheduler.yield().
नतीजा
टास्क मैनेज करना मुश्किल है. हालांकि, ऐसा करने से यह पक्का किया जा सकता है कि आपका पेज, उपयोगकर्ता के इंटरैक्शन का जवाब ज़्यादा तेज़ी से दे. टास्क मैनेज करने और उन्हें प्राथमिकता देने के लिए, कोई एक तरीका नहीं है. इसके लिए, कई अलग-अलग तरीके हैं. टास्क मैनेज करते समय, इन मुख्य बातों का ध्यान रखें:
- उपयोगकर्ता के लिए ज़रूरी टास्क के लिए, मुख्य थ्रेड को प्राथमिकता दें.
scheduler.yield()का इस्तेमाल करें. इससे क्रॉस-ब्राउज़र फ़ॉलबैक की सुविधा मिलती है. साथ ही, इससे एर्गोनॉमिक तरीके से काम करने और प्राथमिकता के आधार पर काम जारी रखने में मदद मिलती है- आखिर में, अपने फ़ंक्शन में कम से कम काम करें.
scheduler.yield(), टास्क शेड्यूलिंग से जुड़े scheduler.postTask(), और टास्क को प्राथमिकता देने के बारे में ज़्यादा जानने के लिए, टास्क को प्राथमिकता देने से जुड़े शेड्यूलिंग एपीआई के दस्तावेज़ देखें.
इनमें से एक या उससे ज़्यादा टूल की मदद से, आपको अपने ऐप्लिकेशन में काम को इस तरह से व्यवस्थित करना चाहिए कि उपयोगकर्ता की ज़रूरतों को प्राथमिकता दी जा सके. साथ ही, यह पक्का किया जा सके कि कम ज़रूरी काम भी पूरा हो जाए. इससे उपयोगकर्ताओं को बेहतर अनुभव मिलेगा. साथ ही, उन्हें ज़्यादा रिस्पॉन्सिव और इस्तेमाल करने में मज़ेदार ऐप्लिकेशन मिलेगा.
इस गाइड की तकनीकी जांच करने के लिए, Philip Walton को खास तौर पर धन्यवाद.
थंबनेल इमेज, Unsplash से ली गई है. इसके लिए, हम Amirali Mirhashemian का शुक्रिया अदा करते हैं.