main thread گرونترین منبع مرورگره
خلاصهٔ کاملتر
نویسنده از یه حس آشنا شروع میکنه: اسکرول جایی گیر میکنه، دکمه یهکم دیر جواب میده، حروفی که توی سرچباکس تایپ میکنی نیمضرب عقبتر ظاهر میشن. واکنش معمول ما اینه که دنبال الگوریتم کند بگردیم، ولی به گفتهٔ نویسنده اغلب سرعت کد مشکل نیست: کد تو فقط اتفاقی همون کدیه که main thread رو گرفته. مرورگر ترد کم نداره، ولی تقریباً هر چیزی که ما از کد بهش دست میزنیم روی همون یه ترد جمع شده.
سهم main thread فقط اجرای جاوااسکریپت نیست، بخش جلویی کشیدن صفحه هم هست: کالبکهای requestAnimationFrame، محاسبهٔ استایل، layout (یعنی حساب کردن جا و اندازهٔ هر المان) و paint. فقط مرحلهٔ آخر یعنی compositing میره روی ترد جدا. روی نمایشگر ۶۰ هرتز هر فریم حدود ۱۶.۶ میلیثانیه وقت داره و بعد از کم کردن هزینهٔ خود مرورگر عملاً حدود ۱۰ میلیثانیه دستته؛ روی ۱۲۰ هرتز نصف همین.
مسئله اینجاست که این دو نوع کار توی یه صف واحد میایستن. تا یه تسک تموم نشه هیچچیز دیگهای اجرا نمیشه، پس یه تابع ۲۰۰ میلیثانیهای یعنی ۲۰۰ میلیثانیه صفحهٔ یخزده و کلیک بیجواب. به این تسکهای طولانی میگن long task، و معیارهایی مثل INP و TBT در عمل دارن همین «چقدر main thread بلوکه بود» رو اندازه میگیرن.
اولین و پایهایترین حرکت splitting ـه: کار طولانی رو تیکهتیکه کن و بین تیکهها یه لحظه ترد رو پس بده تا مرورگر فرصت کنه فریم بکشه و ورودی کاربر رو پردازش کنه. مثال نویسنده پنل چت یه لایو استریمه که موقع شلوغی دهها پیام با هم میرسه. حواست باشه که yield کردن کار رو سریعتر نمیکنه، حتی چند میلیثانیه سربار اضافه میکنه؛ فقط شکاف میسازه و همون شکافها به کاربر حس روانی میده.
// پیامها دستهجمعی میرسن؛ بعد از هر ۲۰ تا، main thread رو پس میدیم
async function renderChats(chats) {
let count = 0;
for (const chat of chats) {
appendChatNode(chat);
if (++count % 20 === 0) {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}اگه انیمیشنی در جریانه، بهتره بهجای شمارش تعداد، بر اساس زمان ببری: با performance.now() چک کن از شروع فریم چقدر گذشته و با requestAnimationFrame درست قبل از فریم بعدی برگرد. نویسنده حدود ۵ میلیثانیه از اون بودجهٔ ۱۰ میلیثانیهای رو عدد معقولی میدونه. دو تا تله هم هست: ریز کردن بیش از حد تیکهها سربارش از خود کار بیشتر میشه، و setTimeout حداقل تأخیر داره، برای همین React توی زمانبندش از MessageChannel استفاده میکنه و استاندارد تازهٔ scheduler.yield() هم برای همین اومده.
حرکت دوم batching ـه که برعکس به نظر میرسه ولی هدفش همونه: رسوندن تسکها به اندازهٔ درست. splitting سراغ کارهای خیلی طولانی میره و batching سراغ کارهایی که خیلی زیاد تکرار میشن. ایونتهایی مثل scroll و resize و input بهترین هدفن، با debounce و throttle. برای آپدیتهای تصویری هم requestAnimationFrame جواب میده، چون صفحه بیشتر از یه بار در فریم کشیده نمیشه. حرکت سوم prioritizing ـه: کارها رو توی یه صف بذار و کار فوری رو بکش جلوی صف.
نکات کلیدی:
- بودجهٔ عملی هر فریم روی ۶۰ هرتز حدود ۱۰ میلیثانیهست، نه کل ۱۶.۶ میلیثانیه
- از کل پایپلاین رندر فقط compositing روی ترد جدا اجرا میشه، بقیهاش روی main thread ـه
- setTimeout و scheduler.yield() بدون توجه به چرخهٔ رندر برمیگردن، ولی requestAnimationFrame درست قبل از فریم بعدی
- کار اتمیک مثل JSON.parse روی پاسخ چندمگابایتی اصلاً تقسیمشدنی نیست و باید کلاً از main thread بیرون بره




