Long Animation Frames API (LoAF): המדריך המלא לדיבאג INP ב-2026

מדריך מעשי ל-Long Animation Frames API (LoAF): איך לאתר ולדבג צווארי בקבוק של INP בפרודקשן עם שיוך סקריפטים, web-vitals.js v4 ו-DevTools של Chrome. כולל קוד מוכן להעתקה.

אם ניסיתם פעם לאתר את הסיבה לציון INP גרוע בפרודקשן, אתם בוודאי מכירים את התסכול הזה. ה-Performance API הקלאסי של Long Tasks אומר לכם ש"משהו ארוך קרה במיין-תרד", אבל לא איזה סקריפט, לא באיזו שורה, ובטח שלא כמה זמן בזבזה כל פונקציה. זה כמו שהאזעקה במכונית צופרת אבל אף אחד לא טורח לרשום מי הפעיל אותה.

ב-Chrome 123 גוגל סוף סוף סגרה את הפער הזה עם Long Animation Frames API (או בקיצור LoAF) — API חדש שמספק שיוך מלא של סקריפטים, סטיילים ולייאאוט לכל פריים שלקח יותר מ-50ms. במדריך הזה תלמדו בדיוק איך להשתמש ב-LoAF כדי לדבג INP בפרודקשן, איך לחבר אותו ל-web-vitals.js v4, ואיזו תבנית קוד באמת עוזרת לאתר צווארי בקבוק ב-2026.

למה ה-Long Tasks API פשוט לא הספיק

ה-Long Tasks API איתנו כבר כמה שנים והוא מדווח על כל משימה במיין-תרד שלקחה יותר מ-50ms. הבעיה? משימה ארוכה היא רק חלק קטן מהתמונה.

אינטראקציה איטית של משתמש (זו שמדדנו ב-INP) מורכבת משלושה שלבים נפרדים: Input Delay, Processing Duration ו-Presentation Delay. הרבה פעמים הזמן שאיבד את הפריים נמצא דווקא בשלב ה-style/layout או ב-rendering, ולא ב-JavaScript "טהור". ובכלל, Long Tasks אף פעם לא ידע להגיד לכם איזה סקריפט אחראי — רק שמשהו רץ. תודה רבה.

LoAF נבנה כדי לפתור בדיוק את זה. במקום להסתכל על משימות בודדות, הוא מסתכל על פריים אנימציה שלם — מהרגע שהדפדפן התחיל לעבד את הפריים ועד שהוא הציג אותו על המסך. אם הפריים הזה לקח יותר מ-50ms, הדפדפן יוצר רשומת long-animation-frame עם פירוט מלא של כל מה שקרה בו. כל הסקריפטים שרצו, כמה זמן לקח style ו-layout כפוי, ואיזה אירוע משתמש (אם בכלל) הפעיל את הפריים מלכתחילה.

אנטומיה של רשומת LoAF

הממשק PerformanceLongAnimationFrameTiming חושף את כל החלקים של פריים ארוך:

  • startTime — מתי התחיל הפריים.
  • duration — משך הפריים הכולל (לפחות 50ms כדי בכלל להופיע).
  • renderStart — מתי התחיל שלב ה-rendering, אחרי שכל ה-tasks סיימו.
  • styleAndLayoutStart — מתי הדפדפן התחיל לחשב סטיילים ולייאאוט.
  • blockingDuration — סכום הזמן שבו המיין-תרד היה חסום בפועל מעבר ל-50ms. זה השדה הכי חשוב לדיבאג INP, ואני לא מגזים.
  • firstUIEventTimestamp — אם משתמש לחץ או הקליד במהלך הפריים, החותמת תהיה כאן (אחרת 0).
  • scripts — מערך של PerformanceScriptTiming, רשומה אחת לכל סקריפט שרץ יותר מ-5ms בתוך הפריים.

קצת על blockingDuration: זה ה"גביע הקדוש" לדיבאג INP מעשי. בניגוד ל-duration, הוא לא סופר את הזמן שבו הדפדפן עשה דברים "חוקיים" כמו רינדור — הוא מודד כמה זמן הקוד שלכם באמת חסם את ה-Event Loop. זה המספר שאתם רוצים לראות יורד.

שיוך סקריפטים: PerformanceScriptTiming

וזה החלק שמשנה את המשחק. כל רשומת PerformanceScriptTiming כוללת:

  • sourceURL — כתובת ה-URL של הסקריפט (אם זמינה, ואחר כך נדבר על "אם").
  • sourceFunctionName — שם הפונקציה שהיוותה את נקודת הכניסה.
  • sourceCharPosition — מיקום התו בקובץ המקור.
  • invokerType — סוג ההפעלה: למשל event-listener, user-callback, promise-resolve.
  • invoker — תיאור קונקרטי, למשל BUTTON#submit.onclick אם זה event handler על אלמנט.
  • executionStart ו-duration — מתי הסקריפט התחיל ובמשך כמה זמן רץ.
  • forcedStyleAndLayoutDuration — זמן שבו הסקריפט גרם ל-style/layout כפוי (סימן מובהק לרידאוט/רייטה לא יעיל של ה-DOM).
  • pauseDuration — זמן שבזבז על קריאות חוסמות כמו alert() או XHR סינכרוני. אם אתם עדיין משתמשים ב-XHR סינכרוני ב-2026, יש לנו על מה לדבר.

שימו לב לדבר חשוב אחד: השיוך עובד רק לסקריפטים במיין-תרד של הדף, כולל iframes באותה מקור. Web Workers, Service Workers, iframes חוצי-מקור והרחבות דפדפן לא מקבלים שיוך מפורט — גם אם הם בהחלט השפיעו על משך הפריים. תזכרו את זה כשתפענחו רשומות "מסתוריות" בהמשך.

תבנית בסיסית: Observer ל-LoAF

הצעד הראשון הוא להאזין לרשומות. הקוד הבא מתעד כל פריים ארוך, ולכל סקריפט שרץ בו מדפיס את ה-URL, שם הפונקציה ומשך הריצה:

if (PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')) {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      console.group(`LoAF: ${entry.duration.toFixed(0)}ms (blocking: ${entry.blockingDuration}ms)`);
      console.log('renderStart:', entry.renderStart);
      console.log('firstUIEventTimestamp:', entry.firstUIEventTimestamp);

      for (const script of entry.scripts) {
        console.log({
          url: script.sourceURL,
          fn: script.sourceFunctionName,
          invoker: script.invoker,
          duration: script.duration,
          forcedLayout: script.forcedStyleAndLayoutDuration,
          pause: script.pauseDuration,
        });
      }
      console.groupEnd();
    }
  });

  observer.observe({ type: 'long-animation-frame', buffered: true });
}

השימוש ב-buffered: true חשוב מאוד — הוא מבטיח שתקבלו גם רשומות שנוצרו לפני שה-Observer נרשם, למשל בזמן טעינה ראשונית של הדף. בלעדיו תפספסו את החלק הסוער ביותר.

זיהוי LoAF שקשור לאינטראקציה

בשביל דיבאג INP אנחנו רוצים רק את הפריימים שהיו בהם אינטראקציות משתמש. בשביל זה השדה firstUIEventTimestamp קיים — אם הוא גדול מ-0, היה אירוע UI במהלך הפריים:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.firstUIEventTimestamp === 0) continue;
    if (entry.blockingDuration < 100) continue; // התמקדו בגרועים

    const culprit = entry.scripts
      .slice()
      .sort((a, b) => b.duration - a.duration)[0];

    console.warn('Slow interaction-related frame', {
      blockingMs: entry.blockingDuration,
      slowestScript: culprit?.sourceURL,
      function: culprit?.sourceFunctionName,
      invoker: culprit?.invoker,
      duration: culprit?.duration,
      forcedLayout: culprit?.forcedStyleAndLayoutDuration,
    });
  }
});

observer.observe({ type: 'long-animation-frame', buffered: true });

התבנית הזו תופסת רק פריימים "כואבים" (חסימה מעל 100ms) שגם היו בהם אינטראקציות — בדיוק האוכלוסייה שתורמת ל-INP גבוה. בפעם הראשונה שהרצתי את זה על האפליקציה שלי, גיליתי תוך עשר דקות שיש סקריפט אנליטיקס של צד שלישי שאף אחד לא זוכר למה הוא שם. זה היה רגע מאלף.

שמירת ה-LoAF הגרועים ושליחה לאנליטיקס

בפרודקשן אנחנו לא רוצים להציף את הרשת בכל פריים ארוך — זה יעלה לכם יותר מהבעיה עצמה. הדפוס המקובל הוא להחזיק את עשרת הפריימים החוסמים ביותר ולשלוח אותם רק כשהמשתמש עוזב את הדף:

const MAX_LOAFS = 10;
let worstLoAFs = [];

const observer = new PerformanceObserver((list) => {
  worstLoAFs = worstLoAFs
    .concat(list.getEntries())
    .sort((a, b) => b.blockingDuration - a.blockingDuration)
    .slice(0, MAX_LOAFS);
});

observer.observe({ type: 'long-animation-frame', buffered: true });

addEventListener('visibilitychange', () => {
  if (document.visibilityState !== 'hidden') return;

  const payload = worstLoAFs.map((entry) => ({
    duration: entry.duration,
    blocking: entry.blockingDuration,
    hadInteraction: entry.firstUIEventTimestamp > 0,
    scripts: entry.scripts.map((s) => ({
      url: s.sourceURL,
      fn: s.sourceFunctionName,
      duration: Math.round(s.duration),
      forcedLayout: Math.round(s.forcedStyleAndLayoutDuration),
    })),
  }));

  navigator.sendBeacon('/analytics/loaf', JSON.stringify(payload));
});

השימוש ב-sendBeacon מבטיח שהדפדפן ישלח את הנתונים גם אם המשתמש סוגר את הטאב. שימו לב לעלות ה-payload — בדפים פעילים יש המון פריימים ארוכים, וכדאי להגביל למה שבאמת תוכלו לפעול לפיו. אחרת אתם פשוט שומרים זבל יקר באנליטיקס.

שילוב עם web-vitals.js v4

אז עכשיו החדשות הטובות. במקום לבנות שיוך LoAF←INP ידנית, ספריית web-vitals מגרסה 4 כבר עושה את זה בשבילכם. המבנה attribution.longAnimationFrameEntries מכיל את כל ה-LoAFs שהצטלבו עם זמן האינטראקציה האיטית ביותר על הדף:

import { onINP } from 'web-vitals/attribution';

onINP((metric) => {
  const { value, attribution } = metric;
  const {
    interactionTarget,
    interactionType,
    inputDelay,
    processingDuration,
    presentationDelay,
    longAnimationFrameEntries,
    longestScript,
  } = attribution;

  const debug = {
    inp: value,
    target: interactionTarget,
    type: interactionType,
    breakdown: { inputDelay, processingDuration, presentationDelay },
    slowestScript: longestScript ? {
      url: longestScript.entry.sourceURL,
      fn: longestScript.entry.sourceFunctionName,
      duration: longestScript.intersectingDuration,
      subpart: longestScript.subpart,
    } : null,
    loafCount: longAnimationFrameEntries.length,
  };

  navigator.sendBeacon('/analytics/inp', JSON.stringify(debug));
});

השדה longestScript הוא לב הכוח כאן. הוא מצביע ישירות על הסקריפט הספציפי שתפס הכי הרבה זמן בתוך אינטראקציית ה-INP, ואומר לכם באיזה תת-שלב הוא רץ: input-delay, processing-duration או presentation-delay. זה חוסך שעות (אם לא ימים) של ניחושים.

אסטרטגיית תיקון לפי סוג הבעיה

ברגע שיש לכם נתוני LoAF — איך באמת פועלים? התשובה תלויה בשדה הדומיננטי בתוך הפריים.

זמן סקריפט גבוה (script duration)

הסקריפט עצמו פשוט רץ ארוך מדי. הפתרונות הקלאסיים: לשבור עבודה ארוכה ל-tasks קצרים עם scheduler.yield() (Chrome 129+) או setTimeout, להעביר חישובים כבדים ל-Web Worker, או — ולפעמים זה הכי יעיל — להוריד event listeners מצד שלישי שלא צריכים להיות בנתיב האינטראקציה מלכתחילה.

forcedStyleAndLayoutDuration גבוה

הסקריפט גורם ל-Layout Thrashing. אתם קוראים מאפיין שמכריח חישוב לייאאוט (offsetWidth, getBoundingClientRect) אחרי כתיבה ל-DOM. הפתרון: לקבץ את כל הקריאות לפני כל הכתיבות, או להשתמש ב-ResizeObserver/IntersectionObserver במקום מדידה ידנית.

blockingDuration גבוה אבל scripts ריק

זה הגיוני אם הקוד הגיע מ-Web Worker, Service Worker או iframe חוצה-מקור. תבדקו את ה-Service Worker שלכם, או הוסיפו crossorigin="anonymous" לסקריפטים של צד שלישי כדי לקבל URLs (גוגל מסתירה את ה-URL מסיבות פרטיות אם זה לא מוגדר).

presentationDelay גבוה ב-INP

אם longestScript.subpart הוא presentation-delay, הבעיה היא לא הסקריפט אלא ה-rendering אחריו. בדקו אנימציות CSS כבדות, פילטרים, או DOM גדול שצריך re-layout. הפתרונות הקלאסיים כאן: contain: layout, content-visibility: auto, וירטואליזציה של רשימות ארוכות.

זיהוי תכונות ותמיכת דפדפנים

נכון לאפריל 2026, LoAF זמין ב-Chrome ובדפדפנים מבוססי Chromium מגרסה 123. Firefox ו-Safari עדיין לא תומכים, ולכן הקוד שלכם חייב לבדוק תמיכה לפני שימוש:

const supportsLoAF = typeof PerformanceObserver !== 'undefined'
  && PerformanceObserver.supportedEntryTypes
  && PerformanceObserver.supportedEntryTypes.includes('long-animation-frame');

if (!supportsLoAF) {
  // Fallback: השתמשו ב-Long Tasks API או רק ב-INP בסיסי
  return;
}

גם אם רוב המשתמשים שלכם בדפדפנים אחרים, ה-Chromium-only data מספיק כדי לזהות 80%+ מצווארי הבקבוק במיין-תרד. הסקריפטים והפונקציות הבעייתיות יהיו אותם דברים בכל הדפדפנים — כי הקוד שלכם זהה בכולם.

שילוב עם DevTools של Chrome

מעבר לאיסוף בפרודקשן, ה-Performance Panel ב-DevTools של Chrome 124+ מציג את ה-LoAFs ישירות בטיימליין כלוחיות אדומות (קל לפספס בהתחלה, אבל ברגע שתשימו לב — לא תפסיקו לראות אותן). כשאתם מקליטים אינטראקציה, תוכלו לראות:

  • את גבולות הפריים הארוך עם blockingDuration מוצג ישירות.
  • את כל הסקריפטים שרצו, ממוינים לפי משך.
  • את ה-call stack של כל סקריפט בלחיצה אחת.
  • קישור ישיר ל-Sources Panel לשורת הקוד הספציפית.

זה החלק החזק ביותר עבור debugging מקומי: אתם רואים את אותו הדבר שאתם משדרים מהשטח, רק עם הפרטים המלאים.

שאלות נפוצות

מה ההבדל בין Long Tasks ל-Long Animation Frames?

Long Tasks מודד משימות בודדות במיין-תרד שלקחו יותר מ-50ms. LoAF, לעומת זאת, מודד פריים אנימציה שלם שלקח יותר מ-50ms — כולל זמן הסקריפטים, ה-style/layout וה-rendering. וחשוב מכל, LoAF מספק שיוך ברמת הסקריפט והפונקציה, מה ש-Long Tasks אף פעם לא יכול היה לעשות.

למה ה-sourceURL של סקריפט מסוים ריק?

שתי סיבות עיקריות. או שהסקריפט נטען מ-cross-origin ללא header של Access-Control-Allow-Origin ותכונת crossorigin="anonymous" בתג ה-script, או שהוא הוזרק על ידי הרחבת דפדפן. במקרה הראשון תוסיפו את ה-attribute והגדרת ה-CORS המתאימה. במקרה השני? אין מה לעשות מהצד שלכם.

האם LoAF זמין רק ב-Chrome?

נכון לאפריל 2026, כן — Chrome ודפדפנים מבוססי Chromium (Edge, Opera, Brave) מגרסה 123 ומעלה. Firefox הצהירה על תמיכה חיובית אבל עדיין לא הטמיעה, ו-Safari לא תמכה עדיין. עם זאת, תקופת המדידה ב-Chromium מספיקה לזהות את רוב צווארי הבקבוק במיין-תרד.

איך LoAF משפיע על ביצועי הדף עצמו?

איסוף LoAFs דרך PerformanceObserver הוא זול מאוד. הדפדפן בכל מקרה יוצר את הרשומות, ואתם רק קוראים אותן. ה-overhead מורגש בעיקר אם אתם שולחים כל רשומה בנפרד; השתמשו ב-sendBeacon בסוף הסשן, או דגמו רק את 10 הגרועים ביותר כפי שהראינו למעלה.

מה הקשר בין blockingDuration לבין INP?

INP מודד את האינטראקציה האיטית ביותר על הדף. blockingDuration בתוך LoAF הוא המדד הקרוב ביותר ל-"כמה זמן הקוד שלכם חסם את האינטראקציה הזאת". אם תקטינו את ה-blockingDuration בפריימים שמכילים אינטראקציות (firstUIEventTimestamp > 0), ה-INP שלכם יירד באופן ישיר. פשוט וברור.

סיכום

Long Animation Frames API הוא הכלי החסר שמצא את עצמו במשך שנים בין Long Tasks הגנרי ל-INP המופשט. עם רשומות ברמת הסקריפט והפונקציה, ושילוב מובנה ב-web-vitals.js v4, אין יותר תירוצים לדיבאג INP בעלטה.

התחילו עם Observer פשוט בקוד הפרודקשן שלכם, אספו את 10 הפריימים הגרועים ביותר לכל סשן, וצרו תהליך עבודה שבועי שבו אתם בוחנים את הפונקציות ואת ה-URLs שחוזרים שוב ושוב. תוך חודש-חודשיים, מהניסיון שלי, תוכלו לראות תזוזה ממשית בדאטה של CrUX. אז קדימה — תפתחו DevTools ותריצו את ה-Observer הראשון עוד היום.

אודות הכותב Editorial Team

Our team of expert writers and editors.