تأخیر ۳۰۰ میلیثانیهای پنهان در React.lazy و Suspense
خلاصهٔ کاملتر
Tab Widgets یه افزونهٔ new tab با React 19 و Rspack هست که هر ویجتش با React.lazy تو یه chunk جدا لود میشه و داخل Suspense خودش قرار داره. تیم سازنده تو این مقاله میگه اولش فکر میکردن باندل خیلی بزرگه. یه re-export اضافه رو حذف کردن و حدود 100KB از کد شروع کم شد، ولی وقتی یه بیلد production رو تو Chromium headless پروفایل کردن دیدن همهٔ اسکریپتها تو حدود 100ms لود شدن و ویجتها تازه تو حدود 380ms ظاهر میشن. تو بیشتر این فاصله مرورگر عملاً بیکار بود.
به گفتهٔ نویسنده، دوتا رفتار React با هم این تأخیر رو میسازن. اول اینکه کامپوننت lazy همیشه دفعهٔ اول suspend میشه (یعنی رندرش متوقف میشه و fallback نشون داده میشه)، حتی اگه کدش از قبل لود شده باشه؛ چون import() رو تازه موقع اولین رندر صدا میزنه و جوابش با promise میاد. دوم اینکه React محتوای suspendشده رو حداکثر هر 300ms یه بار نمایش میده (FALLBACK_THROTTLE_MS). این عمداً برای جلوگیری از چشمک زدن صفحهست، ولی یعنی هر ویجت حداقل ۳۰۰ میلیثانیه معطل میشد. startTransition هم کمکی نمیکنه، چون boundaryای که تازه mount میشه باز هم fallback نشون میده.
راهحل اول این بود که قبل از رندر، component._init(component._payload) رو خودشون صدا بزنن تا import زودتر شروع بشه، و حداکثر 100ms صبر کنن. ویجتها تو headless از حدود ۳۸۰ به ۹۵ میلیثانیه رسیدن. ولی هر ویجتی که بیشتر از این سقف طول میکشید باز suspend میشد و حتی دیرتر از قبل نمایش داده میشد. چون _init و _payload فیلدهای خصوصی React هستن، یه تست واحد هم نوشتن که اگه آپدیت React این رفتار رو خراب کنه، تست fail بشه.
راهحل نهایی اینه که ویجتها اصلاً روی کد خودشون suspend نشن. یه hook به اسم useLazyReady چک میکنه کد لود شده یا نه؛ اگه نه، placeholder رو بهعنوان محتوای معمولی رندر میکنه و با یه state update عادی ویجت واقعی رو جایگزین میکنه:
const isReady = useLazyReady(WidgetComponent);
return isReady ? <WidgetComponent {...config} /> : <WidgetLoading />;چون دیگه چیزی suspend نمیشه، تأخیر ۳۰۰ میلیثانیه هم اعمال نمیشه. Suspense و error boundary همچنان دورش میمونن تا import ناموفق رو بگیرن. هزینهاش اینه که ویجتها یکییکی ظاهر میشن نه دستهجمعی، ولی چون هر قاب اندازهٔ ثابت داره، صفحه جابهجا نمیشه. برای کسی که نمیخواد به فیلدهای خصوصی دست بزنه، نویسنده یه نسخهٔ بدون React.lazy هم با یه Map از کامپوننتهای لودشده پیشنهاد میده.
تو اندازهگیری روی کروم ویندوز (میانگین ۴ اجرا)، اولین محتوای ویجت از 540ms به 298ms رسید و ۸۵ درصد محتوا از 540ms به 336ms. ولی زمان «همهٔ محتوا» تغییری نکرد (544ms)، چون ویجتهایی مثل Weather بعد از لود کد برای گرفتن داده دوباره suspend میشن و این بخش هنوز درست نشده. کوچیک کردن باندل اصلی هم هیچ تفاوت قابل اندازهگیریای ایجاد نکرد.
نکات کلیدی:
- کامپوننت React.lazy همیشه بار اول یه بار suspend میشه، حتی با کد از قبل لودشده.
- React محتوای suspendشده رو با throttle ثابت 300ms نمایش میده (FALLBACK_THROTTLE_MS).
- startTransition جلوی fallback یه boundary تازه mountشده رو تو بیلد production نمیگیره.
- رندر placeholder بهجای suspend، اولین ویجت رو حدود 242ms زودتر نشون داد.
- حذف re-export ماژالها حدود 100KB از کد شروع کم کرد ولی سرعت نمایش رو تغییر نداد.
- suspend روی fetch داده هنوز همون تأخیر رو برای Weather و Word of the Day داره.




