# Prefetch resource validation

> Source: <https://webperf-snippets.net/Loading/Prefetch-Resource-Validation>

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

| Hint       | Priority | Timing            | Cache        | Use Case                                  |
| ---------- | -------- | ----------------- | ------------ | ----------------------------------------- |
| `preload`  | High     | Current page      | Memory cache | Critical resources for current page (LCP) |
| `prefetch` | Lowest   | Future navigation | Disk cache   | Resources for likely next pages           |

### Visual comparison

Diagram: 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 navigations

### Problems commonly detected

| Issue                      | Impact                   | Detection                                 |
| -------------------------- | ------------------------ | ----------------------------------------- |
| **Excessive prefetch**     | Wasted bandwidth         | More than 10-15 resources prefetched      |
| **Large files**            | Mobile data consumption  | Individual resources >500KB               |
| **Total size**             | Network congestion       | Cumulative prefetch >2MB                  |
| **Wrong resource types**   | Ineffective optimization | Large images/videos instead of code       |
| **Current page resources** | Wrong hint type          | Should use `preload` instead              |
| **Missing cache headers**  | Prefetch won't be used   | Resources without proper cache directives |

### Snippet

```js
// 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

| Category               | Severity | Description                                                              |
| ---------------------- | -------- | ------------------------------------------------------------------------ |
| **Wrong hint type**    | Error    | Resource used on current page - should use `preload` not `prefetch` (\*) |
| **Missing as**         | Warning  | Missing `as` attribute prevents correct MIME type matching               |
| **Invalid as**         | Warning  | Invalid `as` attribute value (typo or unsupported type)                  |
| **Duplicate prefetch** | Warning  | Same URL is prefetched multiple times (wasteful)                         |
| **Excessive count**    | Warning  | Too many prefetch hints (>10) wastes bandwidth                           |
| **Large file**         | Warning  | Individual resource >500KB may be too large                              |
| **Excessive size**     | Error    | Total prefetch >5MB significantly impacts mobile users                   |
| **High size**          | Warning  | Total prefetch >2MB should be reduced                                    |
| **Inappropriate type** | Warning  | Resource type/size not suitable (video, large image/script)              |
| **Not loaded**         | Info     | Prefetch hint present but resource not loaded yet                        |
| **No prefetch hints**  | Info     | No `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:

| Column       | Description                                                           |
| ------------ | --------------------------------------------------------------------- |
| **Resource** | Filename of the prefetched resource                                   |
| **Type**     | Resource type from `as` attribute                                     |
| **Size**     | Decompressed size in KB (from Performance API)                        |
| **Duration** | Download time in milliseconds (from Performance API)                  |
| **Status**   | Whether resource was loaded or is pending (valid prefetch table only) |
| **Cache**    | Cache status - cached vs network fetch (valid prefetch table only)    |
| **Issue**    | Specific problems detected (issues tables only)                       |

### Thresholds

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

### Real-world examples

### Common prefetch scenarios

| Scenario              | Recommendation                    | When to Use                     | Probability |
| --------------------- | --------------------------------- | ------------------------------- | ----------- |
| **Multi-step Forms**  | ✅ Prefetch next step              | User filling current step       | High (>80%) |
| **Product Listings**  | ⚠️ Prefetch on hover/intent only  | User shows interest signal      | Medium      |
| **Article Pages**     | ✅ Prefetch related articles       | Analytics show 60%+ click rate  | High        |
| **SPA Route Chunks**  | ✅ Prefetch visible link targets   | Framework handles automatically | Variable    |
| **Dashboard Widgets** | ✅ Prefetch after initial render   | User will likely interact       | High        |
| **Image Galleries**   | ⚠️ Prefetch on viewport proximity | User scrolling towards content  | Medium      |
| **Search Results**    | ❌ Don't prefetch all results      | Unknown user intent             | Low         |
| **Video Content**     | ❌ Too large for prefetch          | Use adaptive streaming instead  | N/A         |

### Good: Next.js route prefetching

```html
<!-- 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

```html
<!-- 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

```html
<!-- 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

```html
<!-- 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

```javascript
// 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:

```javascript
// 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

```javascript
// 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

```html
<!-- 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

Diagram: 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-2g

### Adaptive prefetch based on connection

```javascript
// 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

```javascript
// 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

Diagram: 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 cache

### 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

```javascript
// 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**

```html
<!-- 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**

```html
<!-- Future page resources -->
<link rel="prefetch" href="/next-page.js" as="script" />
<link rel="prefetch" href="/dashboard-styles.css" as="style" />
```

### Quick decision tree

Diagram: 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 hint

### Prefetch resource lifecycle

Diagram: 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 cancelled

### Browser support

The snippet needs these features to run.

| Feature                                                                                                                     | Chrome | Edge | Firefox | Safari |
| --------------------------------------------------------------------------------------------------------------------------- | :----: | :--: | :-----: | :----: |
| [PerformanceResourceTiming.transferSize](https://developer.mozilla.org/docs/Web/API/PerformanceResourceTiming/transferSize) |  ✅ 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.

| Feature                                                                                                 | Chrome | Edge | Firefox | Safari |
| ------------------------------------------------------------------------------------------------------- | :----: | :--: | :-----: | :----: |
| [\<link rel="prefetch">](https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/rel/prefetch) |   ✅ 8  | ✅ 12 |   ✅ 2   |    ❌   |

Source: [MDN browser compatibility data](https://github.com/mdn/browser-compat-data), 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:

```http
✅ 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).

```javascript
// 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

- [Resource Hints: `rel=prefetch`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/prefetch) | MDN
- [Resource Hints Specification](https://www.w3.org/TR/resource-hints/#prefetch) | W3C
- [Prefetching in Chrome](https://developer.chrome.com/docs/web-platform/prerender-pages) | Chrome Developers
- [Optimize resource loading with the Fetch Priority API](https://web.dev/articles/fetch-priority) | web.dev
- [Fast load times with prefetch](https://web.dev/articles/link-prefetch) | web.dev
- [Resource Hints comparison](https://www.debugbear.com/blog/resource-hints-rel-preload-prefetch-preconnect) | DebugBear
- [The Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API) | MDN
