BFCache ב-2026: המדריך המלא לניווט מיידי, NotRestoredReasons ותיקון חסימות

מדריך BFCache ב-2026: איך לאבחן חסימות עם NotRestoredReasons API, להחליף unload ב-pagehide ולהחזיר את ניווטי אחורה/קדימה לפחות מ-100 מ״ש, כולל תיקונים ל-Next.js 15/16 ולניטור בפרודקשן.

BFCache 2026: NotRestoredReasons ותיקון

עודכן: 29 באוגוסט, 2026

BFCache (ראשי תיבות של Back/Forward Cache) הוא מנגנון של הדפדפן ששומר את הדף כולו: DOM, JavaScript heap, סטייט של סקרולים וכל טופס, בזיכרון ברגע שהמשתמש עוזב אותו. התוצאה היא שלחיצה על "אחורה" או "קדימה" משחזרת את הדף תוך פחות מ-100 מ״ש, בלי לרוץ מחדש, בלי בקשות רשת, ובלי לבצע רינדור. בפרקטיקה זה אומר ציוני LCP ו-INP קרובים לאפס בניווטים חוזרים, אבל רק אם הדף שלך "כשיר" (eligible). ברוב האתרים שאני מבקר, לפחות 20% מהניווטים חוזרים אינם מנצלים BFCache בגלל טעויות קונפיגורציה שאפשר לתקן בשעה עבודה. המדריך הזה מלמד איך לאבחן ולתקן אותן, בדיוק על סמך הבאגים שאני פוגש בשטח.

  • BFCache שומר את מצב הדף המלא בזיכרון ומאפשר שחזור תת-100 מ״ש בלחיצה על אחורה/קדימה. CrUX 2026 מדווח על כ-50% מהניווטים בדסקטופ ו-30% במובייל.
  • NotRestoredReasons API (זמין ב-Chrome 132+ וב-Edge) חושף את הסיבה המדויקת שדף לא נכנס ל-BFCache, כולל ה-frame החוסם.
  • שלושת החוסמים הנפוצים ביותר הם Cache-Control: no-store, מאזיני unload/beforeunload, ו-WebSocket/IndexedDB פתוחים ברגע ה-pagehide.
  • שדרוג מ-unload אל pagehide, בתוספת מאזין ל-pageshow עם event.persisted, הוא התיקון הכי מהיר להחזיר eligibility.
  • שיפור ב-BFCache eligibility משנה ישירות את חלוקת ה-LCP וה-INP ב-CrUX. כל הניווטים המשוחזרים נחשבים ל-Good אוטומטית.

מה זה BFCache ואיך הוא עובד?

BFCache (Back/Forward Cache) הוא סוג נפרד לחלוטין של cache בדפדפן. הוא לא ה-HTTP cache שמאחסן קבצי JS ו-CSS על הדיסק, וגם לא ה-Service Worker cache. במקום זאת, כשהמשתמש עוזב דף (ניווט אחר, סגירת טאב או מעבר לאפליקציה אחרת), הדפדפן "מקפיא" את כל התהליך של הדף בזיכרון. זה כולל את ה-heap של JavaScript, את ה-DOM, את הסטייט של אלמנטים כמו <details>, את מיקום הסקרול ואת התוכן שהמשתמש הקליד בטפסים. אם בתוך פרק זמן קצר (30 דקות ב-Chrome, לפעמים יותר) המשתמש חוזר, הדף מופשר, מקבל אירוע pageshow עם persisted=true, ומוצג מיידית.

ההשוואה הכי טובה שאני מכיר היא ל-setTimeout על כל האפליקציה. הזמן קפא, ה-JavaScript מפסיק לרוץ, טיימרים מושהים, וקשרים פתוחים (למשל WebSockets) מנותקים או שהדף נפסל לגמרי. כשמפשירים, ה-JavaScript חוזר בדיוק לאותו State כפי שהיה. זו הסיבה ש-BFCache הוא, בעיניי, הפיצ׳ר עם ה-ROI הכי גבוה בכל אופטימיזציית ביצועים: אין קוד לכתוב, רק להסיר חסימות. לפי התיעוד הרשמי של web.dev, אתרים שהעלו את שיעור ה-BFCache שלהם מ-30% ל-70% ראו ירידה של 20-30% ב-LCP הכללי ב-CrUX.

איך בודקים אם BFCache עובד באתר שלך?

הדרך המהירה ביותר לבדוק eligibility היא דרך Chrome DevTools. פותחים DevTools בטאב Application, נכנסים ל-Back/forward cache ולוחצים "Test back/forward cache". Chrome מבצע ניווט מדומה החוצה וחזרה, ומדווח האם הדף נכנס ל-BFCache או לא. אם לא, הפאנל יציג רשימת סיבות מדויקות. לדוגמה: HasCacheControlNoStore, UnloadHandlerExistsInMainFrame, או WebSocket. הפאנל הזה מהיר לבדיקה חד-פעמית, אבל הוא לא מתאים לניטור פרודקשן.

למעקב אמיתי בשטח כדאי להשתמש בספריית web-vitals של Google, שמאז גרסה 4.2 כוללת דיווח על BFCache restores דרך אירוע ה-pageshow. הנה איך אני משלב את זה בפרוייקטים שלי:

import { onLCP, onINP, onCLS } from 'web-vitals';

// The library automatically re-fires metrics on bfcache restore
// with navigationType === 'back-forward-cache'
onLCP((metric) => {
  if (metric.navigationType === 'back-forward-cache') {
    console.log('LCP after bfcache restore:', metric.value, 'ms');
    // Report to your analytics; this will always be near 0
  }
});

// Manually track pageshow to see if bfcache was used
window.addEventListener('pageshow', (event) => {
  const restoredFromBFCache = event.persisted;
  // Send restoredFromBFCache = true/false to analytics
  navigator.sendBeacon('/analytics', JSON.stringify({
    event: 'pageshow',
    bfcache: restoredFromBFCache,
    timestamp: Date.now(),
  }));
});

אם אתה מנטר את ביצועי אתרים בפרודקשן באמצעות web-vitals ו-CrUX, כדאי להוסיף שדה navigationType לכל דיווח מטריקה. זה יאפשר לך להשוות התפלגות LCP בין ניווטים רגילים לבין ניווטי BFCache, והפער בדרך כלל דרמטי.

NotRestoredReasons API: הכלי לאבחון מדויק

ה-NotRestoredReasons API הוא הכלי החזק ביותר שקיים היום לדיבאג BFCache. הוא זמין מ-Chrome 132 ומדצמבר 2025 גם ב-Edge ובגרסאות Chromium החדשות (Brave, Opera, Arc). ה-API חושף עבור כל ניווט שנכשל בשחזור בדיוק למה. הוא נגיש דרך performance.getEntriesByType('navigation')[0].notRestoredReasons:

window.addEventListener('pageshow', (event) => {
  const navEntry = performance.getEntriesByType('navigation')[0];
  const reasons = navEntry?.notRestoredReasons;

  if (!reasons) {
    // Restored successfully or API not supported
    return;
  }

  // Log the main-frame blocking reasons
  reasons.reasons.forEach((r) => {
    console.warn(`BFCache blocked by: ${r.reason}`);
    // Example reasons: "unload-listener", "has-cache-control-no-store",
    // "websocket", "indexeddb", "notifications-permission-request"
  });

  // Recurse into blocked child frames (iframes, ads)
  reasons.children?.forEach((child) => {
    console.warn('Blocked child frame:', child.src, child.reasons);
  });

  // Send to analytics for aggregation
  navigator.sendBeacon('/analytics/bfcache', JSON.stringify({
    url: location.pathname,
    reasons: reasons.reasons.map(r => r.reason),
    blockedFrames: reasons.children?.length ?? 0,
  }));
});

הפורמט של האובייקט מדויק: reasons.reasons מכיל את הסיבות של ה-main frame, ו-reasons.children מכיל רקורסיבית את כל ה-frame-ים הפנימיים שחסמו. זה חשוב מיוחד לאתרים עם iframes של פרסום, מפות, או וידג׳טים חיצוניים. פעמים רבות ה-main frame שלך "נקי" אבל iframe של Google Ads מכניס Cache-Control: no-store שחוסם את כל הדף. בלי ה-API הזה, אין דרך לגלות את זה חוץ מלנחש.

לפי התיעוד של Chrome לפיתוח web, הפורמט הזה סופי ומתועד ב-Web Performance WG, כך שאין חשש לשינויים breaking. שלב אותו בדשבורד RUM שלך והוסף התראה כאשר סיבה מסוימת עוברת סף (למשל, יותר מ-5% מהניווטים חסומים ע"י websocket). זה יחסוך לך ימי דיבאג.

הסיבות הנפוצות ל-BFCache שלא עובד

מתוך עשרות פרוייקטים שבדקתי בשנתיים האחרונות, שש סיבות מסבירות מעל 90% מהחסימות. אני מסדר אותן לפי שכיחות ורמת קושי לתיקון.

1. Cache-Control: no-store

זו החסימה הכי נפוצה, והכי קלה לתיקון. אם ה-response של הדף (או של iframe פנימי) כולל את הכותרת Cache-Control: no-store, הדפדפן לא יכניס אותו ל-BFCache אפילו פעם אחת. הרבה מפתחים משתמשים ב-no-store מחשש שדפים אישיים יישמרו, אבל BFCache הוא זיכרון פר-טאב, לא cache משותף. הפתרון: השתמש ב-Cache-Control: no-cache, private. זה יבטיח שהדף לא יישמר ב-cache משותף אבל כן יזכה ל-BFCache. מ-Chrome 96, יש חריגות ספציפיות (למשל Cookies שמעורבבים), אבל הכלל הכללי עדיין תקף.

2. מאזיני unload ו-beforeunload

ה-event unload מסומן כ-deprecated ב-2024, אבל עדיין נפוץ בקוד ישן ובספריות אנליטיקס. כל מאזין unload במקור פוסל אוטומטית את הדף מ-BFCache ב-Chrome ו-Firefox. הפתרון: החלף את unload ב-pagehide. עבור beforeunload, ההשפעה תלויה בפלטפורמה. במובייל זה תמיד חוסם, בדסקטופ רק אם הפונקציה מחזירה ערך. הימנע ממנו כמעט לחלוטין. אם אתה חייב אזהרת "יש לך שינויים לא שמורים", הוסף אותו רק כשהמשתמש התחיל לערוך, והסר אותו מיד אחרי שמירה.

3. WebSocket, WebRTC ו-IndexedDB פתוחים

קשרים פתוחים בזמן pagehide חוסמים BFCache באופן קטגורי. סגור אותם ידנית:

let ws = new WebSocket('wss://example.com/live');

window.addEventListener('pagehide', () => {
  // Close before the page freezes to allow bfcache
  if (ws?.readyState === WebSocket.OPEN) {
    ws.close();
  }
});

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // Restored from bfcache; reopen the connection
    ws = new WebSocket('wss://example.com/live');
  }
});

4. בקשות הרשאה פתוחות (Notifications, Geolocation)

אם קראת ל-Notification.requestPermission() וה-prompt עדיין פעיל, הדף לא נכנס ל-BFCache. פתרון: אל תבקש הרשאות אוטומטית בטעינה. השתמש ב-דפוסי אינטראקציה מותאמים, ובקש הרשאות רק אחרי לחיצה מפורשת של המשתמש.

5. HTTPS-בלבד ותכני mixed content

Chrome פוסל דפים עם תכני mixed content מ-BFCache באופן קבוע. וודא שכל המשאבים (תמונות, iframes, XHR) נטענים ב-https. פרוקסי דרך CDN יעזור לך לנקות את זה בקלות. אני נתקלתי בזה בפרוייקט אחד ששרת legacy עדיין החזיר תמונות ב-http, ולקח לי חצי יום להבין למה BFCache פתאום ירד ל-8%.

6. Sensors ו-Screen Wake Lock

שימוש ב-Sensor API, Screen Wake Lock, או WebXR חוסם BFCache בזמן שהם פעילים. שחרר את המשאבים ב-pagehide ובקש אותם מחדש ב-pageshow.

איך BFCache משפיע על Core Web Vitals?

לפי מסמך web.dev על BFCache ו-Core Web Vitals, Google מודדת LCP, INP ו-CLS גם על ניווטי BFCache, ומכיוון שהדף משוחזר מיידית, כל המדדים האלה נחשבים ל-Good אוטומטית. כלומר, אחוז ה-BFCache שלך משפיע ישירות על אחוז ה-Good בכל אחד משלושת המדדים ב-CrUX. אם 40% מהניווטים שלך משתמשים ב-BFCache, ו-60% נותנים LCP רגיל של 3 שניות, ה-CrUX הכולל יהיה בסביבות 2.4 שניות. זה יכול להעביר אותך מ-Poor ל-Needs Improvement או מ-Needs Improvement ל-Good.

ההשפעה על INP דרמטית עוד יותר. אחרי BFCache restore, ה-JavaScript heap כבר חם, כל האינטראקציות הראשונות מגיבות תוך פחות מ-16 מ״ש, ואין long tasks. זה בדיוק תרחיש שבו אני ממליץ להשתמש ב-Long Animation Frames API לדיבאג INP, כדי לוודא שגם בניווטים רגילים אתה מגיע לביצועים דומים לזה של BFCache.

לגבי CLS, יש הבדל חשוב: מדידת CLS לאחר BFCache restore מתאפסת. כלומר, CLS מצטבר לא "נמשך" בין הדפים, וכל שיפט חדש לאחר restore נספר בנפרד. זו הזדמנות טובה. אם אתה מקבל CLS גבוה רק בטעינה ראשונית (למשל בגלל תמונות בלי מימדים), משתמשים חוזרים דרך BFCache לא יראו אותו בכלל.

pageshow, pagehide וניהול State נכון

שני אירועים חשובים לניהול מחזור החיים של דף עם BFCache: pagehide מופעל כשהדף עוזב (לפני שהוא מוקפא), ו-pageshow מופעל כשהוא חוזר (אחרי הפשרה או טעינה רגילה). המפתח הוא ה-property persisted באירוע pageshow:

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // Page was restored from bfcache; refresh volatile data only
    refreshUserSession();
    reconnectRealtimeSubscriptions();
    updateCartBadge(); // in case the user added items in another tab
  } else {
    // Normal navigation, full page load
    initializeApp();
  }
});

window.addEventListener('pagehide', (event) => {
  if (event.persisted) {
    // Page is entering bfcache; clean up resources but keep state
    pauseAnimations();
    closeWebSockets();
  } else {
    // Page is truly being unloaded; final analytics beacon
    sendFinalAnalytics();
  }
});

בפועל, אתה רוצה לחדש רק נתונים "רגישים" בזמן: session tokens, badges של הודעות חדשות, ומצב עגלת קניות. אל תרנדר מחדש את כל האפליקציה, זה מבטל את היתרון של BFCache. חשוב במיוחד ב-SPA: אל תפעיל מחדש את ה-router, אל תבצע hydration חוזרת. State של Redux/Zustand/Signals נשמר במלואו.

Chrome מול Safari מול Firefox

שלושת הדפדפנים תומכים ב-BFCache אבל עם התנהגות שונה:

תכונה Chrome / Edge Safari Firefox
תמיכה ב-Cache-Control: no-store חוסם (עם חריגות מ-96) מאפשר חוסם
WebSocket פתוח חוסם מאפשר (מנתק ומחדש) חוסם
מאזין unload חוסם חוסם חוסם
NotRestoredReasons API Chrome 132+ לא נתמך Firefox 128+ (חלקי)
DevTools Bfcache Panel כן (Application) Web Inspector 17+ לא
זמן מקסימלי ב-cache כ-10 דקות (מובייל), 30 דקות (דסקטופ) ללא הגבלה קשיחה כ-30 דקות

ההבדל הכי חשוב לדעת: Safari הרבה פחות "אכזרי". הוא מכניס דפים ל-BFCache גם אם יש WebSocket פתוח, ומחלק את התהליך של סגירה/פתיחה מחדש ל-lifecycle events פנימיים. זה אומר שבדיקה רק בכרום מטעה, וצריך גם ב-Safari וב-Firefox. אני ממליץ ב-QA להריץ תסריט אוטומטי עם Playwright ב-3 הדפדפנים ולוודא ש-event.persisted חוזר true.

תיקון BFCache באפליקציית Next.js אמיתית

ב-Next.js 15 ו-16, App Router מגדיר אוטומטית Cache-Control: no-store על דפים דינמיים, וזו החסימה מספר 1 של BFCache באפליקציות Next.js. הפתרון תלוי בסוג הדף:

// app/dashboard/page.tsx: dynamic page
// The default `no-store` blocks bfcache. Override to `private, no-cache`
// which still prevents shared caches but allows bfcache.

export const dynamic = 'force-dynamic';

export async function generateMetadata() {
  return {
    // metadata
  };
}

export default async function DashboardPage() {
  const data = await fetchUserData();
  return <Dashboard data={data} />;
}

// middleware.ts: override the header
import { NextResponse } from 'next/server';

export function middleware(req) {
  const res = NextResponse.next();
  // Replace no-store with private no-cache for bfcache eligibility
  const cc = res.headers.get('cache-control') ?? '';
  if (cc.includes('no-store')) {
    res.headers.set('cache-control', 'private, no-cache, must-revalidate');
  }
  return res;
}

export const config = { matcher: ['/dashboard/:path*'] };

אם אתה משתמש ב-Cache Components ו-PPR של Next.js 16, הבעיה הזאת כמעט נפתרת אוטומטית. החלקים הסטטיים של הדף מקבלים public, s-maxage וזכאים ל-BFCache. עדיין כדאי לוודא עם NotRestoredReasons ש-mid-frame של iframes (למשל Stripe Elements, reCAPTCHA) לא חוסמים.

בעיה שנייה בבנייה של Next.js: ה-Router.prefetch מוסיף לפעמים iframe מוסתר עם no-store. הפתרון: השתמש בעדכני next המאוחרים (15.4+) ששיפרו את זה, ופתח Issue ב-GitHub של Next.js אם מצאת חסימה שקשורה ל-frame פנימי. הצוות מגיב מהר לדיווחים כאלה, זה מה שקרה לי בבאג שדיווחתי בתחילת השנה.

אופטימיזציה מתקדמת: לשלב BFCache עם Speculation Rules

הצעד הבא מעבר ל-BFCache הוא שילוב עם Speculation Rules API, פרה-רנדרינג של דפים שהמשתמש עומד לנווט אליהם. השילוב הוא אידיאלי: BFCache מטפל בניווט אחורה, ו-Speculation Rules מטפל בניווט קדימה. יחד הם מכסים 80-90% מכל הניווטים באתר טיפוסי, ומספקים חוויית ניווט שנראית מיידית לחלוטין. חשוב לוודא ש-Speculation Rules לא מוסיף iframes ש-no-store, וש-Prerender Same-Origin לא סופר כתאונת CLS או LCP חדשה. Chrome 128+ מטפל בזה כראוי.

כדאי גם לשלב עם Resource Hints ו-Fetch Priority. אם אתה יודע ש-90% מהמשתמשים לוחצים "אחורה" אחרי כניסה, אתה יכול להשתמש ב-prefetch על משאבים של הדף הקודם כשהמשתמש הגיע. יחד עם BFCache eligibility, זה מבטיח שגם אם ה-cache expired, החזרה תישאר תת-100 מ״ש.

שאלות נפוצות

מה זה BFCache ב-Chrome ואיך הוא שונה מ-HTTP Cache?

BFCache הוא זיכרון In-memory של דפדפן שמאחסן את הדף כולו (DOM, JavaScript heap וסטייט) בלחיצה על אחורה או קדימה. בשונה מ-HTTP Cache שמאחסן קבצים בודדים על הדיסק ומצריך רינדור מחדש, BFCache משחזר את הדף בפחות מ-100 מ״ש כי אין רינדור בכלל.

איך אני יודע ש-BFCache עובד באתר שלי?

ב-Chrome DevTools פתח את הטאב Application, כנס ל-Back/forward cache ולחץ "Test back/forward cache". לניטור בפרודקשן השתמש בספריית web-vitals: אירוע pageshow עם event.persisted=true מציין restore מוצלח. עבור אבחון מדויק, השתמש ב-NotRestoredReasons API.

למה BFCache לא עובד למרות שאין לי מאזין unload?

הסיבה השכיחה השנייה היא הכותרת Cache-Control: no-store. שנה אותה ל-private, no-cache. סיבות נוספות כוללות WebSocket פתוח, iframe עם no-store (למשל Google Ads), או ספרייה חיצונית שמוסיפה unload באופן פנימי. השתמש ב-NotRestoredReasons API כדי לגלות את הסיבה המדויקת.

האם BFCache עובד עם WebSockets?

לא ב-Chrome ו-Firefox. WebSocket פתוח בזמן pagehide פוסל את הדף. הפתרון: סגור את ה-WebSocket ב-pagehide ופתח אותו מחדש ב-pageshow כאשר event.persisted=true. ב-Safari התנהגות שונה, הדפדפן מנהל את הניתוק/חיבור אוטומטית.

איך BFCache משפיע על ציוני Core Web Vitals ב-CrUX?

ניווטי BFCache נמדדים כרגיל אבל LCP, INP ו-CLS שלהם קרובים לאפס כי אין רינדור. כתוצאה, כל אחוז BFCache מוסיף כמעט ישירות לאחוז הניווטים ב-Good ב-CrUX. אתרים שהעלו BFCache מ-30% ל-70% מדווחים על שיפור של 20-30% ב-LCP הכללי בשדה.

מה הכי הרבה BFCache שאני יכול להשיג?

עבור אתרי תוכן סטטיים, 80-90% מהניווטים אחורה אפשריים ב-BFCache. עבור אפליקציות דינמיות שדורשות אימות, סביר להגיע ל-50-70%. יעד ריאלי: 60% במובייל ו-75% בדסקטופ. שיעורים נמוכים מ-30% תמיד מצביעים על בעיה שאפשר לתקן.

Alex Petrov
אודות הכותב Alex Petrov

Web performance engineer who treats every millisecond as a personal challenge. Has profiled more sites than he can count.