# Resource hints

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

Analyzes resource hints on the page, checking for proper usage and identifying optimization opportunities. Resource hints help the browser prioritize resource loading for better performance.

### Resource hint types

| Hint              | Purpose                           | When to Use                                   | Priority |
| ----------------- | --------------------------------- | --------------------------------------------- | -------- |
| **preload**       | Load resource for current page    | Critical CSS, fonts, hero images, LCP         | Highest  |
| **modulepreload** | Preload ES modules                | Critical JavaScript modules                   | High     |
| **preconnect**    | Establish early connection        | Third-party origins you'll fetch from         | High     |
| **dns-prefetch**  | Resolve DNS early                 | Third-party origins (fallback for preconnect) | Medium   |
| **prefetch**      | Load resource for next navigation | Resources needed on likely next page          | Low      |
| **prerender**     | Pre-render entire page            | High-probability next navigation              | Lowest   |

### Loading priority

```
preload/modulepreload → preconnect → dns-prefetch → prefetch → prerender
     (current page)                                    (future navigation)
```

### Common mistakes

| Mistake                            | Impact                   | Solution                              |
| ---------------------------------- | ------------------------ | ------------------------------------- |
| Preload without `as`               | Browser can't prioritize | Add `as="script"`, `as="style"`, etc. |
| Font preload without `crossorigin` | Double fetch             | Always add `crossorigin` for fonts    |
| Too many preloads (> 5-6)          | Bandwidth competition    | Prioritize only critical resources    |
| Unused preloads                    | Wasted bandwidth         | Remove or verify usage                |
| Preconnect without usage           | Wasted connection        | Only preconnect to used origins       |

> **Note:** **Tip:** Preload is for resources needed in the current page. Prefetch is for resources needed in future navigations. Don't confuse them!

### Why preconnect helps

`preconnect` pays off when a resource lives on a third-party origin: it moves the connection setup (DNS, TCP, TLS) off the critical path, so the request can start as soon as the resource is discovered. Step through the scenarios to compare connection reuse across HTTP versions and see what `preconnect` saves:

Interactive demo: [Interactive network waterfall comparing HTTP/1.1 with Connection: close, HTTP/1.1 keep-alive, HTTP/2, and HTTP/2 with preconnect](https://webperf-snippets.net/demos/network-waterfall.html). Switch between HTTP/1.1, keep-alive, HTTP/2, and HTTP/2 + preconnect to compare how each affects the request waterfall.

### Snippet

```js
// Resource Hints Analysis
// 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

  // @shared formatBytes
  function formatBytes(bytes) {
    if (bytes === null || bytes === undefined || Number.isNaN(bytes)) return "-";
    if (bytes === 0) return "0 B";
    const units = ["B", "KB", "MB", "GB"];
    const i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1));
    return (bytes / Math.pow(1024, i)).toFixed(1) + " " + units[i];
  }
  // @end-shared formatBytes

  // Resource hint definitions
  const hintTypes = [
    {
      rel: "preload",
      icon: "⚡",
      description: "Critical resource for current page",
      requiresAs: true,
    },
    {
      rel: "modulepreload",
      icon: "📦",
      description: "ES module for current page",
      requiresAs: false,
    },
    {
      rel: "preconnect",
      icon: "🔌",
      description: "Early connection to origin",
      requiresAs: false,
    },
    {
      rel: "dns-prefetch",
      icon: "🔍",
      description: "DNS lookup for origin",
      requiresAs: false,
    },
    {
      rel: "prefetch",
      icon: "📥",
      description: "Resource for future navigation",
      requiresAs: false,
    },
    {
      rel: "prerender",
      icon: "🖼️",
      description: "Pre-render page",
      requiresAs: false,
    },
  ];

  // Get all link elements with resource hints
  const allHints = [];
  const issues = [];

  hintTypes.forEach((type) => {
    const links = document.querySelectorAll(`link[rel="${type.rel}"]`);
    links.forEach((link) => {
      const href = link.href || link.getAttribute("href") || "";
      let hostname = "";
      try {
        hostname = new URL(href, location.origin).hostname;
      } catch {}

      const hint = {
        rel: type.rel,
        icon: type.icon,
        href,
        shortHref: href.length > 50 ? "..." + href.slice(-47) : href,
        hostname,
        as: link.getAttribute("as") || "",
        crossorigin: link.crossOrigin || link.getAttribute("crossorigin") || "",
        fetchpriority: link.fetchPriority || link.getAttribute("fetchpriority") || "",
        type: link.type || "",
        media: link.media || "",
        element: link,
      };

      allHints.push(hint);

      // Check for issues
      if (type.requiresAs && !hint.as) {
        issues.push({
          hint,
          issue: "Missing 'as' attribute (required for preload)",
          severity: "error",
          fix: `Add as="script|style|font|image|fetch"`,
        });
      }

      if (hint.as === "font" && !hint.crossorigin) {
        issues.push({
          hint,
          issue: "Font preload without crossorigin (causes double fetch)",
          severity: "error",
          fix: 'Add crossorigin="anonymous"',
        });
      }

      if (type.rel === "preconnect" && !hint.crossorigin) {
        // Check if we load CORS resources from this origin
        const corsResources = performance
          .getEntriesByType("resource").filter((e) => !isOwnRequest(e))
          .filter(
            (r) =>
              r.name.includes(hostname) &&
              (r.initiatorType === "script" ||
                r.initiatorType === "css" ||
                r.initiatorType === "font")
          );
        if (corsResources.length > 0) {
          issues.push({
            hint,
            issue: "Preconnect may need crossorigin for CORS resources",
            severity: "warning",
            fix: 'Consider adding crossorigin="anonymous"',
          });
        }
      }
    });
  });

  // Check for unused preloads
  const preloads = allHints.filter((h) => h.rel === "preload");
  const loadedResources = performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e));

  preloads.forEach((preload) => {
    const wasUsed = loadedResources.some((r) => r.name === preload.href);
    if (!wasUsed) {
      issues.push({
        hint: preload,
        issue: "Preloaded resource not used within page load",
        severity: "warning",
        fix: "Remove unused preload or verify it's needed",
      });
    }
  });

  // Check for preconnects without loaded resources
  const preconnects = allHints.filter(
    (h) => h.rel === "preconnect" || h.rel === "dns-prefetch"
  );
  preconnects.forEach((pc) => {
    const hasResources = loadedResources.some((r) => {
      try {
        return new URL(r.name).hostname === pc.hostname;
      } catch {
        return false;
      }
    });
    if (!hasResources && pc.hostname) {
      issues.push({
        hint: pc,
        issue: `No resources loaded from ${pc.hostname}`,
        severity: "warning",
        fix: "Remove unused preconnect or verify it's needed for future resources",
      });
    }
  });

  // Check for too many preloads
  if (preloads.length > 6) {
    issues.push({
      hint: { rel: "preload", href: `${preloads.length} preloads` },
      issue: `Too many preloads (${preloads.length}) - they compete for bandwidth`,
      severity: "warning",
      fix: "Limit to 5-6 most critical resources",
    });
  }

  // Get third-party origins that could benefit from preconnect
  const thirdPartyOrigins = new Map();
  // Third-party resources without Timing-Allow-Origin report every size as 0
  let sizeUnknownCount = 0;
  loadedResources.forEach((r) => {
    try {
      const url = new URL(r.name);
      if (url.hostname !== location.hostname) {
        if (!thirdPartyOrigins.has(url.origin)) {
          thirdPartyOrigins.set(url.origin, { count: 0, size: 0, types: new Set() });
        }
        const data = thirdPartyOrigins.get(url.origin);
        data.count++;
        data.size += r.transferSize || 0;
        if (r.transferSize === 0 && r.encodedBodySize === 0 && r.decodedBodySize === 0 && r.duration > 0) sizeUnknownCount++;
        data.types.add(r.initiatorType);
      }
    } catch {}
  });

  // Find origins without preconnect
  const preconnectedOrigins = new Set(
    [...preconnects].map((p) => {
      try {
        return new URL(p.href).origin;
      } catch {
        return "";
      }
    })
  );

  const missingPreconnects = [];
  thirdPartyOrigins.forEach((data, origin) => {
    if (!preconnectedOrigins.has(origin) && data.count >= 2) {
      missingPreconnects.push({ origin, ...data });
    }
  });

  // Display results
  console.group("%c🔗 Resource Hints Analysis", "font-weight: bold; font-size: 14px;");

  // Summary
  console.log("");
  console.log("%cSummary:", "font-weight: bold;");

  const summary = hintTypes.map((type) => {
    const count = allHints.filter((h) => h.rel === type.rel).length;
    return { type: type.rel, count, icon: type.icon };
  });

  summary.forEach((s) => {
    const status = s.count > 0 ? "🟢" : "⚪";
    console.log(`   ${status} ${s.icon} ${s.type}: ${s.count}`);
  });

  console.log(`   Total hints: ${allHints.length}`);

  // Details by type
  hintTypes.forEach((type) => {
    const hints = allHints.filter((h) => h.rel === type.rel);
    if (hints.length === 0) return;

    console.log("");
    console.group(
      `%c${type.icon} ${type.rel} (${hints.length})`,
      "color: #3b82f6; font-weight: bold;"
    );

    const tableData = hints.map((h) => {
      const row = {
        Resource: h.shortHref,
      };

      if (type.rel === "preload" || type.rel === "modulepreload") {
        row.As = h.as || "⚠️ missing";
        row.Crossorigin = h.crossorigin || "-";
        if (h.fetchpriority) row.Priority = h.fetchpriority;

        // Check if used
        const used = loadedResources.some((r) => r.name === h.href);
        row.Used = used ? "✅" : "⚠️ No";
      }

      if (type.rel === "preconnect" || type.rel === "dns-prefetch") {
        row.Origin = h.hostname;
        row.Crossorigin = h.crossorigin || "-";

        // Check resources from this origin
        const originResources = loadedResources.filter((r) => {
          try {
            return new URL(r.name).hostname === h.hostname;
          } catch {
            return false;
          }
        });
        row["Resources Loaded"] = originResources.length;
      }

      return row;
    });

    console.table(tableData);

    // Elements for inspection
    console.log("%c🔎 Elements:", "font-weight: bold;");
    hints.forEach((h, i) => {
      console.log(`${i + 1}.`, h.element);
    });

    console.groupEnd();
  });

  // Issues
  const errors = issues.filter((i) => i.severity === "error");
  const warnings = issues.filter((i) => i.severity === "warning");

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

    if (errors.length > 0) {
      console.log("");
      console.log("%c🔴 Errors:", "font-weight: bold; color: #ef4444;");
      errors.forEach((issue) => {
        console.log(`   ${issue.hint.rel}: ${issue.issue}`);
        console.log(`      Resource: ${issue.hint.href}`);
        console.log(`      Fix: ${issue.fix}`);
      });
    }

    if (warnings.length > 0) {
      console.log("");
      console.log("%c🟡 Warnings:", "font-weight: bold; color: #f59e0b;");
      warnings.forEach((issue) => {
        console.log(`   ${issue.hint.rel}: ${issue.issue}`);
        if (issue.hint.href) {
          console.log(`      Resource: ${issue.hint.href}`);
        }
        console.log(`      Fix: ${issue.fix}`);
      });
    }

    console.groupEnd();
  } else {
    console.log("");
    console.log(
      "%c✅ No issues found with resource hints.",
      "color: #22c55e; font-weight: bold;"
    );
  }

  // Recommendations
  if (missingPreconnects.length > 0) {
    console.log("");
    console.group("%c💡 Optimization Opportunities", "color: #3b82f6; font-weight: bold;");

    console.log("");
    console.log("%cThird-party origins without preconnect:", "font-weight: bold;");
    console.log("These origins load multiple resources and could benefit from preconnect:");
    console.log("");

    const recTable = missingPreconnects
      .sort((a, b) => b.count - a.count)
      .slice(0, 5)
      .map((o) => ({
        Origin: o.origin,
        Resources: o.count,
        Size: formatBytes(o.size),
        Types: Array.from(o.types).join(", "),
      }));
    console.table(recTable);

    console.log("");
    console.log("%cAdd preconnect for critical origins:", "font-weight: bold;");
    missingPreconnects.slice(0, 3).forEach((o) => {
      console.log(
        `%c<link rel="preconnect" href="${o.origin}" crossorigin>`,
        "font-family: monospace; color: #22c55e;"
      );
    });

    console.groupEnd();
  }

  // Best practices
  console.log("");
  console.group("%c📝 Best Practices", "color: #3b82f6; font-weight: bold;");
  console.log("");
  console.log("%cPreload (current page critical resources):", "font-weight: bold;");
  console.log(
    '%c<link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin>',
    "font-family: monospace; color: #22c55e;"
  );
  console.log(
    '%c<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">',
    "font-family: monospace; color: #22c55e;"
  );
  console.log("");
  console.log("%cPreconnect (third-party origins):", "font-weight: bold;");
  console.log(
    '%c<link rel="preconnect" href="https://fonts.googleapis.com">',
    "font-family: monospace; color: #22c55e;"
  );
  console.log(
    '%c<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',
    "font-family: monospace; color: #22c55e;"
  );
  console.log("");
  console.log("%cPrefetch (next page resources):", "font-weight: bold;");
  console.log(
    '%c<link rel="prefetch" href="/next-page.js" as="script">',
    "font-family: monospace; color: #22c55e;"
  );
  console.groupEnd();

  console.groupEnd();

  // The list keeps 50 hints: those with an error or a warning first, then in document order; count and details cover all of them
  const MAX_ITEMS = 50;
  const hintRank = (h) => (issues.some((i) => i.hint === h && i.severity === "error") ? 0 : issues.some((i) => i.hint === h) ? 1 : 2);
  const rankedHints = allHints
    .map((h, index) => ({ h, index }))
    .sort((a, b) => hintRank(a.h) - hintRank(b.h) || a.index - b.index)
    .map(({ h }) => h);
  return {
    script: "Resource-Hints",
    status: "ok",
    count: allHints.length,
    corsLimitedAnalysis: sizeUnknownCount > 0,
    details: {
      sizeUnknownCount,
      byType: Object.fromEntries(hintTypes.map(t => [t.rel, allHints.filter(h => h.rel === t.rel).length])),
      missingPreconnectsCount: missingPreconnects.length,
      missingPreconnects: missingPreconnects.slice(0, 20).map((o) => ({
        origin: o.origin,
        requestCount: o.count,
        sizeBytes: o.size,
        resourceTypes: Array.from(o.types),
        recommendedHint: "preconnect",
      })),
    },
    items: rankedHints.slice(0, MAX_ITEMS).map(h => ({ rel: h.rel, href: h.shortHref, as: h.as, crossorigin: h.crossorigin, fetchpriority: h.fetchpriority })),
    issues: issues.map(i => ({ severity: i.severity, message: `${i.hint.rel}: ${i.issue}. Fix: ${i.fix}` })),
  };
})();
```

### Understanding the results

### Summary section

- Count of each resource hint type
- Green (🟢) = hints present, Grey (⚪) = no hints of this type

### Details by type

For **preload/modulepreload**:

| Column      | Description                                |
| ----------- | ------------------------------------------ |
| Resource    | URL (truncated if long)                    |
| As          | Resource type (script, style, font, image) |
| Crossorigin | CORS setting                               |
| Priority    | fetchpriority if set                       |
| Used        | Whether the resource was actually loaded   |

For **preconnect/dns-prefetch**:

| Column           | Description                         |
| ---------------- | ----------------------------------- |
| Origin           | Domain being connected to           |
| Crossorigin      | CORS setting                        |
| Resources Loaded | Count of resources from this origin |

### Issues detected

| Issue                                     | Severity | Impact                             |
| ----------------------------------------- | -------- | ---------------------------------- |
| Preload missing `as`                      | Error    | Browser can't prioritize correctly |
| Font preload without `crossorigin`        | Error    | Resource fetched twice             |
| Preconnect without `crossorigin` for CORS | Warning  | Connection may not be reused       |
| Unused preload                            | Warning  | Wasted bandwidth                   |
| Preconnect without resources              | Warning  | Wasted connection                  |
| Too many preloads (> 6)                   | Warning  | Bandwidth competition              |

### Optimization opportunities

- Third-party origins loading multiple resources without preconnect
- Suggested `<link>` tags to add

### Priority guidelines

| Resource Type       | Suggested Hint                        |
| ------------------- | ------------------------------------- |
| LCP image           | `preload` with `fetchpriority="high"` |
| Critical fonts      | `preload` with `crossorigin`          |
| Critical CSS        | `preload`                             |
| Third-party scripts | `preconnect` to origin                |
| Below-fold images   | None (use `loading="lazy"`)           |
| Next page resources | `prefetch`                            |

### 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="preload">](https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/rel/preload)             |  ✅ 50  | ✅ 79 |   ✅ 85  | ✅ 11.1 |
| [\<link rel="preconnect">](https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/rel/preconnect)       |  ✅ 46  | ✅ 79 |   ✅ 39  | ✅ 11.1 |
| [\<link rel="dns-prefetch">](https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/rel/dns-prefetch)   |  ✅ 46  | ✅ 79 |  ✅ 127  |   ✅ 5  |
| [\<link rel="modulepreload">](https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/rel/modulepreload) |  ✅ 66  | ✅ 79 |  ✅ 115  |  ✅ 17  |

Source: [MDN browser compatibility data](https://github.com/mdn/browser-compat-data), version 8.1.4.

### Further reading

- [Preload critical assets](https://web.dev/articles/preload-critical-assets) | web.dev
- [Establish network connections early](https://web.dev/articles/preconnect-and-dns-prefetch) | web.dev
- [Prefetch resources](https://developer.chrome.com/docs/devtools/application/debugging-speculation-rules) | Chrome Developers
- [Resource Hints specification](https://www.w3.org/TR/resource-hints/) | W3C
