ব্রাউজার বা Node.js এ জাভাস্ক্রিপ্ট এক্সিকিউশন ফ্লো হয় event loop এর উপর ভিত্তি করে।
একটি সঠিক আর্কিটেকচার তৈরি করতে এবং অপ্টিমাইজেশনের জন্য কিভাবে ইভেন্ট লুপ কাজ করে তা বুঝা গুরুত্বপূর্ণ।
প্রথমে আমরা এর তাত্ত্বিক বিশ্লেষণ করব, এবং তা আমাদের ব্যবহারিক জীবনে এটি কিভাবে ব্যবহার করতে পারি তা দেখব।
event loop এর ধারণাটি একদম সহজ। একটি ইনফিনিট লুপ থাকে, যেখানে জাভাস্ক্রিপ্ট ইঞ্জিন টাস্কের জন্য অপেক্ষা করে, টাস্ক আসলে তাদের এক্সিকিউট করে স্লিপ মোডে যাবে, এবং আরো টাস্কের জন্য অপেক্ষা করবে।
ইঞ্জিনের অ্যালগরিদমটি হল:
- যখন কোন টাস্ক আসবে:
- তাদের এক্সিকিউট করবে, প্রথমে সবার শুরুতে আসা টাস্কটা এক্সিকিউট করবে।
- টাস্ক আসা পর্যন্ত স্লিপ মোডে থাকবে, এরপর আবার ১ম ধাপে যাবে।
যখন আমরা কোন একটি পেজ ব্রাউজ করা শুরু করি তখনি এটি ইনিশিয়ালাইজ হয়। বেশিরভাগ সময় জাভাস্ক্রিপ্ট ইঞ্জিন কিছু করে না, এটি শুধুমাত্র রান হয় কোন একটি স্ক্রিপ্ট/হ্যান্ডেলার/ইভেন্ট এর কার্যকলাপ ঘটলে।
কিছু টাস্কের উদাহরণ:
- যখন কোন এক্সটার্নাল স্ক্রিপ্ট লোড হয়
<script src="...">
, টাস্ক এটিকে এক্সিকিউট করে। - যখন ইউজার মাউস মুভ করবে, টাস্কটি হবে
mousemove
ইভেন্টকে ডিসপ্যাস করা এবং হ্যান্ডেলারটি রান করা। - যখন শিডিউলড ফাংশন যেমন
setTimeout
কল করা, তখন টাস্কটি হবে এর কলব্যাককে এক্সিকিউট করা। - ...এবং আরো অনেক
টাস্ক সেট হয় -- ইঞ্জিন এদের হ্যান্ডেল করে -- এবং আরো টাস্কের জন্য অপেক্ষা করে।
ইঞ্জিন ব্যাস্ত থাকার সময় কোন একটি টাস্ক আসলে, এটি enqueued হিসেবে অপেক্ষা করবে।
টাস্ক queue হিসেবে থাকে যাকে বলা হয় "macrotask queue" (v8 term):
যেমন, যখন জাভাস্ক্রিপ্ট ইঞ্জিন একটি script
এক্সিকিউট করতে ব্যস্ত, তখন ইউজার মাউস মুভ করার জন্য mousemove
ইভেন্ট টাস্ক এবং setTimeout
ফাংশন ইত্যাদি queue তে অপেক্ষা করবে যা আমরা উপরের ছবিটিতে দেখছি।
এতদূর আমরা বুঝছি, তাই না?
আরো দুটি ব্যাপার:
- ইঞ্জিন টাস্ক এক্সিকিউশনের সময় কোন পেজে কোন কিছু রেন্ডার হবে না। এখানে টাস্ক এক্সিকিউট হতে অনেক সময় লাগছে কিনা তা বিবেচ্য নয়। মূল কথা DOM এর যে কোন পরিবর্তন ঘটবে টাস্ক এক্সিকিউট হওয়ার পর।
- যদি টাস্ক এক্সিকিউট হতে অনেক সময় লাগে, সে সময় ব্রাউজার অন্য কোন টাস্ক এক্সিকিউট করতে পারে না , যেমন ইউজারের ইভেন্ট প্রসেসিং। সুতরাং কিছু সময় পর, এটি এমন একটি অ্যালার্ট দেখাবে "Page Unresponsive", যা আমাদের পুরো পেজকে এক্সিট করতে পরামর্শ দেয়। জটিল গণনা বা কোন ধরণের প্রোগ্রামিং এরর বা ইনফিনিট লুপের জন্য এমন হতে পারে।
এতক্ষণ আমরা তাত্ত্বিক ব্যপারটা জানলাম। এখন চলুন দেখি কিভাবে এটি প্রাত্যহিক জীবনে ব্যবহার করব।
ধরুন আমাদের একটি টাস্ক আছে যা প্রচুর পরিমাণ CPU রিসোর্স ব্যবহার করে।
যেমন, সিনট্যাক্স হাইলাইটিং কিছুটা CPU-heavy টাস্ক। কোন একটি কোড হাইলাইট করার সময়, এটি বিভিন্ন ধরণের অ্যানালাইসিস করে, বিভিন্ন এলিমেন্ট তৈরি করে, এবং তাদের DOM এ সংযুক্ত করে। দীর্ঘ কোন কোডকে হাইলাইট করার সময় এটি অনেক সময় নিবে।
যখন ইঞ্জিন সিনট্যাক্স হাইলাইটিং প্রসেস করবে, তখন এটি DOM এর অন্য কোন কাজ বা ইউজার ইভেন্ট ইত্যাদি প্রসেস করতে পারবে না। এছাড়াও অনেক সময় ব্রাউজার হ্যাং হয়ে যেতে পারে যা আমরা আশা করি না।
এইক্ষেত্রে একটি বড় টাস্ককে অসংখ্য ক্ষুদ্র ক্ষুদ্র অংশে ভাগ করে আমরা সমস্যাটি সমাধান করতে পারি। প্রথম ১০০ লাইনকে হাইলাইট করবে তারপর পরের ১০০ লাইন কে শিডিউল setTimeout
(zero-delay) এভাবে শেষ পর্যন্ত।
সমস্যাটি বুঝতে এবং সহজে প্রদর্শনের জন্য সিনট্যাক্স হাইলাইটারের বদলে চলুন 1
থেকে 1000000000
পর্যন্ত গণনা করার একটি ফাংশন লিখি।
যদি আপনি নিচের কোডটি রান করেন তাহলে ব্রাউজার কিছু সময়ের জন্য হ্যাং হয়ে যাবে। সার্ভার সাইডে আমরা সহজেই এটি বুঝতে পারব, ব্রাউজারের ক্ষেত্রে আপনি মাউসের ডানের বাটন (contextmenu
ইভেন্ট ট্রিগার) ক্লিক করুন গণনাটি শেষ হওয়া পর্যন্ত দেখবেন কন্টেক্সট মেনুটি দেখাবে না।
let i = 0;
let start = Date.now();
function count() {
// do a heavy job
for (let j = 0; j < 1e9; j++) {
i++;
}
alert("Done in " + (Date.now() - start) + 'ms');
}
count();
এছাড়াও এই ধরণের একটি ওয়ার্নিং দেখাতে পারে "the script takes too long"।
চলুন এখন আমরা এটিকে setTimeout
দ্বারা বিভক্ত করি:
let i = 0;
let start = Date.now();
function count() {
// do a piece of the heavy job (*)
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
} else {
setTimeout(count); // schedule the new call (**)
}
}
count();
এখন গণনা চলার সময় আমাদের ব্রাউজার হ্যাং হবে না, মাউসের ডানের বাটন (contextmenu
ইভেন্ট ট্রিগার) ক্লিক করুন দেখবেন সাথে সাথে কন্টেক্সট মেনুটি দেখাবে।
প্রতিটি count
কলে (*)
অংশটুকু এক্সিকিউট হবে, এবং যতক্ষণ প্রয়োজন হয় ততক্ষণ (**)
অংশটুকু শিডিউলড হবে:
- প্রথমবার রান হওয়ার পর গণনা হবে:
i=1...1000000
। - দ্বিতীয়বান রান হওয়ার পর গণনা হবে:
i=1000001..2000000
. - ...এভাবে চলতে থাকবে
এখন, প্রথমবার রান হওয়ার সময় যদি অন্য কোন ইভেন্ট যেমন মাউসের ডানের বাটন ক্লিকে contextmenu
ইভেন্ট ট্রিগার হলে তা event loop এ queue তে এক্সিকিউট হওয়ার জন্য অপেক্ষা করবে এবং এরপর count
এর দ্বিতীয় অংশটুকু এক্সিকিউট হবে। সুতরাং গণনার সময় অন্য কোন ইভেন্ট ট্রিগার হলে তা শিডিউলের মাঝের queue তে অপেক্ষা করবে।
উভয়ই ক্ষেত্রের লক্ষনীয় পার্থক্য হল -- setTimeout
এর দ্বারা কোডকে ক্ষুদ্র ক্ষুদ্র অংশে বিভক্ত করার ফলে তাদের সময় একটু বেশি লাগছে।
চলুন পারফরম্যান্স আর কিছুটা বাড়াতে চেষ্টা করি।
শিডিউল count()
কলটাকে আমরা শুরুত নিয়ে আসি :
let i = 0;
let start = Date.now();
function count() {
// move the scheduling to the beginning
if (i < 1e9 - 1e6) {
setTimeout(count); // schedule the new call
}
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
}
}
count();
এখন count()
শুরুর পূর্বে আমরা যাচাই করে নিচ্ছি আমাদের আর count()
লাগবে কিনা যদি লাগে তাহলে সাথে সাথে গণনা শুরুর পূর্বেই একে আমরা শিডিউল করে দিচ্ছি।
যদি আপনি এটি রান করেন, তাহলে দেখবেন পূর্বেরটার চেয়ে এটি আরো দ্রুত গণনা শেষ করছে।
কেন?
কারণ, ব্রাউজারের সর্বনিম্ন নেস্টেড setTimeout
কলের জন্য 4ms সময় লাগে। যদি আমরা delay
কে 0
সেট করি তারপরও। সুতরাং যদি একে আগে শিডিউল করি তাহলে এটি দ্রুত রান হবে।
আমরা দেখলাম কিভাবে আমাদের একটি CPU-hungry দীর্ঘ কাজকে ছোট ছোট অংশে বিভক্ত করতে পারি, যার ফলে এটি আমাদের UI কে ব্লক করতে পারে না, এবং সর্বমোট এক্সিকিউশন সময়ের মধ্যেও তেমন পার্থক্য থাকে না।
একটি দীর্ঘ টাস্ককে ছোট ছোট টাস্কে ভাগ করার আরেকটি সুবিধা হল UI তে টাস্কের প্রগ্রেস দেখাতে পারব।
উপরে উল্লেখিত কোডে আমরা দেখছি সম্পূর্ণ টাস্কটি শেষ হলে আমরা UI তে তা দেখায়।
তবে অনেক সময় এই ফিচারটি সুবিধাজনক, কেননা অনেক সময় আমাদের অনেক এলিমেন্ট তৈরি করা লাগে এবং তাদের বিভিন্ন স্ট্যাইল অ্যাট্রিবিউট এর বিভিন্ন পরিবর্তন করতে হয় এবং আমাদের এলিমেন্টগুলো সম্পূর্ণ হতে কিছু সময় লাগতে পারে, এই সময় অসম্পূর্ণ এলিমেন্টগুলো ইউজারের কাছে অদৃশ্য থাকা উচিত, তাই না?
এখানে একটি উদাহরণ দেখুন, এখানে ফাংশনটির ক্যাল্কুলেশন শেষ না হওয়া পর্যন্ত i
এর মান দেখাবে না, এক্ষেত্রে শুধুমাত্র শেষ মানটি অর্থাৎ 999999
দেখাবে:
<div id="progress"></div>
<script>
function count() {
for (let i = 0; i < 1e6; i++) {
i++;
progress.innerHTML = i;
}
}
count();
</script>
...কিন্তু আমরা টাস্কটি চলার সময় UI তে এমন কিছু দেখাতে চাই যেন ইউজার বুঝতে পারে টাস্কটি চলছে, যেমন progress bar।
যদি আমরা কাজটিকে setTimeout
এর মাধ্যমে বিভক্ত করে i
এর মান UI তে দেখায় তাহলে ইউজার কাউন্টের প্রগ্রেস দেখবে।
এখন দেখুন:
<div id="progress"></div>
<script>
let i = 0;
function count() {
// do a piece of the heavy job (*)
do {
i++;
progress.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e7) {
setTimeout(count);
}
}
count();
</script>
এখন <div>
এ i
এর মান বাড়ছে তা দেখব, যা একটি প্রগ্রেস বারের মত।
একটি ইভেন্ট হ্যান্ডেলার আমাদের এমনভাবে ডিজাইন করতে হয় যেন হ্যান্ডেলারের সকল টাস্ক শেষ হওয়ার পর যেন আমাদের নতুন টাস্কটি শুরু হয়। এক্ষেত্রে আমরা setTimeout
এর দ্বিতীয় আর্গুমেন্ট 0
ms সেট করব।
যা আমরা এই অধ্যায়ে আলোচনা করেছিলাম info:dispatch-events আমরা একটি উদাহরণ দেখেছিলাম: কাস্টম ইভেন্ট menu-open
কে setTimeout
এ আবদ্ধ করেছি, যার ফলে ইভেন্টটি ডিস্প্যাচ হয় সম্পূর্ন "click" ইভেন্টটি হ্যান্ডেল হওয়ার পর।
menu.onclick = function() {
// ...
// মেনুতে ক্লিকের জন্য একটি কাস্টম ইভেন্ট তৈরি করি
let customEvent = new CustomEvent("menu-open", {
bubbles: true
});
// ইভেন্টটি অ্যাসিঙ্ক্রোনাসলি ডিস্প্যাচ হয়
setTimeout(() => menu.dispatchEvent(customEvent));
};
এই অধ্যায়ে আলোচিত macrotasks এর পাশাপাশি microtasks নামের আরেকটি টার্ম আছে, যা এই অধ্যায়ে আলোচনা করা হয়েছে info:microtask-queue।
মাইক্রোটাস্ক আমাদের স্ক্রিপ্ট থেকে জেনারেট হয়। যা promises দ্বারা তৈরি করা হয়: .then/catch/finally
কে এক্সিকিউশনের সময় এদের হ্যান্ডেলার মাইক্রোটাস্ক হিসেবে বিবেচিত হয়। await
এর ক্ষেত্রেও বিহাইন্ড দ্যা সীনে মাইক্রোটাস্ক ব্যবহৃত হয়, যেহেতু এটি promise
এর উপর ভিত্তি করে গড়ে উঠেছে।
এছাড়াও একটি বিশেষ ফাংশন আছে queueMicrotask(func)
যার ফলে func
টি মাইক্রোটাস্ক কিউতে এক্সিকিউশন হয়।
প্রতিটি macrotask এর পর, ইঞ্জিন অন্য সকল macrotask এর আগে microtask এর সকল টাস্ক সম্পন্ন করবে।
এই উদাহরণটি দেখুন:
setTimeout(() => alert("timeout"));
Promise.resolve()
.then(() => alert("promise"));
alert("code");
এখানে কোড এক্সিকিউশনের ধাপগুলো খেয়াল করছেন?
- প্রথমে দেখাবে
code
, কেননা এটি একটি সিঙ্ক্রোনাস কল। - এরপর দেখাবে
promise
, কেননা.then
পাস হয় microtask তে, তাই এটি দ্বিতীয় অ্যালার্টে দেখাবে। - সবার শেষে
timeout
, কেননা এটি একটি macrotask।
নিচের ছবিটিতে দেখুন (এক্সিকিশনের ক্রমটি হল উপর থেকে নিচে, অর্থাৎ প্রথমে script, তারপর microtasks, rendering ইত্যাদি):
ইভেন্ট হ্যান্ডেলিং বা রেন্ডারিং বা অন্যান্য macrotask এর আগে microtasks সম্পন্ন হবে।
কেন এটি গুরুত্বপূর্ন, এটি আমাদের নিশ্চয়তা প্রদান করে যে microtask চলাকালীন আমাদের অ্যাপ্লিকেশনের এনভায়রনম্যান্ট একই (no mouse coordinate changes, no new network data, ইত্যাদি)।
আমরা যদি কোন একটি ফাংশনকে রেন্ডার বা কোন একটি ইভেন্ট হ্যান্ডেল করার আগে অ্যাসিঙ্ক্রোনাসলি চালাতে চায় তাহলে queueMicrotask
দ্বারা করতে পারি।
এখানে উপরের "counting progress bar" কে আবার ইমপ্লিমেন্ট করলাম, তবে এখানে setTimeout
এর বদলে queueMicrotask
ব্যবহার করছি। যার ফলে রেন্ডারকৃত মানটি দেখব সবার শেষে। সিঙ্ক্রোনাস কোডের মত:
<div id="progress"></div>
<script>
let i = 0;
function count() {
// do a piece of the heavy job (*)
do {
i++;
progress.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e6) {
*!*
queueMicrotask(count);
*/!*
}
}
count();
</script>
ইভেন্ট লুপ সম্পর্কে আরো বিস্তারিত(specification):
- macrotask কিউতে থাকা টাস্কগুলো থেকে সবার প্রথমে আসা টাস্ককে রান করে টাস্কটি DEQUE হবে (যেমন "script")।
- সকল microtasks কে এক্সিকিউট করবে:
- যদি microtask কিউটি খালি না হয়:
- সবার প্রথমের টাস্কটিকে এক্সিকিউট এর পর দ্বিতীয়টি এভাবে সব টাস্ক এক্সিকিউট করে।
- যদি microtask কিউটি খালি না হয়:
- কোন পরিবর্তন হলে রেন্ডার করবে।
- যদি macrotask কিউ খালি হয় তাহলে আরেকটি macrotask সংগঠিত হওয়ার জন্য অপেক্ষা করবে।
- এর পর আবার ধাপ 1 এ যাবে।
একটি নতুন macrotask শিডিউলের জন্য:
setTimeout(f)
এর দ্বিতীয় আর্গুমেন্ট0
ms সেট করি।
যার সাহায্যে কোন একটি দীর্ঘ টাস্ককে ছোট ছোট টাস্কে বিভক্ত করতে পারি, এবং টাস্কটি চলাকালীন আমাদের ব্রাউজার হ্যাং হবে না।
এছাড়াও কোন একটি ইভেন্ট হ্যান্ডেলার সম্পূর্ন হওয়ার পর অন্য কোন টাস্ক শিডিউলড করতে ব্যবহার করতে পারি।
একটি নতুন microtask শিডিউলের জন্য:
queueMicrotask(f)
কল করুন।- এছাড়াও প্রমিস হ্যান্ডেলারগুলো microtask queue শিডিউল হয়।
microtask চলাকালীন আমাদের অ্যাপ্লিকেশনের এনভায়রনম্যান্টের কোন পরিবর্তন হয় না, এরা একটার পর একটা রান হয়।
সুতরাং একই এনভায়রনম্যান্ট স্টেটের জন্য কোন ফাংশনকে অ্যাসিঙ্ক্রোনাসলি ব্যবহার করতে আমরা queueMicrotask
ব্যবহার করতে পারি।
কোন জটিল গণনার জন্য আমাদের ইভেন্ট লুপকে ব্লক করে রাখা উচিত নয়, এক্ষেত্রে আমরা ব্যবহার করতে পারি [Web Workers](https://html.spec.whatwg.org/multipage/workers.html)।
এর সাহায্যে আমরা কোডকে প্যারালাল থ্রেডে রান করতে পারি।
মেইন প্রসেসের সাথে ওয়েব ওয়ার্কার ডাটা আদান প্রদান করতে পারে, তবে এর নিজস্ব ভ্যারিয়েবল এবং ইভেন্ট লুপ আছে।
এটির DOM এ কোন অ্যাক্সেস নেই, সুতরাং জটিল গণনা একাধিক কোরে একইসাথে চালাতে Web Worker ব্যবহার করা হয়।