האם אי פעם בניתם tooltip שקופץ מהפינה השמאלית העליונה למקום הנכון שלו? או modal שמהבהב בגודל הלא נכון לפני שהוא מתייצב במקומו? הבעיה הזו של שבריר שנייה היא layout flicker. זה קורה כש-React קורא את ה-DOM, מחשב תיקון, ומעדכן את ה-state, אבל הדפדפן כבר התחיל לצייר פיקסלים על המסך. הפתרון הרגיל הוא להחליף את useEffect ב-useLayoutEffect. ההחלפה עובדת, אך רק אם מבינים בדיוק מתי כל hook מופעל בתוך ה-pipeline של הדפדפן.
ה-Browser Pipeline: Render, Commit, Paint
React מעדכנת רכיב בשלושה שלבים נפרדים. בשלב ה-render, React בונה — או בונה מחדש — את ה-Virtual DOM ומחשבת את ה-diff. עדיין אין שינויים אמיתיים בפיקסלים; זהו חישוב טהור שמתבצע בזיכרון. לאחר מכן מגיע שלב ה-commit, שבו React מחילה את השינויים הללו על ה-DOM nodes האמיתיים. ה-Styles מתעדכנים, nodes מתווספים או מוסרים, וטקסטים משתנים.
ואז הדפדפן נכנס לתמונה. בשלב ה-paint, מנוע הרינדור של הדפדפן מחשב את גיאומטריית הפריסה (layout geometry) ומצייר פיקסלים על המסך. הרצף הזה הוא קשיח. הדפדפן חייב להשלים את ה-layout לפני שהוא יכול לבצע paint, והוא חייב לסיים את ה-paint לפני שהמשתמש רואה משהו חדש. הפער בין commit ל-paint נמדד במילישניות, אך הוא אמיתי, וזהו החלון שבו useEffect ו-useLayoutEffect נפרדים.
למה useEffect גורם ל-Flicker
useEffect רץ באופן אסינכרוני, ומתוזמן לפעול לאחר שהדפדפן כבר ביצע paint למסך. ה-DOM מעודכן, הפיקסלים מצוירים, ואז React חוזרת לפעולה כדי להריץ את ה-effect שלכם.
דמיינו שאתם מרנדרים תפריט dropdown מתחת לכפתור. בתוך useEffect, אתם קוראים ל-buttonRef.current.getBoundingClientRect(), מחשבים את קואורדינטות ה-top וה-left הנכונות, ושומרים אותן ב-state. מכיוון ש-useEffect רץ אחרי ה-paint, הדפדפן כבר צייר את ה-dropdown במיקום ברירת המחדל שלו, אולי ב-top: 0, left: 0. רק לאחר ה-paint הזה ה-effect שלכם מעדכן את ה-state. React מבצעת commit לקואורדינטות המתוקנות, והדפדפן מבצע paint שוב. המשתמש רואה שני פריימים: את המיקום הלא נכון, ואז את הנכון. ה"קפיצה" הוויזואלית הזו היא ה-flicker שכולם מנסים להימנע ממנו.
עבור שליפת נתונים (data fetching), קריאות API, מעקב אנליטיקה או הגדרת event listeners, העיכוב הזה אינו משנה. לא אכפת למשתמש אם analytics beacon נורה כמה מילישניות אחרי ה-paint. למעשה, דחיית עבודה שאינה ויזואלית עד אחרי ה-paint שומרת על הרינדור הראשוני בתגובתיות (responsive). אך עבור תיקוני פריסה (layout-dependent corrections), useEffect פשוט מאוחר מדי.
איך useLayoutEffect חוסם את ה-Paint
useLayoutEffect רץ באופן סינכרוני, מיד לאחר ש-React משנה (mutates) את ה-DOM אך לפני שיש לדפדפן הזדמנות לחשב את ה-layout או לצייר פיקסלים. הוא חוסם לחלוטין את ה-paint pipeline.
אם תבצעו את אותה מדידת ה-dropdown בתוך useLayoutEffect, הרצף משתנה. React מבצעת commit לעדכון ה-DOM הראשוני, מריצה את ה-layout effect שלכם, ועדכון ה-state שלכם מפעיל re-render סינכרוני. React מבצעת commit לקואורדינטות המתוקנות, ורק אז הדפדפן מבצע paint. המשתמש רואה פריים אחד, שכבר תקין.
ההתנהגות החוסמת הזו היא גם התכונה וגם הסיכון. מכיוון ש-useLayoutEffect מונע מהדפדפן לבצע paint עד שהוא מסיים, כל חישוב כבד בתוכו מקפיא את ה-UI. אפילו כמה עשרות מילישניות של paint חסום מרגישות כמו jank (תקיעות) למשתמש. זו הסיבה שהתיעוד של React אומר לכם במפורש להתחיל עם useEffect ורק לשדרג ל-useLayoutEffect כאשר אתם אכן מבחינים ב-flicker שאינכם יכולים לסבול.
מתי להשתמש בכל hook
רוב הלוגיקה שלכם שייכת ל-useEffect. השתמשו בו עבור:
- שליפת נתונים מ-API
- הגדרת subscriptions או event listeners
- שליחת אירועי אנליטיקה
- כל side effect שאינו קורא או משנה layout באופן מיידי
שמרו את useLayoutEffect עבור פעולות שחייבות לקרוא את ה-DOM ולכתוב חזרה לפני שהמשתמש רואה את הפריים:
- מדידת מימדי אלמנטים, כגון רוחב (width), גובה (height) או מיקום גלילה (scroll position)
- חישוב קואורדינטות עבור tooltips, popovers או תפריטי הקשר (context menus)
- מניעת שינויי פריסה (layout shifts) נראים לעין כאשר המיקום הוויזואלי תלוי בגיאומטריה המרונדרת
אם אינכם בטוחים במה לבחור, בחרו בברירת מחדל ב-useEffect. עברו ל-useLayoutEffect רק כאשר אתם מבחינים בחוסר יציבות ויזואלית. הכלל הזה לבדו ישמור על רוב יישומי ה-React שלכם רצים בצורה חלקה.
מלכודת ה-Server-Side Rendering (Gotcha)
אם תשתמשו ב-Next.js, Remix, או בכל framework שמבצע rendering ל-React בשרת, תקבלו אזהרה עם useLayoutEffect. מכיוון שלשרת אין DOM, ל-hook אין מה למדוד. React מזהירה אתכם שהיא ציפתה לסביבת דפדפן ולא מצאה כזו. במהלך ה-hydration, חוסר התאמה זה עלול לגרום גם לבאגים עדינים, מכיוון שה-markup שרונדר בשרת וה-render הראשון המיועד בצד הלקוח עשויים להיות שונים.
הפתרון הסטנדרטי הוא isomorphic hook שבוחר את ה-effect הנכון בהתאם לסביבה:
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
השתמשו ב-wrapper הזה בכל component שחייב למדוד DOM nodes אך עלול להתבצע במהלך server rendering. הוא משתיק את האזהרה ושומר על עקביות ב-output של השרת.
ביצועים ושיטות עבודה מומלצות
מכיוון ש-useLayoutEffect חוסם את ה-painting, שמרו על גוף ה-hook קל ככל האפשר. קראו את ערך ה-layout, חשבו את התיקון, וכתבו אותו בחזרה. אל תבצעו fetch לנתונים, אל תנתחו (parse) אובייקטים גדולים, ואל תריצו אלגוריתמים כבדים בתוכו. קוד כבד כאן יעכב את ה-main thread ויגרום לממשק להרגיש קפוא.
כשאתם מודדים אלמנטים, השתמשו ב-React refs במקום ב-document.getElementById. Refs קשורים ל-component instance שלכם, שורדים re-renders ללא טריקים של שאילתות, ועובדים בצורה אמינה עם portals או conditional rendering. חיפושי ID גלובליים שוברים את ה-encapsulation של ה-component ויכולים להחזיר null בדיוק ברגע שבו אתם זקוקים להם.
useEffect הוא ברירת המחדל הנכונה כמעט לכל side effect. הוא מאפשר לדפדפן לבצע paint ללא הפרעה ומטפל בנתונים, אירועים (events) וסנכרון חיצוני בצורה נקייה. useLayoutEffect הוא כלי ייעודי לבעיה ספציפית: קריאת layout וכתיבה בחזרה לפני ה-paint. אם תתמכרו בהבדלי התזמון ביניהם, תפסיקו לרדוף אחרי flickers ותתחילו למנוע אותם.
התובנה האמיתית: התחילו עם useEffect לכל דבר. ברגע שאתם רואים tooltip או modal מהבהבים למקום הלא נכון לפני שהם מתקנים את עצמם, זה הסימן שלכם. עברו ל-useLayoutEffect, מדדו את ה-DOM, התאימו את ה-layout שלכם, ותנו לדפדפן לבצע paint פעם אחת — בצורה נכונה.
