לולאת האירועים (event loop) של JavaScript שמניעה את דף האינטרנט שלך מתנהגת בצורה שונה מאוד מזו שמפעילה שרת Node.js, וחוסר ההתאמה הזה עלול להקפיא ממשק משתמש (UI) או לחנוק I/O אם לא נזהרים. הבנת הנקודות שבהן השתיים מתפצלות היא חיונית לכל מי שכותב קוד אסינכרוני (async) שרץ בשתי הסביבות.
למה ההבחנה הזו חשובה
לולאת האירועים אינה מוגדרת על ידי מפרט ECMAScript; היא קיימת בתוך ה-host. דפדפנים חייבים לשמור על דף תגובתי בזמן רינדור פריימים, בעוד ש-Node.js בנויה סביב I/O לא חוסם. ערבוב של תבניות עבודה שמתאימות ל-host אחד עם השני יכול ליצור באגים שקשה לשחזר: שרשרת ארוכה של promises עלולה לעכב את ה-repaint של הדפדפן, בעוד שלולאת process.nextTick לא מבוקרת עלולה למנוע מ-Node להגיע לשלבי ה-I/O שלה.
לולאת ה-turn-based של הדפדפן
בדפדפן, הלולאה מריצה מחזור בודד המשלב ביצוע משימות (task execution), ריקון מיקרו-משימות (microtask draining) ורינדור:
- הרצת macrotask אחת (מטפל לחיצה,
setTimeoutוכו'). - ריקון של כל המיקרו-משימות (promises,
queueMicrotask). - אם הגיע זמן של פריים, ביצוע paint ו-composite כדי להגיע ל-60 fps היעד.
- חזרה על התהליך.
שני ממשקי API מספקים למפתחים נקודות חיבור (hooks) מפורשות למחזור זה:
requestAnimationFrame– נקרא ממש לפני שהדפדפן מבצע paint. זה המקום הנכון לעבודות אנימציה מכיוון שה-callback רץ אחרי המיקרו-משימות הנוכחיות אך לפני הפריים הבא.requestIdleCallback– מופעל כאשר לדפדפן אין עבודה בעדיפות גבוהה. זה שימושי למשימות בעלות השפעה נמוכה כמו אנליטיקה או טעינה מוקדמת של נתונים.
מלכודת: רעב מיקרו-משימות (microtask starvation)
מכיוון שהדפדפן מרוקן את תור המיקרו-משימות לפני שהוא מבצע רינדור, שרשרת ארוכה של promises עלולה למנוע מה-UI לבצע paint לעולם. ה-call stack אינו חסום; הדף פשוט לעולם לא מגיע לשלב הרינדור, מה שמרגיש למשתמש כמו הקפאה.
הלולאה של Node המונעת על ידי libuv
Node.js מפקידה את הלולאה שלה ל-libuv, ספריית C שמחלקת את העבודה לשלבים נפרדים, שלכל אחד מהם תור משלו:
- Timers – callbacks מ-
setTimeoutו-setInterval. - Pending callbacks – callbacks של I/O שנדחו וכבר הושלמו ברמת מערכת ההפעלה.
- Poll – שליפת אירועי I/O חדשים (קריאת קבצים, נתוני רשת).
- Check – הרצת callbacks של
setImmediate. - Close callbacks – מופעל כאשר socket או handle נסגרים.
שני מבנים נמצאים מחוץ לסדר השלבים הזה:
process.nextTick– רץ לפני תור המיקרו-משימות, מיד לאחר סיום הפעולה הנוכחית.
מלכודת: רעב I/O (I/O starvation)
אם פונקציה מתזמנת שוב ושוב process.nextTick מבלי לתת מקום (yielding), Node לעולם לא תתקדם מעבר לשלב ה-"next-tick". בקשות רשת, קריאות קבצים וטיימרים יישארו במצב המתנה, מה שיגרום לקפיצות בשיהוי (latency) בצד השרת או לתקיעה מוחלטת.
setImmediate לעומת setTimeout בפועל
שניהם מתזמנים callbacks לאיטרציה הבאה, אך הסדר היחסי שלהם תלוי במקום שבו הם נקראים:
- קוד ברמת ה-Top-level – הסדר אינו מובטח; הוא תלוי במהירות שבה התהליך עולה.
- בתוך I/O callback – הסדר הוא דטרמיניסטי:
setImmediateירוץ לפניsetTimeout(fn, 0). לאחר סיום שלב ה-Poll, libuv עוברת לשלב ה-Check (שבו נמצאsetImmediate) לפני שהיא תיכנס מחדש לשלב ה-Timers עבור timeout עם השהיה אפסית.
עדינות זו חשובה כשאתם מסתמכים על רצף מדויק, כמו ניקוי משאב מיד לאחר סיום קריאה.
הבדלים עיקריים במבט חטוף
- מטרה: דפדפנים נותנים עדיפות לעדכונים ויזואליים; Node נותנת עדיפות למוכנות I/O.
- נקודת חיבור לרינדור:
requestAnimationFrame(בדפדפן בלבד). - נקודת חיבור ספציפית לשלב:
setImmediate(ב-Node בלבד, מופעל בשלב ה-Check). - תור בעדיפות גבוהה:
process.nextTick(ב-Node בלבד, רץ לפני מיקרו-משימות). - סיכון לרעב (starvation): שרשראות promises ארוכות בדפדפנים;
process.nextTickבלתי מוגבל ב-Node.
מה כדאי לבדוק בהמשך
אם אתם מתחזקים בסיס קוד שרץ בשתי הסביבות (למשל, ספריות איזומורפיות), בדקו כל מקום שבו אתם:
- מחברים הרבה promises מבלי לתת מקום (yielding) ללולאת האירועים. הכניסו
await new Promise(r => setTimeout(r, 0))או השתמשו ב-requestIdleCallbackבדפדפן כדי לתת לרינדורר הזדמנות. - משתמשים ב-
process.nextTickלעבודה שניתן לדחות. העדיפוsetImmediateאו promise רגיל כשאתם לא זקוקים לדחיפות של "next-tick". - מניחים ש-
setTimeout(fn, 0)ו-setImmediateהם חליפיים. בדקו את סדר ההרצה בתוך I/O callbacks אם הרצף חשוב לכם.
שורה תחתונה
ה-event loop הוא מתזמן (scheduler) ספציפי למארח (host), ולא תכונה אוניברסלית של JavaScript. דפדפנים משלבים את ה-rendering לתוך הלופ; Node מבודד את ה-I/O לשלבים של libuv. שימוש שגוי במנגנוני עדיפות — microtasks בדפדפן, process.nextTick ב-Node — עלול לגרום להרעבה (starvation) של החלק במערכת שכל סביבה נועדה לשרת. התאימו את תבניות ה-async שלכם למודל הלופ של המארח, ותימנעו הן מדפים קפואים והן משרתים חסומים.
