# Priority hints audit

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

Audits all `fetchpriority` attribute usage across the page, covering non-image resources (scripts, preload links, iframes), `fetchpriority="low"` analysis, and conflicts between `preload` and low priority. Identifies the LCP candidate when it lacks `fetchpriority="high"`.

> **Note:** **Related:** Use the [Find Images With Lazy and Fetchpriority](https://webperf-snippets.net/Loading/Find-Images-With-Lazy-and-Fetchpriority.md) snippet to detect the specific `loading="lazy"` + `fetchpriority="high"` conflict on images.

### What this snippet checks

| Check                                              | Scope                             |
| -------------------------------------------------- | --------------------------------- |
| Non-image resources with `fetchpriority`           | `<script>`, `<link>`, `<iframe>`  |
| `<link rel="preload">` + `fetchpriority="low"`     | Contradictory combination         |
| `fetchpriority="low"` on early-loaded resources    | Possible misconfiguration         |
| `fetchpriority="high"` on late-loaded resources    | Investigate blocking dependencies |
| LCP candidate image without `fetchpriority="high"` | Missed LCP optimization           |

### Why `fetchpriority` needs validation

`fetchpriority` was introduced in 2022 and hints to the browser how to prioritize a resource fetch relative to others of the same type. Incorrect usage sends conflicting signals:

| Combination                                                   | Problem                                                                                |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `rel="preload"` + `fetchpriority="low"`                       | `preload` declares a resource as critical for the current page; `low` contradicts that |
| `fetchpriority="low"` on a resource loaded in the first 500ms | If it loads that early, the browser may already consider it critical                   |
| `fetchpriority="high"` on a resource loaded after 3s          | High priority did not help, investigate render-blocking dependencies                   |
| LCP image without `fetchpriority="high"`                      | Missed opportunity to improve Largest Contentful Paint                                 |

### See how priority reorders a load

Compare the same page with different `fetchpriority` values and watch when the LCP image finishes, and which of these combinations the snippet flags:

Interactive demo: [Interactive simulated waterfall of six requests sharing a 4 Mbps connection, comparing no fetchpriority hints, high on the LCP image, low on the other resources, both together, and a preload combined with low, with the time at which the LCP image finishes and the issues the snippet reports](https://webperf-snippets.net/demos/fetchpriority-waterfall.html). Switch scenarios and replay the load to see how the bandwidth is shared.

### Snippet

```js
// Priority Hints Audit
// 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

  function isInViewport(el) {
    const rect = el.getBoundingClientRect();
    return (
      rect.top < window.innerHeight &&
      rect.bottom > 0 &&
      rect.left < window.innerWidth &&
      rect.right > 0 &&
      rect.width > 0 &&
      rect.height > 0
    );
  }

  function findLcpCandidate() {
    const imgs = Array.from(document.querySelectorAll("img")).filter(isInViewport);
    let candidate = null;
    let maxArea = 0;
    imgs.forEach((img) => {
      const { width, height } = img.getBoundingClientRect();
      const area = width * height;
      if (area > maxArea) {
        maxArea = area;
        candidate = img;
      }
    });
    return candidate;
  }

  function shortUrl(el) {
    const src = el.src || el.href || el.getAttribute("href") || "";
    return src.split("/").pop()?.split("?")[0] || src.slice(-50) || "(unknown)";
  }

  // Build performance entry map for timing lookups
  const entryMap = new Map();
  performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e)).forEach((e) => {
    try {
      entryMap.set(new URL(e.name, location.origin).href, e);
    } catch {}
  });

  function getEntry(el) {
    const src = el.src || el.href || el.getAttribute("href") || "";
    if (!src) return null;
    try {
      return entryMap.get(new URL(src, location.origin).href) || null;
    } catch {
      return null;
    }
  }

  // All elements with fetchpriority
  const allElements = Array.from(document.querySelectorAll("[fetchpriority]"));
  // Exclude images — covered by "Find Images With Lazy and Fetchpriority"
  const nonImageElements = allElements.filter((el) => el.tagName !== "IMG");
  const images = allElements.filter((el) => el.tagName === "IMG");
  const preloadsWithPriority = Array.from(
    document.querySelectorAll('link[rel="preload"][fetchpriority]')
  );
  const lcpCandidate = findLcpCandidate();

  const issues = [];

  // Issue 1: <link rel="preload"> with fetchpriority="low" (contradictory)
  preloadsWithPriority
    .filter((link) => link.getAttribute("fetchpriority") === "low")
    .forEach((link) => {
      issues.push({
        severity: "error",
        element: link,
        resource: shortUrl(link),
        message: 'preload + fetchpriority="low" is contradictory',
        detail:
          'preload signals this resource is critical for the current page; fetchpriority="low" contradicts that.',
        fix: 'Remove fetchpriority="low", or use fetchpriority="high" if the resource is truly critical',
      });
    });

  // Issue 2: fetchpriority="low" on resources loaded in the first 500ms
  allElements
    .filter((el) => el.getAttribute("fetchpriority") === "low")
    .forEach((el) => {
      const entry = getEntry(el);
      if (entry && entry.startTime < 500) {
        issues.push({
          severity: "warning",
          element: el,
          resource: shortUrl(el),
          message: `fetchpriority="low" but loaded at ${entry.startTime.toFixed(0)}ms`,
          detail:
            "This resource loaded very early. If it's genuinely non-critical, the browser is deprioritizing it correctly. Otherwise, reconsider the priority.",
          fix: 'Verify the resource is non-critical, or remove fetchpriority="low"',
        });
      }
    });

  // Issue 3: fetchpriority="high" on resources loaded after 3000ms
  allElements
    .filter((el) => el.getAttribute("fetchpriority") === "high")
    .forEach((el) => {
      const entry = getEntry(el);
      if (entry && entry.startTime > 3000) {
        issues.push({
          severity: "warning",
          element: el,
          resource: shortUrl(el),
          message: `fetchpriority="high" but loaded at ${entry.startTime.toFixed(0)}ms`,
          detail:
            "High-priority resources should load early. A late start may indicate render-blocking dependencies or incorrect placement in the document.",
          fix: "Move the element earlier in the HTML or investigate render-blocking dependencies",
        });
      }
    });

  // Issue 4: LCP candidate image without fetchpriority="high"
  if (lcpCandidate && lcpCandidate.getAttribute("fetchpriority") !== "high") {
    const fp = lcpCandidate.getAttribute("fetchpriority");
    issues.push({
      severity: "warning",
      element: lcpCandidate,
      resource:
        (lcpCandidate.currentSrc || lcpCandidate.src).split("/").pop()?.split("?")[0] ||
        "LCP image",
      message: `LCP candidate image ${fp ? `has fetchpriority="${fp}"` : "has no fetchpriority set"}`,
      detail:
        'The largest visible image is likely the LCP element and benefits most from fetchpriority="high".',
      fix: 'Add fetchpriority="high" to the LCP image to improve Largest Contentful Paint',
    });
  }

  // Display results
  console.group("%c🎯 Priority Hints Audit", "font-weight: bold; font-size: 14px;");

  const highCount = allElements.filter(
    (el) => el.getAttribute("fetchpriority") === "high"
  ).length;
  const lowCount = allElements.filter(
    (el) => el.getAttribute("fetchpriority") === "low"
  ).length;
  const autoCount = allElements.filter(
    (el) => el.getAttribute("fetchpriority") === "auto"
  ).length;

  console.log("");
  console.log("%cInventory:", "font-weight: bold;");
  console.log(`   Total elements with fetchpriority: ${allElements.length}`);
  console.log(
    `     • Images: ${images.length} (see "Find Images With Lazy and Fetchpriority" for image-specific checks)`
  );
  console.log(`     • Non-image resources: ${nonImageElements.length}`);
  console.log(`     • Preloads with fetchpriority: ${preloadsWithPriority.length}`);
  console.log(
    `   Priority breakdown: high=${highCount}, low=${lowCount}, auto=${autoCount}`
  );

  if (allElements.length === 0 && !lcpCandidate) {
    console.log("");
    console.log(
      "%cℹ️ No fetchpriority attributes found on this page.",
      "color: #3b82f6;"
    );
    console.groupEnd();
    return { script: "Priority-Hints-Audit", status: "ok", count: 0, items: [], issues: [] };
  }

  // LCP candidate status
  if (lcpCandidate) {
    const fp = lcpCandidate.getAttribute("fetchpriority");
    const status =
      fp === "high"
        ? '✅ fetchpriority="high"'
        : fp
        ? `⚠️ fetchpriority="${fp}"`
        : "⚠️ No fetchpriority set";
    console.log("");
    console.log("%cLCP Candidate:", "font-weight: bold;");
    console.log(`   ${status}`);
    console.log("   Element:", lcpCandidate);
  }

  // Non-image resources table
  if (nonImageElements.length > 0) {
    console.log("");
    console.group(
      `%c📋 Non-Image Resources with fetchpriority (${nonImageElements.length})`,
      "font-weight: bold;"
    );

    console.table(
      nonImageElements.map((el) => {
        const entry = getEntry(el);
        const rel = el.getAttribute("rel") || "";
        const as = el.getAttribute("as") || "";
        const tag =
          el.tagName.toLowerCase() +
          (rel ? `[rel="${rel}"]` : "") +
          (as ? `[as="${as}"]` : "");
        return {
          Tag: tag,
          Priority: el.getAttribute("fetchpriority"),
          "Start (ms)": entry ? entry.startTime.toFixed(0) : "N/A",
          "Size (KB)":
            entry?.decodedBodySize
              ? (entry.decodedBodySize / 1024).toFixed(1)
              : "N/A",
          Resource: shortUrl(el),
        };
      })
    );

    console.log("%c🔎 Elements:", "font-weight: bold;");
    nonImageElements.forEach((el, i) => console.log(`${i + 1}.`, el));
    console.groupEnd();
  }

  // Preload links with fetchpriority
  if (preloadsWithPriority.length > 0) {
    console.log("");
    console.group(
      `%c🔗 Preload Links with fetchpriority (${preloadsWithPriority.length})`,
      "font-weight: bold;"
    );

    console.table(
      preloadsWithPriority.map((link) => {
        const fp = link.getAttribute("fetchpriority");
        const entry = getEntry(link);
        return {
          fetchpriority: fp,
          as: link.getAttribute("as") || "(missing)",
          Status:
            fp === "low" ? "⚠️ Conflict" : fp === "high" ? "✅ Good" : "ℹ️ Auto",
          "Start (ms)": entry ? entry.startTime.toFixed(0) : "N/A",
          Resource: shortUrl(link),
        };
      })
    );

    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;"
    );

    [...errors, ...warnings].forEach((issue) => {
      const color = issue.severity === "error" ? "#ef4444" : "#f59e0b";
      const icon = issue.severity === "error" ? "🔴" : "⚠️";
      console.log("");
      console.log(
        `%c${icon} ${issue.message}`,
        `font-weight: bold; color: ${color};`
      );
      console.log(`   ${issue.detail}`);
      console.log(`   Fix: ${issue.fix}`);
      console.log("   Element:", issue.element);
    });

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

  // Quick reference
  console.log("");
  console.group(
    "%c📝 fetchpriority Quick Reference",
    "color: #3b82f6; font-weight: bold;"
  );
  console.log("");
  console.log("%c  ✅ LCP hero image:", "color: #22c55e;");
  console.log(
    '%c  <img src="hero.webp" fetchpriority="high" alt="...">',
    "font-family: monospace;"
  );
  console.log("");
  console.log("%c  ✅ Preloaded critical font:", "color: #22c55e;");
  console.log(
    '%c  <link rel="preload" href="font.woff2" as="font" fetchpriority="high" crossorigin>',
    "font-family: monospace;"
  );
  console.log("");
  console.log("%c  ✅ Below-fold image:", "color: #22c55e;");
  console.log(
    '%c  <img src="content.jpg" loading="lazy" fetchpriority="low" alt="...">',
    "font-family: monospace;"
  );
  console.log("");
  console.log("%c  ❌ Contradictory — preload + low priority:", "color: #ef4444;");
  console.log(
    '%c  <link rel="preload" href="font.woff2" as="font" fetchpriority="low">',
    "font-family: monospace;"
  );
  console.groupEnd();

  console.log("");
  console.log(
    '%c💡 Related: "Find Images With Lazy and Fetchpriority" detects the lazy+high conflict on images. https://webperf-snippets.net/Loading/Find-Images-With-Lazy-and-Fetchpriority',
    "color: #6b7280;"
  );

  console.groupEnd();

  return {
    script: "Priority-Hints-Audit",
    status: "ok",
    count: allElements.length,
    details: {
      highCount,
      lowCount,
      autoCount,
      preloadsWithPriorityCount: preloadsWithPriority.length,
      lcpCandidateHasFetchpriority: lcpCandidate ? lcpCandidate.getAttribute("fetchpriority") === "high" : null,
    },
    items: allElements.map(el => ({
      tag: el.tagName.toLowerCase(),
      fetchpriority: el.getAttribute("fetchpriority"),
      resource: (el.src || el.href || el.getAttribute("href") || "").split("/").pop()?.split("?")[0] || "",
    })),
    issues: issues.map(i => ({
      severity: i.severity,
      message: i.fix ? `${i.message.replace(/\.$/, "")}. Fix: ${i.fix}` : i.message,
    })),
  };
})();
```

### Understanding the results

### Inventory section

| Field                       | Description                                           |
| --------------------------- | ----------------------------------------------------- |
| Total elements              | All elements with a `fetchpriority` attribute         |
| Images                      | Excluded from non-image checks (see related snippet)  |
| Non-image resources         | `<script>`, `<link>`, `<iframe>` with `fetchpriority` |
| Preloads with fetchpriority | `<link rel="preload">` that also set priority         |
| Priority breakdown          | Count of `high`, `low`, and `auto` values             |

### Non-image resources table

| Column     | Description                                              |
| ---------- | -------------------------------------------------------- |
| Tag        | Element tag and key attributes (`rel`, `as`)             |
| Priority   | The `fetchpriority` value set                            |
| Start (ms) | When the browser started fetching (from Performance API) |
| Size (KB)  | Decompressed resource size                               |
| Resource   | Filename extracted from the URL                          |

### Preload links table

| Column        | Description                                     |
| ------------- | ----------------------------------------------- |
| fetchpriority | Value set on the preload link                   |
| as            | Resource type (`font`, `image`, `script`, etc.) |
| Status        | `✅ Good` (high), `⚠️ Conflict` (low), `ℹ️ Auto` |
| Start (ms)    | Load start time from the Performance API        |
| Resource      | Filename                                        |

### Issues detected

| Issue                                        | Severity | Explanation                                          |
| -------------------------------------------- | -------- | ---------------------------------------------------- |
| `preload` + `fetchpriority="low"`            | Error    | Contradictory signals, remove or change to `high`    |
| `fetchpriority="low"` loaded in <500ms       | Warning  | Resource may be more critical than declared          |
| `fetchpriority="high"` loaded after 3000ms   | Warning  | High priority did not help, investigate dependencies |
| LCP candidate without `fetchpriority="high"` | Warning  | Missed opportunity to improve LCP                    |

### Best practices

**LCP image**, always set `fetchpriority="high"`:

```html
<img src="hero.webp" fetchpriority="high" alt="Hero image">
```

**Preloaded critical resource**, pair `preload` with `fetchpriority="high"`:

```html
<link rel="preload" href="critical-font.woff2" as="font" fetchpriority="high" crossorigin>
```

**Below-fold image**, `loading="lazy"` with optional `fetchpriority="low"`:

```html
<img src="gallery.jpg" loading="lazy" fetchpriority="low" alt="Gallery image">
```

### Never combine `preload` with `fetchpriority="low"`

```html
<!-- ❌ Contradictory -->
<link rel="preload" href="font.woff2" as="font" fetchpriority="low">

<!-- ✅ Correct -->
<link rel="preload" href="font.woff2" as="font" fetchpriority="high" crossorigin>
```

### Browser support

The snippet needs these features to run.

| Feature                                                                                                                           | Chrome | Edge | Firefox | Safari |
| --------------------------------------------------------------------------------------------------------------------------------- | :----: | :--: | :-----: | :----: |
| [Element.getBoundingClientRect](https://developer.mozilla.org/docs/Web/API/Element/getBoundingClientRect)                         |   ✅ 2  | ✅ 12 |   ✅ 3   |   ✅ 4  |
| [PerformanceResourceTiming.decodedBodySize](https://developer.mozilla.org/docs/Web/API/PerformanceResourceTiming/decodedBodySize) |  ✅ 54  | ✅ 17 |   ✅ 45  | ✅ 16.4 |
| All of the above                                                                                                                  |  ✅ 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 |
| --------------------------------------------------------------------------------------------------------- | :----: | :---: | :-----: | :----: |
| [\<img fetchpriority>](https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/fetchpriority)    |  ✅ 101 | ✅ 101 |  ✅ 132  | ✅ 17.2 |
| [\<link fetchpriority>](https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/fetchpriority)   |  ✅ 101 | ✅ 101 |  ✅ 132  | ✅ 17.2 |
| [\<script fetchpriority>](https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/fetchpriority) |  ✅ 101 | ✅ 101 |  ✅ 132  | ✅ 17.2 |

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

### Further reading

- [Optimizing resource loading with Priority Hints](https://web.dev/articles/fetch-priority) | web.dev
- [fetchpriority attribute](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) | MDN
- [Resource priorities in Chrome](https://web.dev/articles/resource-priority) | web.dev
- [Optimizing LCP](https://web.dev/articles/optimize-lcp) | web.dev
