Skip to content

setTimeout(fn, 0) সাথে সাথে চলে না কেন

ম্যাক্রোটাস্ক, মাইক্রোটাস্ক আর রেন্ডারিং — ইভেন্ট লুপের ধাপগুলো জানলে অ্যাসিঙ্ক কোডের ক্রম আর অনুমান করতে হয় না।

নাফিস রহমান

নাফিস রহমান

ডিজাইন সিস্টেম লিড

৭ মিনিটের পড়া
শেয়ার

এই কোডটার আউটপুট কী হবে?

JavaScript
console.log('১')

setTimeout(() => console.log('২'), 0)

Promise.resolve().then(() => console.log('৩'))

console.log('৪')

উত্তর ১ ৪ ৩ ২। setTimeout-এ ০ মিলিসেকেন্ড দেওয়া সত্ত্বেও সেটা সবার শেষে চলে, এমনকি এমন একটা প্রমিসের পরেও যেটা তার পরে লেখা হয়েছে।

"০ মানে এখনই নয়" — এইটুকু মনে রাখলে কাজ চলে যায়, কিন্তু কেন নয় সেটা জানলে আর মুখস্থ করতে হয় না।

কল স্ট্যাক আগে শেষ হয়

জাভাস্ক্রিপ্ট সিঙ্গল-থ্রেডেড। যেকোনো মুহূর্তে একটাই জিনিস চলে, আর সেটা কল স্ট্যাকের উপরের ফাংশনটা।

ইভেন্ট লুপ কোনো কিছু তুলে নেওয়ার আগে স্ট্যাক পুরো খালি হওয়া লাগে। তাই উপরের কোডে console.log('১') আর console.log('৪') — যেগুলো সিঙ্ক্রোনাস — আগে চলবেই। বাকি সবকিছু সারিতে অপেক্ষা করে।

এখান থেকেই সবচেয়ে সাধারণ বাগটা আসে:

JavaScript
// ব্রাউজার এই পুরো লুপটা শেষ না হওয়া পর্যন্ত জমে থাকে
for (let i = 0; i < 1e9; i++) {}

স্ট্যাক খালি না হলে ইভেন্ট লুপ কিছুই করতে পারে না — ক্লিক হ্যান্ডলার চলবে না, রেন্ডারও হবে না। "পেজ হ্যাং করে আছে" মানে বেশিরভাগ সময় "স্ট্যাক খালি হচ্ছে না"।

দুইটা আলাদা সারি

স্ট্যাক খালি হলে ইভেন্ট লুপ যা দেখে, তা একটা সারি নয় — দুইটা, আর তাদের অগ্রাধিকার আলাদা।

মাইক্রোটাস্ক সারি

  • Promise.then / .catch / .finally
  • await-এর পরের অংশ
  • queueMicrotask
  • MutationObserver

ম্যাক্রোটাস্ক সারি (স্পেকে "টাস্ক")

  • setTimeout, setInterval
  • DOM ইভেন্ট (click, input)
  • নেটওয়ার্ক কলব্যাক
  • requestAnimationFrame (আলাদা ধাপে, রেন্ডারের আগে)

নিয়মটা এক লাইনে:

প্রতিটা ম্যাক্রোটাস্কের পর, পুরো মাইক্রোটাস্ক সারি খালি করা হয় — তারপর পরের ম্যাক্রোটাস্ক।

তাই ৩ (প্রমিস, মাইক্রোটাস্ক) ২-এর (টাইমআউট, ম্যাক্রোটাস্ক) আগে চলে। ০ মিলিসেকেন্ড লেখা থাকলেও সেটা অন্য সারিতে, আর ওই সারির পালা পরে।

"পুরো" শব্দটা গুরুত্বপূর্ণ

মাইক্রোটাস্ক সারি খালি করার সময় নতুন মাইক্রোটাস্ক যোগ হলে সেগুলোও একই দফায় চলে। এটা সুবিধা, আবার ফাঁদও:

JavaScript
function loop() {
  Promise.resolve().then(loop)
}
loop() // পেজ জমে যায়

এখানে কোনো ইনফিনিট while নেই, তবু ব্রাউজার আর কিছু করতে পারে না — সারিটা কখনো খালি হয় না, তাই লুপ কখনো পরের ধাপে যায় না।

একই কোড setTimeout দিয়ে লিখলে পেজ সচল থাকে:

JavaScript
function loop() {
  setTimeout(loop, 0)
}
loop() // চলতেই থাকে, কিন্তু পেজ ব্যবহারযোগ্য

কারণ প্রতিটা ম্যাক্রোটাস্কের মাঝে ব্রাউজার রেন্ডার করার সুযোগ পায়।

await আসলে কী

await নতুন কিছু নয় — এটা .then-এর সিনট্যাক্স।

JavaScript
async function run() {
  console.log('ক')
  await null // ← এখানেই ফাংশনটা থামে
  console.log('খ') // ← এই অংশটা মাইক্রোটাস্ক
}

run()
console.log('গ')

আউটপুট ক গ খ। await null — যেখানে অপেক্ষা করার মতো কিছুই নেই — তবু ফাংশনটা থামে, বাকিটা মাইক্রোটাস্ক হিসেবে সারিতে যায়, আর গ আগে চলে।

তাই await-এর পরের প্রতিটা লাইন কার্যত একটা কলব্যাকের ভেতরে। সিরিয়ালি লেখা যায় বলে এটা ভুলে যাওয়া সহজ।

রেন্ডারিং কোথায়

ব্রাউজার সাধারণত প্রতি ফ্রেমে একবার রেন্ডার করে — মোটামুটি ১৬.৭ms পরপর, ৬০Hz-এ। ধাপটা এরকম:

text
ম্যাক্রোটাস্ক → মাইক্রোটাস্ক (সব) → rAF কলব্যাক → স্টাইল → লেআউট → পেইন্ট

দুইটা ব্যবহারিক ফল:

এক — একই টিকে DOM বদলে আবার পড়লে ব্রাউজার জোর করে লেআউট হিসাব করে (layout thrashing):

JavaScript
// খারাপ — প্রতি ধাপে জোর করে reflow
for (const el of items) {
  el.style.height = el.offsetHeight + 10 + 'px'
}

// ভালো — আগে সব পড়ুন, তারপর সব লিখুন
const heights = items.map((el) => el.offsetHeight)
items.forEach((el, i) => (el.style.height = heights[i] + 10 + 'px'))

দুই — অ্যানিমেশন setTimeout দিয়ে নয়, requestAnimationFrame দিয়ে। rAF রেন্ডার ধাপের ঠিক আগে চলে, তাই ফ্রেম মিস হয় না।

০ আসলে ০ নয়

শেষ একটা খুঁটিনাটি: HTML স্পেক অনুযায়ী নেস্টেড setTimeout পাঁচ স্তরের পর সর্বনিম্ন ৪ মিলিসেকেন্ড ক্ল্যাম্প হয়। ব্যাকগ্রাউন্ড ট্যাবে ব্রাউজার এটা আরও বাড়িয়ে ১০০০ms-ও করে দিতে পারে।

অর্থাৎ setTimeout(fn, 0) মানে "যত দ্রুত সম্ভব" নয়, মানে "পরের টাস্ক হিসেবে, সুবিধামতো"। এখনই দরকার হলে queueMicrotask আছে — কিন্তু তাতে ব্রাউজার রেন্ডার করার সুযোগ পায় না, যেটা প্রায়ই আপনি চাইছিলেন।

  • #JavaScript
  • #ইভেন্ট লুপ
  • #অ্যাসিঙ্ক
শেয়ার

সম্পর্কিত লেখা

ওয়েব ডেভেলপমেন্ট৭ মিনিটের পড়া

কম্পোনেন্টের গায়ে margin দেবেন না

ফাঁকা জায়গার মালিক প্যারেন্ট, চাইল্ড নয়। একটা কম্পোনেন্ট নিজের বাইরের মার্জিন নিজে ঠিক করলে সেটা আর যেকোনো জায়গায় বসানো যায় না।

নাফিস রহমাননাফিস রহমান
ব্যবসা ও মনোবিজ্ঞান৮ মিনিটের পড়া

মানুষ কেন অচেনা কারো কাছ থেকে কিনতে দ্বিধা করে — Trust Signal যেভাবে সেল বাড়ায়

একই ল্যান্ডিং পেজ, একই দাম, একই প্রোডাক্ট — শুধু নিচে তিনটা রিভিউ যোগ করলেই কনভার্শন বদলে যায়। এটা কাকতালীয় না, মানুষ সিদ্ধান্ত নেওয়ার সময় অন্যদের আচরণ দেখেই ভরসা করে।

সাদিয়া ইসলামসাদিয়া ইসলাম
ব্যবসা ও মনোবিজ্ঞান৮ মিনিটের পড়া

প্রোডাক্ট বেচবেন না, সমস্যার সমাধান বেচুন — Customer আসলে কী 'hire' করছে

একজন কাস্টমার মিল্কশেক কেনে না — সকালের বোরিং গাড়ি-চালানোর সময়টাকে সহনীয় করার একটা উপায় কেনে। প্রোডাক্টের ফিচার লিস্ট না, এই "job"-টা বুঝলেই সেল বাড়ে।

সাদিয়া ইসলামসাদিয়া ইসলাম

মাসে একটা কাজের ইমেইল

ডিজাইন সিস্টেম প্যাটার্ন, ফ্রন্ট-এন্ড টেকনিক আর লার্ন প্ল্যাটফর্মের নোট। কোনো প্রচার নেই, অন্যের লিংকের সংকলনও নেই।

যেকোনো সময় আনসাবস্ক্রাইব করতে পারবেন। আপনার ঠিকানা কারো সাথে শেয়ার করি না।