Skip to Content
LoadingPrefetch Resource Validation

Prefetch resource validation

Chrome 54+ Edge 17+ Firefox 45+ Safari 16.4+ Details

Detects potential performance issues with rel="prefetch" resource hints by analyzing quantity, size, type, and appropriateness of prefetched resources. Excessive or incorrect prefetch usage can waste bandwidth, delay critical resources, and negatively impact mobile users.

What is prefetch?

rel="prefetch" is a resource hint that tells the browser to fetch and cache resources that may be needed for future navigations, during idle time. Unlike preload, which is for the current page, prefetch is for anticipated next-page resources.

Difference between preload and prefetch

HintPriorityTimingCacheUse Case
preloadHighCurrent pageMemory cacheCritical resources for current page (LCP)
prefetchLowestFuture navigationDisk cacheResources for likely next pages

Visual comparison

Preload runs at high priority into the memory cache for critical resources of the current page, while prefetch runs at the lowest priority into the disk cache for future navigationsConnections: User loads page to Resource hint type?; Resource hint type? to High Priority (preload); Resource hint type? to Lowest Priority (prefetch); High Priority to Memory Cache; Lowest Priority to Disk Cache; High Priority to Use: Current Page Critical Resources; Lowest Priority to Use: Future Navigation; Memory Cache to Fast access for current page; Disk Cache to Available for next navigation.User loads pageResource hinttype?High PriorityLowest PriorityMemory CacheDisk CacheUse: Current PageCritical ResourcesUse: FutureNavigationFast access forcurrent pageAvailable for nextnavigationpreloadprefetch
Preload runs at high priority into the memory cache for critical resources of the current page, while prefetch runs at the lowest priority into the disk cache for future navigationsConnections: User loads page to Resource hint type?; Resource hint type? to High Priority (preload); Resource hint type? to Lowest Priority (prefetch); High Priority to Memory Cache; Lowest Priority to Disk Cache; High Priority to Use: Current Page Critical Resources; Lowest Priority to Use: Future Navigation; Memory Cache to Fast access for current page; Disk Cache to Available for next navigation.User loads pageResource hinttype?High PriorityLowest PriorityMemory CacheDisk CacheUse: CurrentPage CriticalResourcesUse: FutureNavigationFast access forcurrent pageAvailable for nextnavigationpreloadprefetch

Problems commonly detected

IssueImpactDetection
Excessive prefetchWasted bandwidthMore than 10-15 resources prefetched
Large filesMobile data consumptionIndividual resources >500KB
Total sizeNetwork congestionCumulative prefetch >2MB
Wrong resource typesIneffective optimizationLarge images/videos instead of code
Current page resourcesWrong hint typeShould use preload instead
Missing cache headersPrefetch won’t be usedResources without proper cache directives

Snippet

Run it from an AI agent: the prompt uses the CLI to measure any URL and report the result.

// Prefetch Resource Validation
// https://webperf-snippets.net

(() => {
  // @shared isOwnRequest
  // True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
  // did not make it, so no snippet counts it.
  function isOwnRequest(entry) {
    return entry.name.endsWith("#webperf-snippets");
  }
  // @end-shared isOwnRequest

  // Get all prefetch hints
  const prefetchLinks = Array.from(document.querySelectorAll('link[rel="prefetch"]'));

  if (prefetchLinks.length === 0) {
    // Early return is safe here - console.group hasn't been opened yet
    console.log(
      '%cℹ️ No prefetch hints on this page (rel="prefetch"), nothing to validate.',
      "color: #3b82f6; font-weight: bold;",
    );
    console.log(
      "%cℹ️ Prefetch is for future navigation resources. Use it sparingly for predictable user journeys.",
      "color: #3b82f6;",
    );
    return {
      script: "Prefetch-Resource-Validation",
      status: "ok",
      count: 0,
      items: [],
      issues: [{ severity: "info", message: 'No prefetch hints on this page (rel="prefetch"), so there was nothing to validate.' }],
    };
  }

  // Get performance entries for all resources
  const performanceEntries = performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e));

  // Pre-normalize URLs for efficient matching (avoids repeated URL parsing in nested loops)
  const entryUrlMap = new Map();
  performanceEntries.forEach((entry) => {
    try {
      const normalizedUrl = new URL(entry.name, location.origin).href;
      if (!entryUrlMap.has(normalizedUrl)) {
        entryUrlMap.set(normalizedUrl, []);
      }
      entryUrlMap.get(normalizedUrl).push(entry);
    } catch {
      // Ignore malformed URLs in performance entries
    }
  });

  // Valid 'as' attribute values for prefetch (per HTML spec)
  const validAsValues = new Set([
    "script",
    "style",
    "font",
    "image",
    "video",
    "audio",
    "document",
    "fetch",
    "track",
    "worker",
    // Note: "media" is not a valid spec value, use "video" or "audio" instead
  ]);

  // Thresholds
  const THRESHOLDS = {
    maxCount: 10,
    largeFileSize: 500 * 1024, // 500KB
    largeScriptSize: 1024 * 1024, // 1MB for scripts specifically
    totalSizeWarning: 2 * 1024 * 1024, // 2MB
    totalSizeCritical: 5 * 1024 * 1024, // 5MB
  };

  // Analyze each prefetch
  const issues = [];
  const validPrefetch = [];
  const seenUrls = new Set(); // Track duplicates
  let totalSize = 0;
  let totalTransferSize = 0;

  prefetchLinks.forEach((link) => {
    const href = link.href;
    const as = link.getAttribute("as") || "unknown";
    // Extract filename from URL; fallback to full href if URL is just origin
    // Note: Removes query params but preserves hash (e.g., page#section)
    const shortUrl = href.split("/").pop()?.split("?")[0]?.split("#")[0] || href;

    // Normalize URL for matching
    let normalizedUrl;
    try {
      normalizedUrl = new URL(href, location.origin).href;
    } catch {
      // Invalid href - will be caught later
      normalizedUrl = href;
    }

    // Find matching performance entries using pre-built map
    const matchingEntries = entryUrlMap.get(normalizedUrl) || [];
    const perfEntry = matchingEntries[0]; // Use first matching entry

    const analysis = {
      link,
      href,
      shortUrl,
      as,
      size: 0,
      transferSize: 0,
      duration: 0,
      loaded: false,
      isCurrentPage: false,
      warnings: [],
    };

    // Check for missing or invalid 'as' attribute
    if (as === "unknown") {
      analysis.warnings.push({
        type: "missing-as",
        severity: "warning",
        message: "Missing 'as' attribute - browser cannot apply correct MIME type matching",
      });
    } else if (!validAsValues.has(as)) {
      analysis.warnings.push({
        type: "invalid-as",
        severity: "warning",
        message: `Invalid 'as' value: "${as}" - should be one of: ${Array.from(validAsValues).join(", ")}`,
      });
    }

    // Check for duplicate prefetch
    if (seenUrls.has(normalizedUrl)) {
      analysis.warnings.push({
        type: "duplicate-prefetch",
        severity: "warning",
        message: "Duplicate prefetch - this URL is prefetched multiple times",
      });
    }
    seenUrls.add(normalizedUrl);

    // Check if resource was actually loaded
    if (perfEntry) {
      analysis.loaded = true;
      analysis.size = perfEntry.decodedBodySize || 0;
      analysis.transferSize = perfEntry.transferSize || 0;
      analysis.duration = perfEntry.duration || 0;

      // Cache status detection with CORS considerations
      // transferSize === 0 can mean: (1) cached, or (2) cross-origin without Timing-Allow-Origin
      if (perfEntry.encodedBodySize > 0 && perfEntry.transferSize === 0) {
        analysis.cacheStatus = "cached";
      } else if (perfEntry.encodedBodySize === 0 && perfEntry.transferSize === 0) {
        // Both zero = likely CORS-blocked timing info
        analysis.cacheStatus = "unknown (CORS)";
      } else {
        analysis.cacheStatus = "network";
      }

      totalSize += analysis.size;
      totalTransferSize += analysis.transferSize;

      // Check if it's a current page resource
      // Uses 5s heuristic: resources loaded in first 5s are likely for current page, not prefetch
      // This cutoff works for most pages but may flag slow pages incorrectly
      const isCurrentPageResource = matchingEntries.some(
        (entry) => entry.initiatorType !== "link" && entry.startTime < 5000,
      );

      if (isCurrentPageResource) {
        analysis.isCurrentPage = true;
        analysis.warnings.push({
          type: "wrong-hint",
          severity: "error",
          message: "Resource used on current page - should use preload instead",
        });
      }

      // Check file size
      if (analysis.size > THRESHOLDS.largeFileSize) {
        const sizeMB = (analysis.size / (1024 * 1024)).toFixed(2);
        analysis.warnings.push({
          type: "large-file",
          severity: "warning",
          message: `Large file (${sizeMB}MB) - consider if prefetch is appropriate`,
        });
      }

      // Check resource type appropriateness (size-based)
      // Note: Only checks if resource has loaded (has perfEntry with size)
      let isInappropriate = false;
      let inappropriateReason = "";

      if (as === "video" || as === "audio") {
        isInappropriate = true;
        inappropriateReason = "Video/audio files are typically too large for prefetch";
      } else if (as === "image" && analysis.size > 200 * 1024) {
        isInappropriate = true;
        inappropriateReason = `Image is ${(analysis.size / 1024).toFixed(0)}KB (>200KB threshold)`;
      } else if (as === "script" && analysis.size > THRESHOLDS.largeScriptSize) {
        isInappropriate = true;
        inappropriateReason = `Script is ${(analysis.size / 1024).toFixed(0)}KB (>1MB threshold)`;
      }

      if (isInappropriate) {
        analysis.warnings.push({
          type: "inappropriate-type",
          severity: "warning",
          message: `Type "${as}" may not be suitable for prefetch: ${inappropriateReason}`,
        });
      }
    } else {
      // Prefetch defined but not loaded yet (or failed)
      analysis.warnings.push({
        type: "not-loaded",
        severity: "info",
        message: "Not loaded yet or failed to load",
      });
    }

    // Categorize
    if (analysis.warnings.length > 0) {
      issues.push(analysis);
    } else {
      validPrefetch.push(analysis);
    }
  });

  // Check total count
  const countIssues = [];
  if (prefetchLinks.length > THRESHOLDS.maxCount) {
    countIssues.push({
      type: "excessive-count",
      severity: "warning",
      message: `${prefetchLinks.length} prefetch hints found (recommended: <${THRESHOLDS.maxCount})`,
      explanation: "Excessive prefetch can waste bandwidth, especially on mobile networks",
    });
  }

  // Check total size
  const totalSizeMB = (totalSize / (1024 * 1024)).toFixed(2);
  const totalTransferMB = (totalTransferSize / (1024 * 1024)).toFixed(2);

  if (totalSize > THRESHOLDS.totalSizeCritical) {
    countIssues.push({
      type: "excessive-size",
      severity: "error",
      message: `Total prefetch size: ${totalSizeMB}MB (critical threshold exceeded)`,
      explanation: "This is very high and will significantly impact mobile users",
    });
  } else if (totalSize > THRESHOLDS.totalSizeWarning) {
    countIssues.push({
      type: "high-size",
      severity: "warning",
      message: `Total prefetch size: ${totalSizeMB}MB (warning threshold exceeded)`,
      explanation: "Consider reducing prefetch to improve mobile experience",
    });
  }

  // Display results
  console.group("%c🔍 Prefetch Resource Validation", "font-weight: bold; font-size: 14px;");

  // Count actually loaded resources
  const loadedCount = [...issues, ...validPrefetch].filter((item) => item.loaded).length;

  // Count meaningful issues (exclude "info" severity)
  const meaningfulIssues = issues.filter((i) =>
    i.warnings.some((w) => w.severity === "error" || w.severity === "warning"),
  );
  const totalMeaningfulIssues = meaningfulIssues.length + countIssues.length;

  console.log("");
  console.log("%cSummary:", "font-weight: bold;");
  console.log(`   Total prefetch hints: ${prefetchLinks.length}`);
  console.log(`   Loaded resources: ${loadedCount}`);
  console.log(`   Total size: ${totalSizeMB} MB (decompressed)`);
  console.log(`   Transfer size: ${totalTransferMB} MB (over network)`);
  console.log(`   Issues found: ${totalMeaningfulIssues} (excluding "not loaded" info)`);

  // Show count/size issues first
  if (countIssues.length > 0) {
    console.log("");
    console.group(
      `%c⚠️ Overall Issues (${countIssues.length})`,
      "color: #ef4444; font-weight: bold;",
    );

    countIssues.forEach((issue) => {
      const color = issue.severity === "error" ? "#ef4444" : "#f59e0b";
      const icon = issue.severity === "error" ? "🔴" : "⚠️";
      console.log("");
      console.log(`%c${icon} ${issue.message}`, `color: ${color}; font-weight: bold;`);
      console.log(`   ${issue.explanation}`);
    });

    console.groupEnd();
  }

  // Show resource-specific issues
  const errors = issues.filter((i) => i.warnings.some((w) => w.severity === "error"));
  const warnings = issues.filter(
    (i) =>
      !i.warnings.some((w) => w.severity === "error") &&
      i.warnings.some((w) => w.severity === "warning"),
  );
  const infos = issues.filter((i) => i.warnings.every((w) => w.severity === "info"));

  if (errors.length > 0) {
    console.log("");
    console.group(`%c🔴 Critical Issues (${errors.length})`, "color: #ef4444; font-weight: bold;");

    const errorTable = errors.map((item) => ({
      Resource: item.shortUrl,
      Type: item.as,
      Size: item.size > 0 ? `${(item.size / 1024).toFixed(1)} KB` : "Unknown",
      Duration: item.duration > 0 ? `${item.duration.toFixed(0)} ms` : "N/A",
      Issue: item.warnings.map((w) => w.message).join("; "),
    }));

    console.table(errorTable);

    console.log("");
    console.log("%cDetails:", "font-weight: bold;");
    errors.forEach((item, i) => {
      console.log("");
      console.log(`%c${i + 1}. ${item.shortUrl}`, "font-weight: bold;");
      console.log(`   URL: ${item.href}`);
      console.log(`   Type: ${item.as}`);
      if (item.size > 0) {
        console.log(`   Size: ${(item.size / 1024).toFixed(1)} KB`);
      }
      item.warnings.forEach((w) => {
        console.log(`   ❌ ${w.message}`);
      });
      console.log("   Element:", item.link);
    });

    console.groupEnd();
  }

  if (warnings.length > 0) {
    console.log("");
    console.group(`%c⚠️ Warnings (${warnings.length})`, "color: #f59e0b; font-weight: bold;");

    const warningTable = warnings.map((item) => ({
      Resource: item.shortUrl,
      Type: item.as,
      Size: item.size > 0 ? `${(item.size / 1024).toFixed(1)} KB` : "Unknown",
      Duration: item.duration > 0 ? `${item.duration.toFixed(0)} ms` : "N/A",
      Issue: item.warnings.map((w) => w.message).join("; "),
    }));

    console.table(warningTable);

    console.groupEnd();
  }

  if (infos.length > 0) {
    console.log("");
    console.group(`%cℹ️ Info (${infos.length})`, "color: #3b82f6; font-weight: bold;");

    const infoTable = infos.map((item) => ({
      Resource: item.shortUrl,
      Type: item.as,
      Status: item.warnings.map((w) => w.message).join("; "),
    }));

    console.table(infoTable);

    console.groupEnd();
  }

  // Show valid prefetch
  if (validPrefetch.length > 0) {
    console.log("");
    console.group(
      `%c✅ Valid Prefetch (${validPrefetch.length})`,
      "color: #22c55e; font-weight: bold;",
    );

    const validTable = validPrefetch.map((item) => ({
      Resource: item.shortUrl,
      Type: item.as,
      Size: item.size > 0 ? `${(item.size / 1024).toFixed(1)} KB` : "Unknown",
      Duration: item.duration > 0 ? `${item.duration.toFixed(0)} ms` : "N/A",
      Status: item.loaded ? "✅ Loaded" : "Pending",
      Cache: item.cacheStatus || "N/A",
    }));

    console.table(validTable);

    console.groupEnd();
  }

  // Best practices
  console.log("");
  console.group("%c📝 Best Practices", "color: #3b82f6; font-weight: bold;");
  console.log("");
  console.log("%c✅ Good uses of prefetch:", "font-weight: bold; color: #22c55e;");
  console.log("   • Next page chunks in predictable navigation (e.g., multi-step forms)");
  console.log("   • Fonts used on commonly visited next pages");
  console.log("   • Small scripts/styles for main navigation targets");
  console.log("   • Resources based on user behavior analytics");
  console.log("");
  console.log("%c❌ Bad uses of prefetch:", "font-weight: bold; color: #ef4444;");
  console.log("   • Resources needed on current page (use preload instead)");
  console.log("   • Large images/videos without user intent analysis");
  console.log("   • Excessive prefetch (>10 resources) hurting mobile users");
  console.log("   • Resources without proper cache headers");
  console.log("");
  console.log("%cRecommended thresholds:", "font-weight: bold;");
  console.log(`   • Resource count: <${THRESHOLDS.maxCount} resources`);
  console.log(`   • Individual file size: <${THRESHOLDS.largeFileSize / 1024} KB`);
  console.log(`   • Total prefetch size: <${THRESHOLDS.totalSizeWarning / (1024 * 1024)} MB`);
  console.log("");
  console.log("%cCode examples:", "font-weight: bold;");
  console.log("");
  console.log("%c  ✅ Good: Prefetch next page script", "color: #22c55e;");
  console.log(
    '%c  <link rel="prefetch" href="/checkout-page.js" as="script">',
    "font-family: monospace;",
  );
  console.log("");
  console.log("%c  ❌ Bad: Prefetch current page resource", "color: #ef4444;");
  console.log(
    '%c  <link rel="prefetch" href="/hero-image.jpg" as="image">',
    "font-family: monospace;",
  );
  console.log('%c  <!-- Should be: rel="preload" -->', "font-family: monospace; color: #22c55e;");

  console.groupEnd();

  // Summary
  if (totalMeaningfulIssues === 0) {
    console.log("");
    console.log(
      "%c✅ Great! No issues detected with prefetch usage.",
      "color: #22c55e; font-weight: bold; font-size: 14px;",
    );
    console.log(
      "%cAll prefetch hints appear appropriate for future navigation.",
      "color: #22c55e;",
    );
  } else {
    console.log("");
    console.log(
      `%c⚠️ Found ${totalMeaningfulIssues} issue(s). Review recommendations above.`,
      "color: #ef4444; font-weight: bold;",
    );
  }

  console.groupEnd();

  return {
    script: "Prefetch-Resource-Validation",
    status: "ok",
    count: prefetchLinks.length,
    details: {
      loadedCount,
      validCount: validPrefetch.length,
      issueCount: totalMeaningfulIssues,
      totalSizeBytes: totalSize,
      totalTransferBytes: totalTransferSize,
    },
    items: [...validPrefetch, ...issues].map(item => ({ href: item.shortUrl, as: item.as, sizeBytes: item.size, loaded: item.loaded, isCurrentPage: item.isCurrentPage || false })),
    issues: [
      ...countIssues.map(i => ({ severity: i.severity === "error" ? "error" : "warning", message: i.message })),
      ...issues.flatMap(item => item.warnings.filter(w => w.severity !== "info").map(w => ({ severity: w.severity, message: `${item.shortUrl}: ${w.message}` }))),
    ],
  };
})();

Understanding the results

Summary section

Shows the overall state of prefetch usage:

  • Total number of prefetch hints
  • How many were actually loaded
  • Total size (decompressed and transfer size)
  • Number of issues detected

Issue categories

CategorySeverityDescription
Wrong hint typeErrorResource used on current page - should use preload not prefetch (*)
Missing asWarningMissing as attribute prevents correct MIME type matching
Invalid asWarningInvalid as attribute value (typo or unsupported type)
Duplicate prefetchWarningSame URL is prefetched multiple times (wasteful)
Excessive countWarningToo many prefetch hints (>10) wastes bandwidth
Large fileWarningIndividual resource >500KB may be too large
Excessive sizeErrorTotal prefetch >5MB significantly impacts mobile users
High sizeWarningTotal prefetch >2MB should be reduced
Inappropriate typeWarningResource type/size not suitable (video, large image/script)
Not loadedInfoPrefetch hint present but resource not loaded yet
No prefetch hintsInfoNo rel="prefetch" on the page, so there was nothing to validate

(*) Note on “Wrong hint type” detection: This check uses a 5-second heuristic (resources loaded in the first 5s are considered current-page resources). On very slow-loading pages (>5s), this may produce false positives. The heuristic works well for most pages but consider the page load time when evaluating this error.

Resource analysis

The snippet shows detailed information for each prefetched resource:

ColumnDescription
ResourceFilename of the prefetched resource
TypeResource type from as attribute
SizeDecompressed size in KB (from Performance API)
DurationDownload time in milliseconds (from Performance API)
StatusWhether resource was loaded or is pending (valid prefetch table only)
CacheCache status - cached vs network fetch (valid prefetch table only)
IssueSpecific problems detected (issues tables only)

Thresholds

MetricWarningCriticalRationale
Resource count>10 resources-Excessive prefetch wastes mobile bandwidth
Individual file size>500KB-Large files may not be used immediately
Total prefetch size>2MB>5MBMobile data consumption and network impact

Real-world examples

Common prefetch scenarios

ScenarioRecommendationWhen to UseProbability
Multi-step Forms Prefetch next stepUser filling current stepHigh (>80%)
Product Listings Prefetch on hover/intent onlyUser shows interest signalMedium
Article Pages Prefetch related articlesAnalytics show 60%+ click rateHigh
SPA Route Chunks Prefetch visible link targetsFramework handles automaticallyVariable
Dashboard Widgets Prefetch after initial renderUser will likely interactHigh
Image Galleries Prefetch on viewport proximityUser scrolling towards contentMedium
Search Results Don’t prefetch all resultsUnknown user intentLow
Video Content Too large for prefetchUse adaptive streaming insteadN/A

Good: Next.js route prefetching

<!-- Next.js automatically prefetches visible <Link> components --> <link rel="prefetch" href="/_next/static/chunks/pages/checkout-a1b2c3.js" as="script" /> <link rel="prefetch" href="/_next/static/chunks/pages/products-d4e5f6.js" as="script" /> <!-- Small scripts for likely next pages - excellent use of prefetch -->

Good: Predictable user journey

<!-- Multi-step form: prefetch step 2 while user fills step 1 --> <link rel="prefetch" href="/step-2-validation.js" as="script" /> <link rel="prefetch" href="/step-2-styles.css" as="style" /> <!-- User intent is clear, resources are small -->

Bad: Prefetching current page hero image

<!-- WRONG: Hero image is needed NOW, not for future navigation --> <link rel="prefetch" href="/hero-image.jpg" as="image" /> <!-- CORRECT: Use preload for current page critical resources --> <link rel="preload" href="/hero-image.jpg" as="image" fetchpriority="high" />

Bad: Excessive prefetch without user intent

<!-- Prefetching 20+ product images "just in case" --> <link rel="prefetch" href="/product-1.jpg" /> <link rel="prefetch" href="/product-2.jpg" /> <!-- ... 18 more ... --> <!-- PROBLEM: Wastes bandwidth, user may not visit product pages --> <!-- SOLUTION: Use Intersection Observer to prefetch when user scrolls near links -->

Good: Analytics-driven prefetch

// Prefetch based on analytics: 80% of users go to /pricing after landing if (currentPage === "/features") { const link = document.createElement("link"); link.rel = "prefetch"; link.href = "/pricing-page.js"; link.as = "script"; document.head.appendChild(link); }

Framework-specific guidance

Next.js

Next.js automatically prefetches routes for <Link> components in the viewport. This is generally good, but watch for:

// Check prefetch behavior <Link href="/heavy-page" prefetch={false}> Heavy Page </Link> // Disable prefetch for pages with large resources // User must click before loading begins

Remix / React router

// Intentional prefetch on hover <Link to="/profile" prefetch="intent"> Profile </Link> // Prefetch immediately <Link to="/dashboard" prefetch="render"> Dashboard </Link> // Only prefetch when user hovers/focuses (recommended)

SvelteKit

<!-- Default: prefetch on hover --> <a href="/about" data-sveltekit-preload-data="hover">About</a> <!-- Prefetch immediately when link is visible --> <a href="/dashboard" data-sveltekit-preload-data="viewport">Dashboard</a> <!-- Disable prefetch for heavy pages --> <a href="/gallery" data-sveltekit-preload-data="off">Gallery</a>

Advanced: Runtime prefetch strategies

Adaptive prefetch decision flow

Adaptive prefetch flow: skip all prefetching when data saver is enabled, otherwise prefetch aggressively on 4g, selectively on 3g and not at all on 2g or slow-2gConnections: User on page to Check saveData; Check saveData to Skip all prefetch (Enabled); Check saveData to Check effectiveType (Disabled); Check effectiveType to Prefetch aggressively (4g); Check effectiveType to Prefetch selectively (3g); Check effectiveType to Skip prefetch (2g/slow-2g); Prefetch aggressively to Load 5-10 resources; Prefetch selectively to Load 1-3 most likely resources.User on pageCheck saveDataSkip all prefetchCheck effectiveTypePrefetch aggressivelyPrefetch selectivelySkip prefetchLoad 5-10 resourcesLoad 1-3 most likely resourcesEnabledDisabled4g3g2g/slow-2g
Adaptive prefetch flow: skip all prefetching when data saver is enabled, otherwise prefetch aggressively on 4g, selectively on 3g and not at all on 2g or slow-2gConnections: User on page to Check saveData; Check saveData to Skip all prefetch (Enabled); Check saveData to Check effectiveType (Disabled); Check effectiveType to Prefetch aggressively (4g); Check effectiveType to Prefetch selectively (3g); Check effectiveType to Skip prefetch (2g/slow-2g); Prefetch aggressively to Load 5-10 resources; Prefetch selectively to Load 1-3 most likely resources.User on pageChecksaveDataSkip all prefetchCheckeffectiveTypePrefetchaggressivelyPrefetchselectivelySkip prefetchLoad 5-10resourcesLoad 1-3 mostlikely resourcesEnabledDisabled4g3g2g/slow-2g

Adaptive prefetch based on connection

// Only prefetch on fast connections if ("connection" in navigator) { const conn = navigator.connection; const shouldPrefetch = conn.effectiveType === "4g" && !conn.saveData; if (shouldPrefetch) { // Prefetch next page resources const link = document.createElement("link"); link.rel = "prefetch"; link.href = "/next-page.js"; document.head.appendChild(link); } }

Intersection observer for smart prefetch

// Prefetch when user scrolls near a link const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const link = entry.target; const prefetch = document.createElement("link"); prefetch.rel = "prefetch"; prefetch.href = link.href; document.head.appendChild(prefetch); observer.unobserve(link); } }); }, { rootMargin: "200px" }, ); // Observe important navigation links document.querySelectorAll("a.important-nav").forEach((link) => { observer.observe(link); });

Impact on Core Web Vitals

How prefetch affects page load timeline

Sequence comparing excessive prefetch, which congests the network and delays critical resources, with smart prefetch, which waits until the page is interactive and stores a few resources in the disk cacheSteps: Note: Page Load Starts; Browser to Network: Request HTML (High Priority); Browser to Network: Request CSS (High Priority); Browser to Network: Request LCP Image (High Priority); alt With Excessive Prefetch; Browser to Network: Request 15 prefetch resources (Lowest Priority); Note: Bandwidth congestion on slow connections; Network to Browser: Delayed critical resources; alt With Smart Prefetch; Note: Wait for page interactive; Browser to Network: Request 3 prefetch resources (Lowest Priority); Note: Minimal impact on critical resources; Network to Cache: Store in disk cache; Note: Page Interactive.Page Load StartsRequest HTML (High Priority)Request CSS (High Priority)Request LCP Image (High Priority)alt[With Excessive Prefetch]Request 15 prefetch resources (Lowest Priority)Bandwidth congestion on slow connectionsDelayed critical resourcesalt[With Smart Prefetch]Wait for page interactiveRequest 3 prefetch resources (Lowest Priority)Minimal impact on critical resourcesStore in disk cachePage InteractiveBrowserNetworkCache

Potential negative impacts

  1. LCP (Largest Contentful Paint):

    • Prefetch competes with current page resources at network level
    • On slow connections, can delay LCP image/text
    • Monitor: Keep total prefetch <2MB
  2. FCP (First Contentful Paint):

    • Excessive prefetch during page load can delay FCP
    • Best practice: Delay prefetch until after page is interactive
  3. CLS (Cumulative Layout Shift):

    • Indirect impact: If prefetch delays critical fonts, can cause layout shifts
    • Ensure fonts for current page use preload, not prefetch

Mobile considerations

  • Data saver mode: Respect navigator.connection.saveData
  • Slow connections: Check navigator.connection.effectiveType
  • Battery impact: Prefetch uses CPU and radio, draining battery
  • Data costs: Mobile users may pay per MB
// Respect user preferences if (navigator.connection?.saveData) { // Skip all prefetch console.log("Data saver enabled, skipping prefetch"); return; } // Only prefetch on good connections const goodConnection = navigator.connection?.effectiveType === "4g"; if (!goodConnection) { console.log("Connection too slow for prefetch"); return; }

When prefetch vs preload?

Use rel="preload" when

  • Resource is needed for current page
  • Resource is critical for LCP or FCP
  • You want high priority loading
  • Resource will be used within seconds
<!-- Current page critical resources --> <link rel="preload" href="/hero-image.webp" as="image" fetchpriority="high" /> <link rel="preload" href="/critical-font.woff2" as="font" crossorigin /> <link rel="preload" href="/critical-styles.css" as="style" />

Use rel="prefetch" when

  • Resource is for future navigation
  • User journey is predictable
  • You want low priority loading (idle time)
  • Resource won’t be used for several seconds
<!-- Future page resources --> <link rel="prefetch" href="/next-page.js" as="script" /> <link rel="prefetch" href="/dashboard-styles.css" as="style" />

Quick decision tree

Decision tree for choosing a resource hint: preload when the current page needs the resource, prefetch when the user is more than 50% likely to navigate to a page that needs it, otherwise no hintConnections: Is resource needed on current page? to Answer; Answer to Use rel='preload' (YES); Answer to Is user likely to navigate to page that needs it? (NO); Is user likely to navigate to page that needs it? to Probability?; Probability? to Use rel='prefetch' (>50%); Probability? to Don't use resource hints (<50%); Use rel='preload' to High priority, memory cache; Use rel='prefetch' to Low priority, disk cache; Don't use resource hints to Let browser handle naturally.Is resource needed on currentpage?AnswerUse rel='preload'Is user likely to navigate to pagethat needs it?Probability?Use rel='prefetch'Don't use resource hintsHigh priority, memory cacheLow priority, disk cacheLet browser handle naturallyYESNO>50%<50%
Decision tree for choosing a resource hint: preload when the current page needs the resource, prefetch when the user is more than 50% likely to navigate to a page that needs it, otherwise no hintConnections: Is resource needed on current page? to Answer; Answer to Use rel='preload' (YES); Answer to Is user likely to navigate to page that needs it? (NO); Is user likely to navigate to page that needs it? to Probability?; Probability? to Use rel='prefetch' (>50%); Probability? to Don't use resource hints (<50%); Use rel='preload' to High priority, memory cache; Use rel='prefetch' to Low priority, disk cache; Don't use resource hints to Let browser handle naturally.Is resourceneeded oncurrent page?AnswerUse rel='preload'Is user likely tonavigate to pagethat needs it?Probability?Use rel='prefetch'Don't useresource hintsHigh priority,memory cacheLow priority, diskcacheLet browserhandle naturallyYESNO>50%<50%

Prefetch resource lifecycle

Lifecycle of a prefetched resource: it is discovered, queued and downloaded, then checked against its cache headers and stored in or dropped from the disk cache; a stored resource is promoted to the memory cache on the next navigation and used, while a queued or downloading resource can be cancelledConnections: Start to Discovered (Browser finds prefetch hint); Discovered to Queued (Added to prefetch queue (lowest priority)); Queued to Downloading (Browser idle time starts); Downloading to Disk cache entry (Resource downloaded); Disk cache entry to CheckCache (Check cache headers); CheckCache to Stored (Good cache headers); CheckCache to Discarded (No-cache headers); Stored to NextPage (User navigates to next page); NextPage to MemoryCache (Resource promoted to memory); MemoryCache to Used (Page consumes resource); Queued to Cancelled (User navigates before download); Downloading to Cancelled (Network congestion or navigation).DiskCacheStartDiscoveredQueuedDownloadingCancelledDisk cache entryCheckCacheStoredDiscardedNextPageMemoryCacheUsedBrowser finds prefetch hintAdded to prefetch queue (lowestpriority)Browser idle time startsResource downloadedUser navigates to next pageResource promoted to memoryPage consumes resourceUser navigates before downloadNetwork congestion ornavigationCheck cache headersGood cache headersNo-cache headers
Lifecycle of a prefetched resource: it is discovered, queued and downloaded, then checked against its cache headers and stored in or dropped from the disk cache; a stored resource is promoted to the memory cache on the next navigation and used, while a queued or downloading resource can be cancelledConnections: Start to Discovered (Browser finds prefetch hint); Discovered to Queued (Added to prefetch queue (lowest priority)); Queued to Downloading (Browser idle time starts); Downloading to Disk cache entry (Resource downloaded); Disk cache entry to CheckCache (Check cache headers); CheckCache to Stored (Good cache headers); CheckCache to Discarded (No-cache headers); Stored to NextPage (User navigates to next page); NextPage to MemoryCache (Resource promoted to memory); MemoryCache to Used (Page consumes resource); Queued to Cancelled (User navigates before download); Downloading to Cancelled (Network congestion or navigation).DiskCacheStartDiscoveredQueuedDownloadingCancelledDisk cache entryCheckCacheStoredDiscardedNextPageMemoryCacheUsedBrowser findsprefetch hintAdded to prefetchqueue (lowestpriority)Browser idle timestartsResourcedownloadedUser navigates tonext pageResource promotedto memoryPage consumesresourceUser navigatesbefore downloadNetwork congestionor navigationCheck cacheheadersGood cache headersNo-cache headers

Browser support

The snippet needs these features to run.

FeatureChromeEdgeFirefoxSafari
PerformanceResourceTiming.transferSize (opens in a new tab) 54 17 45 16.4

The snippet reports on these features. It runs without them, and its advice on each one applies where it is supported.

FeatureChromeEdgeFirefoxSafari
<link rel=“prefetch”> (opens in a new tab) 8 12 2

Source: MDN browser compatibility data (opens in a new tab), version 8.1.4.

  • Chrome and Edge prefetch during idle time.
  • Firefox prefetches in the background.
  • Mobile browsers may ignore prefetch on slow connections or with data saver enabled.

Cache requirements

Prefetched resources MUST have proper cache headers to be effective:

✅ Good: Resource will be reused Cache-Control: public, max-age=31536000, immutable ✅ Good: Resource can be revalidated Cache-Control: public, max-age=3600 ❌ Bad: Resource won't be cached Cache-Control: no-cache, no-store

Verify cache headers

Note: The following is a standalone example to check cache headers manually. It is NOT part of the diagnostic snippet above (it makes a real network request).

// Standalone example: Check if a specific resource has good cache headers // This makes a real HEAD request to inspect headers fetch("/prefetched-script.js", { method: "HEAD" }).then((res) => { const cacheControl = res.headers.get("cache-control"); console.log("Cache-Control:", cacheControl); if (cacheControl?.includes("no-cache")) { console.warn("⚠️ Prefetch won't be effective - no caching"); } });

Further reading

Last updated on
MIT 2026 © Joan León | @nucliweb