आपको "मुख्य थ्रेड को ब्लॉक न करें" और "लंबे टास्क को बांटें" कहा गया है, लेकिन इनका क्या मतलब है?
पब्लिश किया गया: 30 सितंबर, 2022, पिछली बार अपडेट किया गया: 19 दिसंबर, 2024
JavaScript ऐप्लिकेशन को तेज़ रखने के लिए, आम तौर पर ये सुझाव दिए जाते हैं:
- "मुख्य थ्रेड को ब्लॉक न करें."
- "लंबे टास्क को छोटे-छोटे हिस्सों में बांटें."
यह एक बेहतरीन सलाह है, लेकिन इसमें क्या काम करना है? कम JavaScript शिप करना अच्छा है, लेकिन क्या इससे यूज़र इंटरफ़ेस अपने-आप ज़्यादा रिस्पॉन्सिव हो जाते हैं? हो सकता है, लेकिन ऐसा भी हो सकता है कि न हो.
JavaScript में टास्क को ऑप्टिमाइज़ करने का तरीका जानने के लिए, आपको सबसे पहले यह जानना होगा कि टास्क क्या होते हैं और ब्राउज़र उन्हें कैसे मैनेज करता है.
टास्क क्या होता है?
टास्क, ब्राउज़र का कोई भी अलग काम होता है. इसमें एचटीएमएल और सीएसएस को रेंडर करना, पार्स करना, JavaScript चलाना, और दूसरे तरह के ऐसे काम शामिल हैं जिन पर आपका सीधा कंट्रोल नहीं हो सकता. इन सभी में से, आपका लिखा गया JavaScript शायद टास्क का सबसे बड़ा सोर्स है.
JavaScript से जुड़े टास्क, परफ़ॉर्मेंस पर कई तरीकों से असर डालते हैं:
- जब कोई ब्राउज़र स्टार्टअप के दौरान कोई JavaScript फ़ाइल डाउनलोड करता है, तो वह उस JavaScript को पार्स और कंपाइल करने के लिए टास्क को सूची में जोड़ता है, ताकि उसे बाद में चलाया जा सके.
- पेज के खुले रहने के दौरान, JavaScript के काम करने पर टास्क कतार में लग जाते हैं. जैसे, इवेंट हैंडलर के ज़रिए इंटरैक्शन का जवाब देना, JavaScript से चलने वाले ऐनिमेशन, और बैकग्राउंड गतिविधि, जैसे कि आंकड़े इकट्ठा करना.
वेब वर्कर्स और मिलते-जुलते एपीआई को छोड़कर, यह सारा काम मुख्य थ्रेड पर होता है.
मुख्य थ्रेड क्या है?
मुख्य थ्रेड में ब्राउज़र में ज़्यादातर टास्क चलते हैं. साथ ही, इसमें आपके लिखे गए ज़्यादातर JavaScript को लागू किया जाता है.
मुख्य थ्रेड एक बार में सिर्फ़ एक टास्क प्रोसेस कर सकता है. जिस टास्क को पूरा करने में 50 मिलीसेकंड से ज़्यादा समय लगता है उसे लॉन्ग टास्क कहा जाता है. 50 मिलीसेकंड से ज़्यादा समय वाले टास्क के लिए, टास्क के कुल समय में से 50 मिलीसेकंड घटाने पर, टास्क की ब्लॉकिंग अवधि मिलती है.
ब्राउज़र, किसी भी अवधि के टास्क के चलने के दौरान इंटरैक्शन को ब्लॉक कर देता है. हालांकि, जब तक टास्क बहुत ज़्यादा समय तक नहीं चलते, तब तक उपयोगकर्ता को इसकी जानकारी नहीं मिलती. जब कोई उपयोगकर्ता किसी पेज से इंटरैक्ट करने की कोशिश करता है, तो अगर उस पेज पर कई लंबे टास्क हैं, तो यूज़र इंटरफ़ेस काम नहीं करेगा. अगर मुख्य थ्रेड बहुत लंबे समय तक ब्लॉक रहता है, तो हो सकता है कि यूज़र इंटरफ़ेस काम करना बंद कर दे.
मुख्य थ्रेड को ज़्यादा देर तक ब्लॉक होने से बचाने के लिए, किसी लंबे टास्क को कई छोटे टास्क में बांटें.
यह ज़रूरी है, क्योंकि जब टास्क को अलग-अलग किया जाता है, तो ब्राउज़र ज़्यादा प्राथमिकता वाले कामों को तुरंत पूरा कर सकता है. इनमें उपयोगकर्ता के इंटरैक्शन भी शामिल हैं. इसके बाद, बाकी बचे टास्क पूरे हो जाते हैं. इससे यह पक्का होता है कि आपने शुरुआत में जो काम लाइन में लगाया था वह पूरा हो गया है.
ऊपर दिए गए इलस्ट्रेशन में सबसे ऊपर, उपयोगकर्ता के इंटरैक्शन से लाइन में लगा हुआ इवेंट हैंडलर, एक लंबे टास्क के पूरा होने का इंतज़ार कर रहा है. इससे इंटरैक्शन में देरी होती है. इस स्थिति में, हो सकता है कि उपयोगकर्ता को ऐप्लिकेशन में रुकावट महसूस हुई हो. सबसे नीचे, इवेंट हैंडलर जल्दी से चलना शुरू हो सकता है और इंटरैक्शन तुरंत महसूस हो सकता है.
अब आपको पता है कि टास्क को अलग-अलग हिस्सों में बांटना क्यों ज़रूरी है. अब JavaScript में ऐसा करने का तरीका जानें.
टास्क मैनेज करने की रणनीतियां
सॉफ़्टवेयर आर्किटेक्चर में एक आम सलाह यह है कि अपने काम को छोटे फ़ंक्शन में बांटें:
function saveSettings () {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
इस उदाहरण में, saveSettings()
नाम का एक फ़ंक्शन है. यह फ़ंक्शन, फ़ॉर्म की पुष्टि करने, स्पिनर दिखाने, ऐप्लिकेशन के बैकएंड को डेटा भेजने, यूज़र इंटरफ़ेस को अपडेट करने, और आंकड़े भेजने के लिए पांच फ़ंक्शन को कॉल करता है.
saveSettings()
को कॉन्सेप्ट के हिसाब से सही तरीके से बनाया गया है. अगर आपको इनमें से किसी फ़ंक्शन को डीबग करना है, तो प्रोजेक्ट ट्री में जाकर यह पता लगाया जा सकता है कि हर फ़ंक्शन क्या करता है. इस तरह से काम को बांटने से, प्रोजेक्ट को नेविगेट और मैनेज करना आसान हो जाता है.
हालांकि, यहां एक संभावित समस्या यह है कि JavaScript इनमें से हर फ़ंक्शन को अलग-अलग टास्क के तौर पर नहीं चलाता, क्योंकि ये 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
के हल होने के बाद चलने के लिए चेन किया गया कोई भी कोड, आने वाले समय में उस टास्क में चलेगा. 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();
}
हालांकि, scheduler.yield()
का असली फ़ायदा यह है कि किसी टास्क के बीच में, टास्क को जारी रखने को प्राथमिकता दी जाती है. इसका मतलब है कि अगर किसी टास्क के बीच में, टास्क को जारी रखने का विकल्प चुना जाता है, तो मौजूदा टास्क को जारी रखने की प्रोसेस, मिलते-जुलते किसी भी दूसरे टास्क के शुरू होने से पहले शुरू हो जाएगी.
इससे, तीसरे पक्ष की स्क्रिप्ट जैसे अन्य टास्क सोर्स के कोड, आपके कोड के क्रम में रुकावट नहीं डाल पाते.
अलग-अलग ब्राउज़र पर काम करना
scheduler.yield()
अभी सभी ब्राउज़र पर काम नहीं करता. इसलिए, फ़ॉलबैक की ज़रूरत है.
एक तरीका यह है कि अपने बिल्ड में scheduler-polyfill
को छोड़ दें. इसके बाद, scheduler.yield()
का सीधे इस्तेमाल किया जा सकता है. polyfill, टास्क शेड्यूल करने वाले अन्य फ़ंक्शन पर स्विच करने की सुविधा देगा, ताकि यह सभी ब्राउज़र पर एक जैसा काम करे.
इसके अलावा, scheduler.yield()
के उपलब्ध न होने पर, फ़ॉलबैक के तौर पर सिर्फ़ setTimeout
का इस्तेमाल करके, कम जटिल वर्शन को कुछ लाइनों में लिखा जा सकता है.
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 को जारी रखने में मदद मिलती है. ऐसा करने पर, JavaScript को टास्क की सूची में सबसे पीछे नहीं भेजा जाता. इससे परफ़ॉर्मेंस में काफ़ी सुधार हो सकता है. इस बारे में शिप करने का इंटेंट में बताया गया है. ऐसा उन साइटों के लिए किया जा सकता है जो मुख्य थ्रेड पर वापस नहीं आती हैं.
हालांकि, उस एपीआई के लॉन्च होने के बाद, हमें Yielding के बारे में ज़्यादा जानकारी मिली है. खास तौर पर, INP के लॉन्च होने के बाद. हमारा सुझाव है कि अब इस एपीआई का इस्तेमाल न करें. इसके बजाय, कई वजहों से इनपुट बाकी है या नहीं, इस बात से कोई फ़र्क़ नहीं पड़ता, इसलिए 'फ़ीड सबमिट करें' विकल्प का इस्तेमाल करें:
- कुछ मामलों में, उपयोगकर्ता के इंटरैक्ट करने के बावजूद,
isInputPending()
गलत तरीके सेfalse
दिखा सकता है. - इनपुट के अलावा, अन्य मामलों में भी टास्क जनरेट होने चाहिए. रिस्पॉन्सिव वेब पेज उपलब्ध कराने के लिए, ऐनिमेशन और यूज़र इंटरफ़ेस के अन्य नियमित अपडेट भी उतने ही ज़रूरी हो सकते हैं.
- इसके बाद, ज़्यादा बेहतर परफ़ॉर्म करने वाले एपीआई लॉन्च किए गए हैं. इनसे
scheduler.postTask()
औरscheduler.yield()
जैसी समस्याओं को हल करने में मदद मिलती है.
नतीजा
टास्क मैनेज करना चुनौती भरा होता है. हालांकि, ऐसा करने से यह पक्का होता है कि आपका पेज, उपयोगकर्ता के इंटरैक्शन का तुरंत जवाब दे. टास्क मैनेज करने और उन्हें प्राथमिकता देने के लिए, कोई एक सलाह नहीं है. इसके लिए, कई अलग-अलग तरीके अपनाए जा सकते हैं. टास्क मैनेज करते समय, इन मुख्य बातों का ध्यान रखें:
- उपयोगकर्ता के लिए ज़रूरी टास्क के लिए, मुख्य थ्रेड को प्राथमिकता दें.
scheduler.yield()
का इस्तेमाल करके, आसानी से फ़ंक्शन के आउटपुट पाएं और उन्हें प्राथमिकता के हिसाब से क्रम में लगाएं. इसके लिए, क्रॉस-ब्राउज़र फ़ॉलबैक का इस्तेमाल करें- आखिर में, अपने फ़ंक्शन में कम से कम काम करें.
scheduler.yield()
, टास्क शेड्यूलिंग से जुड़े उसके scheduler.postTask()
, और टास्क को प्राथमिकता देने के बारे में ज़्यादा जानने के लिए, टास्क को प्राथमिकता देने वाले शेड्यूलिंग एपीआई के दस्तावेज़ देखें.
इनमें से एक या उससे ज़्यादा टूल का इस्तेमाल करके, अपने ऐप्लिकेशन में काम को इस तरह से व्यवस्थित किया जा सकता है कि वह उपयोगकर्ता की ज़रूरतों को प्राथमिकता दे. साथ ही, यह भी पक्का किया जा सकता है कि कम ज़रूरी काम भी पूरा हो जाए. इससे उपयोगकर्ताओं को बेहतर अनुभव मिलेगा. साथ ही, ऐप्लिकेशन को इस्तेमाल करना ज़्यादा आसान और मज़ेदार होगा.
इस गाइड की तकनीकी जांच करने के लिए, फ़िलिप वाल्टन का खास धन्यवाद.
थंबनेल इमेज, Unsplash से ली गई है. इमेज का क्रेडिट Amirali Mirhashemian को जाता है.