Skip to Content
LoadingValidate Preload on Async/Defer Scripts

Validate preload on async/defer scripts

Chrome 43+ Edge 12+ Firefox 35+ Safari 11+ Details

Detects preload resource hints for scripts that use async or defer attributes. This is an anti-pattern that wastes bandwidth and can hurt performance by artificially elevating the priority of resources that should load at lower priority.

Why this matters

IssueImpactExplanation
Preload + async/deferWrong priority escalationElevates async/defer scripts from Low to Medium/High priority, competing with critical resources
Bandwidth competitionDelays LCPPreloaded scripts compete with CSS, fonts, and LCP images for bandwidth
Wasted bytesExtra requestsBrowser may fetch the resource twice if preload doesn’t match script attributes
False optimizationMaintenance burdenAdds complexity without performance benefit

Script loading priorities in Chrome

StrategyNetwork PriorityExecution PriorityBlocks ParserUse Case
<script> in <head>Medium/HighVeryHigh YesCritical scripts affecting FMP layout
<link rel=preload> + <script async>Medium/High HighBriefly Anti-pattern for most cases
<link rel=preload> + <script async fetchpriority=low>Lowest/LowHighBriefly Early discovery + low priority (valid)
<script async>Lowest/LowHighBrieflyIndependent scripts (analytics, ads)
<script defer>Lowest/LowVeryLow NoScripts needing DOM, running in order
<script> at end of <body>Medium/HighLowBrieflyLegacy pattern (use defer instead)
<script defer> at end of <body>Lowest/LowVeryLow NoNon-critical occasional features

Key insight: Adding preload to an async or defer script elevates it from Lowest/Low to Medium/High network priority, causing it to compete with critical resources. This is rarely what you want! Source: JavaScript Loading Priorities in Chrome (opens in a new tab) by Addy Osmani (opens in a new tab)

How async/defer work

Script loading comparison: regular script blocks HTML parsing during download and execution. Script with async downloads in parallel but pauses parsing during execution. Script with defer downloads in parallel and executes after parsing completes.

Diagram comparing script loading strategies. Source: JavaScript modules - module vs script | V8 (opens in a new tab)

Why not to preload async/defer scripts

  1. Async scripts naturally load at Lowest/Low network priority (good!)
  2. Defer scripts also load at Lowest/Low network priority (also good!)
  3. Adding preload escalates them to Medium/High priority (bad!)
  4. The result: They compete with critical CSS, fonts, and LCP images for bandwidth
  5. Performance impact: Can delay LCP by 100-500ms on slower connections

Priority escalation example

Without preload: ├── CSS: Medium/High priority ✅ ├── Fonts: Medium/High priority ✅ ├── LCP image: Medium/High priority ✅ └── async script: Lowest/Low priority ✅ · With preload on async script: ├── CSS: Medium/High priority (now competing) ⚠️ ├── Fonts: Medium/High priority (now competing) ⚠️ ├── LCP image: Medium/High priority (now competing) ⚠️ └── Preloaded async script: Medium/High priority ❌

Key insight: If a script has async or defer, it’s already declared as “not critical for initial render”. Preloading contradicts that by elevating its priority to compete with truly critical resources!

Snippet

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

// Validate Preload on Async/Defer Scripts
// 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 preloaded scripts
  const preloadedScripts = Array.from(
    document.querySelectorAll('link[rel="preload"][as="script"]'),
  );

  if (preloadedScripts.length === 0) {
    console.log(
      '%c✅ No script preloads found (rel="preload" as="script").',
      "color: #22c55e; font-weight: bold;",
    );
    console.log(
      "%cℹ️ This is fine - only preload scripts if they're critical and blocking.",
      "color: #3b82f6;",
    );
    return { script: "Validate-Preload-Async-Defer-Scripts", status: "ok", count: 0, items: [], issues: [] };
  }

  // Get performance entries for scripts
  const performanceScripts = performance
    .getEntriesByType("resource").filter((e) => !isOwnRequest(e))
    .filter((r) => r.initiatorType === "script" || r.initiatorType === "link");

  // Find corresponding script elements for each preload
  const issues = [];
  const validPreloads = [];
  const allScripts = Array.from(document.querySelectorAll("script[src]"));

  preloadedScripts.forEach((preload) => {
    const preloadHref = preload.href;
    const preloadUrl = new URL(preloadHref, location.origin).href;

    // Find matching script element
    const matchingScript = allScripts.find((script) => {
      try {
        const scriptUrl = new URL(script.src, location.origin).href;
        return scriptUrl === preloadUrl;
      } catch {
        return false;
      }
    });

    const shortUrl = preloadHref.split("/").pop()?.split("?")[0] || preloadHref;

    if (!matchingScript) {
      // Check if script was loaded via Performance API (dynamic import, etc.)
      const wasLoaded = performanceScripts.some((entry) => {
        try {
          // First try exact URL match
          if (entry.name === preloadUrl) return true;

          // For relative/partial matches, compare pathnames to reduce false positives
          const entryUrl = new URL(entry.name, location.origin);
          const preloadUrlObj = new URL(preloadUrl, location.origin);

          // Match if pathnames end with the same file (more precise than includes)
          return entryUrl.pathname.endsWith(preloadUrlObj.pathname);
        } catch {
          return false;
        }
      });

      if (wasLoaded) {
        // Script was loaded dynamically (import(), injected, etc.) - this is actually OK
        validPreloads.push({
          type: "dynamic",
          preload,
          url: preloadHref,
          shortUrl,
          note: "Dynamic script (not in DOM, but loaded)",
        });
      } else {
        // Preload without matching script and not loaded - likely unused
        issues.push({
          type: "orphan",
          severity: "warning",
          preload,
          url: preloadHref,
          shortUrl,
          message: "Preloaded script not found or loaded",
          explanation: "This preload appears unused - script not in DOM and not in Performance API",
          fix: "Verify the script is needed, or remove the preload",
        });
      }
    } else {
      const isAsync = matchingScript.async;
      const isDefer = matchingScript.defer;
      const isModule = matchingScript.type === "module";
      const inHead = matchingScript.closest("head") !== null;
      const scriptLocation = inHead ? "head" : "body";
      const fetchPriority = matchingScript.getAttribute("fetchpriority");

      // Determine natural priority
      let naturalPriority = "Medium/High";

      if (isAsync) {
        naturalPriority = "Lowest/Low";
      } else if (isDefer) {
        naturalPriority = "Lowest/Low";
      } else if (inHead) {
        naturalPriority = "Medium/High";
      }

      // Check for anti-pattern: preload + async/defer
      if (isAsync || isDefer) {
        const attributes = [];
        if (isAsync) attributes.push("async");
        if (isDefer) attributes.push("defer");
        if (isModule) attributes.push("type='module'");

        // Check if fetchpriority="low" is used to mitigate the issue
        const hasMitigation = fetchPriority === "low";

        if (hasMitigation) {
          // Valid: preload with async/defer but fetchpriority="low" mitigates the issue
          validPreloads.push({
            type: "mitigated",
            preload,
            script: matchingScript,
            url: preloadHref,
            shortUrl,
            location: scriptLocation,
            attributes: attributes.join(" + ") + " + fetchpriority='low'",
            naturalPriority,
            note: "Early discovery with low priority - acceptable pattern",
          });
        } else {
          issues.push({
            type: "async-defer-preload",
            severity: "error",
            preload,
            script: matchingScript,
            url: preloadHref,
            shortUrl,
            attributes: attributes.join(" + "),
            location: scriptLocation,
            naturalPriority,
            preloadPriority: "Medium/High",
            fetchPriority: fetchPriority || "not set",
            message: `Script has ${attributes.join("/")} but is also preloaded`,
            explanation: `${attributes.join("/")} scripts load at ${naturalPriority} priority. Preloading escalates them to Medium/High, causing bandwidth competition with critical resources.`,
            fix: `Remove the preload, OR add fetchpriority="low" to the <script> tag to keep early discovery without priority escalation`,
          });
        }
      } else if (isModule) {
        // ES modules without async/defer behave like defer by default
        issues.push({
          type: "module-preload",
          severity: "warning",
          preload,
          script: matchingScript,
          url: preloadHref,
          shortUrl,
          attributes: "type='module'",
          location: scriptLocation,
          naturalPriority: "Lowest/Low (module default)",
          preloadPriority: "Medium/High",
          message: "ES module is using rel='preload' instead of rel='modulepreload'",
          explanation:
            "ES modules behave like defer scripts by default (low priority). Using rel='preload' as='script' escalates priority unnecessarily. For modules, use rel='modulepreload' instead.",
          fix: "Change <link rel='preload' as='script'> to <link rel='modulepreload'> for proper module preloading",
        });
      } else {
        // Valid preload (blocking script) - but check if it's really needed
        const needsReview = !inHead; // Blocking scripts at end of body rarely need preload

        validPreloads.push({
          preload,
          script: matchingScript,
          url: preloadHref,
          shortUrl,
          location: scriptLocation,
          naturalPriority,
          needsReview,
          reviewNote: needsReview ? "Consider using 'defer' instead" : "Valid for critical scripts",
        });
      }
    }
  });

  // Display results
  console.group(
    "%c🔍 Preload + Async/Defer Script Validation",
    "font-weight: bold; font-size: 14px;",
  );

  console.log("");
  console.log("%cSummary:", "font-weight: bold;");
  console.log(`   Total preloaded scripts: ${preloadedScripts.length}`);
  console.log(`   Valid preloads (blocking scripts): ${validPreloads.length}`);
  console.log(`   Issues found: ${issues.length}`);

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

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

    const errorTable = errors
      .filter((e) => e.type === "async-defer-preload")
      .map((issue) => ({
        Script: issue.shortUrl,
        Location: issue.location,
        Attributes: issue.attributes,
        "Natural Priority": issue.naturalPriority,
        "Preload Priority": "⚠️ Medium/High",
        Issue: "❌ Priority escalation",
      }));

    if (errorTable.length > 0) {
      console.table(errorTable);
    }

    console.log("");
    console.log("%c🔴 Detailed Issues:", "font-weight: bold; color: #ef4444;");
    errors
      .filter((e) => e.type === "async-defer-preload")
      .forEach((issue, i) => {
        console.log("");
        console.log(`%c${i + 1}. ${issue.message}`, "font-weight: bold;");
        console.log(`   URL: ${issue.url}`);
        console.log(`   Problem: ${issue.explanation}`);
        console.log(`   Fix: ${issue.fix}`);
        console.log("");
        console.log("   Elements:");
        console.log("   Preload:", issue.preload);
        console.log("   Script:", issue.script);
      });

    console.groupEnd();
  }

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

    warnings.forEach((issue) => {
      console.log("");
      console.log(`%c${issue.message}`, "font-weight: bold;");
      console.log(`   URL: ${issue.url}`);
      console.log(`   ${issue.fix}`);
    });

    console.groupEnd();
  }

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

    const blockingScripts = validPreloads.filter((v) => v.type !== "dynamic");
    const dynamicScripts = validPreloads.filter((v) => v.type === "dynamic");

    if (blockingScripts.length > 0) {
      console.log("Blocking scripts that may benefit from preload:");
      console.log("");

      const validTable = blockingScripts.map((v) => ({
        Script: v.shortUrl,
        Location: v.location,
        Priority: v.naturalPriority,
        Status: v.needsReview ? "⚠️ Review needed" : "✅ Valid",
        Note: v.reviewNote,
      }));

      console.table(validTable);

      if (blockingScripts.some((v) => v.needsReview)) {
        console.log("");
        console.log(
          "%c💡 Tip: Blocking scripts at end of <body> rarely need preload.",
          "color: #f59e0b;",
        );
        console.log("   Consider using 'defer' instead for better performance.");
      }
    }

    if (dynamicScripts.length > 0) {
      console.log("");
      console.log(
        `%c📦 Dynamic Scripts (${dynamicScripts.length})`,
        "font-weight: bold; color: #3b82f6;",
      );
      console.log("These scripts are loaded dynamically (import(), code splitting, etc.):");
      console.log("");

      const dynamicTable = dynamicScripts.map((v) => ({
        Script: v.shortUrl,
        Type: "Dynamic/Code-split",
        Status: "✅ OK",
        Note: v.note,
      }));

      console.table(dynamicTable);

      console.log("");
      console.log(
        "%c💡 Info: Dynamic scripts don't appear in the DOM but are loaded by other scripts.",
        "color: #3b82f6;",
      );
      console.log("   This is normal for code-splitting, dynamic imports, or lazy-loaded modules.");
    }

    console.groupEnd();
  }

  // Recommendations
  console.log("");
  console.group("%c📝 Best Practices", "color: #3b82f6; font-weight: bold;");
  console.log("");
  console.log("%c❌ DON'T preload async/defer scripts:", "font-weight: bold;");
  console.log("   Bad pattern:");
  console.log(
    '%c   <link rel="preload" href="analytics.js" as="script">',
    "font-family: monospace; color: #ef4444;",
  );
  console.log(
    '%c   <script src="analytics.js" async></script>',
    "font-family: monospace; color: #ef4444;",
  );
  console.log("");
  console.log("%c✅ DO let async scripts load naturally:", "font-weight: bold;");
  console.log("   Good pattern:");
  console.log(
    '%c   <script src="analytics.js" async></script>',
    "font-family: monospace; color: #22c55e;",
  );
  console.log("");
  console.log("%c✅ DO preload ONLY critical blocking scripts:", "font-weight: bold;");
  console.log("   When script is needed early and blocks parsing:");
  console.log(
    '%c   <link rel="preload" href="critical.js" as="script">',
    "font-family: monospace; color: #22c55e;",
  );
  console.log(
    '%c   <script src="critical.js"></script>',
    "font-family: monospace; color: #22c55e;",
  );
  console.log("");
  console.log("%cResource priority order:", "font-weight: bold;");
  console.log("   1. Critical CSS (for LCP)");
  console.log("   2. Critical fonts (with crossorigin)");
  console.log("   3. LCP images (fetchpriority='high')");
  console.log("   4. Critical blocking scripts (rare)");
  console.log("   5. Everything else (no preload needed)");
  console.groupEnd();

  // Summary
  if (errors.length === 0 && warnings.length === 0) {
    console.log("");
    console.log(
      "%c✅ Great! No anti-patterns detected.",
      "color: #22c55e; font-weight: bold; font-size: 14px;",
    );
    console.log(
      '%cAll script preloads are either on blocking scripts or properly mitigated with fetchpriority="low".',
      "color: #22c55e;",
    );
  } else {
    console.log("");
    console.log(
      `%c⚠️ Found ${errors.length} error(s) and ${warnings.length} warning(s). Review recommendations above.`,
      "color: #ef4444; font-weight: bold;",
    );
  }

  console.groupEnd();

  // One item per script (an issue replaces the valid entry of the same URL), at most 50
  const itemsByUrl = new Map();
  validPreloads.forEach((v) => itemsByUrl.set(v.url, {
    url: v.shortUrl,
    type: v.type === "dynamic" ? "dynamic" : v.type === "mitigated" ? "mitigated" : "blocking",
    strategy: v.attributes || "blocking-script",
    location: v.location || "unknown",
    reviewNote: v.reviewNote || v.note || "",
  }));
  issues.forEach((i) => itemsByUrl.set(i.url, {
    url: i.shortUrl,
    type: "issue",
    strategy: i.attributes || i.type,
    location: i.location || "unknown",
    reviewNote: i.fix || "",
  }));

  return {
    script: "Validate-Preload-Async-Defer-Scripts",
    status: "ok",
    count: preloadedScripts.length,
    details: {
      validCount: validPreloads.length,
      issueCount: issues.length,
    },
    items: Array.from(itemsByUrl.values()).slice(0, 50),
    issues: issues.map(i => ({
      severity: i.severity,
      message: i.fix ? `${i.message.replace(/\.$/, "")}. Fix: ${i.fix}` : i.message,
    })),
  };
})();

Understanding the results

Summary section

  • Total count of preloaded scripts
  • Valid preloads (blocking scripts that may benefit)
  • Issues detected

Issue types

TypeSeverityDescription
Preload + async/deferErrorAnti-pattern: script already loads non-blocking without fetchpriority
Module preloadWarningES module using rel=“preload” instead of rel=“modulepreload”
Orphan preloadWarningPreloaded script not found or loaded

Valid preload types

TypeDescription
Blocking scriptsScripts without async/defer that may benefit from preload
Mitigated patternPreload + async/defer with fetchpriority=“low” (early discovery, low priority)
Dynamic scriptsScripts loaded via import(), code-splitting, or injection

Note: Dynamic scripts (loaded via import() or code-splitting) don’t appear in the DOM but are visible in the Performance API. The snippet detects these by checking resource timing entries and classifies them separately from truly orphaned preloads.

Anti-patterns table

ColumnDescription
ScriptFilename of the preloaded script
LocationWhere the script is placed (head or body)
Attributesasync, defer, or type=‘module’ attributes found
Natural PriorityThe script’s network priority without preload
Preload PriorityThe elevated priority caused by preload
fetchPriorityValue of fetchpriority attribute (shows if mitigation is missing)
IssueType of problem detected

Recommendations

The snippet provides specific guidance on:

  1. Which preloads to remove (async/defer scripts without fetchpriority=“low”)
  2. How to mitigate if preload is needed (add fetchpriority=“low” to script tag)
  3. What to preload instead (critical resources like fonts, CSS, LCP images)
  4. Correct usage of rel=“modulepreload” for ES modules

Real-world example

Anti-pattern (hurts performance)

<!-- Preloading Google Analytics (async) - BAD --> <link rel="preload" href="https://www.google-analytics.com/analytics.js" as="script" /> <script async src="https://www.google-analytics.com/analytics.js"></script> <!-- Result: Analytics competes with critical resources like fonts/CSS -->

Correct approach

<!-- No preload - async is enough --> <script async src="https://www.google-analytics.com/analytics.js"></script> <!-- Save preload for truly critical resources --> <link rel="preload" href="/critical-font.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/hero-image.webp" as="image" fetchpriority="high" />

Why this matters for Core Web Vitals

Impact on LCP (largest contentful paint)

If you preload non-critical async scripts, they compete for bandwidth with:

  • LCP image
  • Critical CSS
  • Web fonts used above the fold

This can measurably delay your LCP, especially on slower connections and bandwidth-constrained devices. The actual impact depends on your site’s resource size, connection speed, and number of competing resources.

Priority order reality

Without preload: ├── CSS (high priority) ├── Fonts (high priority) ├── LCP image (high priority) └── Async scripts (low priority) ✅ · With wrong preload: ├── Preloaded async script (high priority) ❌ ├── CSS (competing) ├── Fonts (competing) └── LCP image (competing)

When is preload + script actually appropriate?

According to JavaScript Loading Priorities (opens in a new tab), preload is appropriate only for very specific cases:

For scripts that:

  1. Generate critical content needed for First Meaningful Paint
  2. Shouldn’t affect above-the-fold layout
  3. Are discovered late in the HTML
<!-- Valid: Critical content generator, discovered late --> <link rel="preload" href="/critical-content-generator.js" as="script" /> <!-- ... lots of HTML ... --> <script src="/critical-content-generator.js" async></script>

Warning: This is a rare edge case. Most scripts should NOT be preloaded.

Better alternatives for common cases

<!-- For framework/polyfills needed early --> <script src="/framework.js"></script> <!-- In <head>, blocking --> <!-- For scripts needing DOM --> <script src="/app.js" defer></script> <!-- Loads at Low priority, executes after parse --> <!-- For independent scripts --> <script src="/analytics.js" async></script> <!-- Loads at Low priority, no preload needed -->

ES modules: Use modulepreload, not preload

ES modules (type="module") behave like defer scripts by default, loading at low priority. If you need to preload them, use the rel="modulepreload" attribute instead of rel="preload":

Incorrect (detected as anti-pattern)

<link rel="preload" href="/app.js" as="script" /> <script type="module" src="/app.js"></script>

Correct approach for modules

<link rel="modulepreload" href="/app.js" /> <script type="module" src="/app.js"></script>

Key differences

  • rel="modulepreload" understands module imports and preloads dependencies
  • rel="preload" as="script" treats it as a regular script (wrong priority escalation)
  • Module scripts already defer by default, so avoid unnecessary preloading

Learn more: MDN: <link rel="modulepreload"> (opens in a new tab)

Alternative fix: Using fetchpriority="low"

If you need to keep the preload for early discovery (e.g., the script is defined late in the HTML but needs to start downloading early), you can use fetchpriority="low" on the script tag to reduce its bandwidth priority while keeping the preload:

Problem: Preload escalates async script priority

<link rel="preload" href="/analytics.js" as="script" /> <!-- ... --> <script async src="/analytics.js"></script> <!-- Priority escalated to Medium/High, competes with critical resources -->

Solution: Keep preload for discovery, but lower script priority

<link rel="preload" href="/analytics.js" as="script" /> <!-- ... --> <script async src="/analytics.js" fetchpriority="low"></script> <!-- Downloaded early, but at low priority - doesn't compete with LCP -->

When to use this approach

  • Script is discovered late in HTML but should start downloading early
  • You need early discovery but don’t want bandwidth competition
  • The script is truly async/defer and doesn’t need high priority

Note: The fetchpriority attribute on the <script> tag takes precedence over the preload hint priority. However, removing the preload entirely is usually the simpler and safer fix for most cases.

Learn more: MDN: fetchpriority (opens in a new tab)

A common scenario where this anti-pattern appears is with third-party consent management scripts (cookie banners, GDPR compliance, etc.). Developers often think: “I want it async, but I need it now” and reach for preload. Let’s analyze the tradeoffs and solutions.

The problem

Common (incorrect) approach
<!-- ❌ Anti-pattern: Preload + async without mitigation --> <link rel="preload" href="https://cdn.web.dev/consent.js" as="script" /> <script async src="https://cdn.web.dev/consent.js"></script>
Why developers do this
  • Want the consent banner to appear quickly (early discovery)
  • Don’t want to block page rendering (async)
  • Assume preload + async is the best of both worlds
What actually happens
A preloaded async script is discovered by the HTML parser and downloaded at medium or high priority, so it competes with the LCP image, CSS and fonts and delays the LCPConnections: HTML Parser to Discovers Preload; Discovers Preload to Download at Medium/High Priority; Download at Medium/High Priority to Competes with LCP Image; Download at Medium/High Priority to Competes with CSS; Download at Medium/High Priority to Competes with Fonts; Competes with LCP Image to Delayed LCP; Competes with CSS to Delayed LCP; Competes with Fonts to Delayed LCP.HTML ParserDiscovers PreloadDownload atMedium/HighPriorityCompetes withLCP ImageCompetes withCSSCompetes withFontsDelayed LCP
A preloaded async script is discovered by the HTML parser and downloaded at medium or high priority, so it competes with the LCP image, CSS and fonts and delays the LCPConnections: HTML Parser to Discovers Preload; Discovers Preload to Download at Medium/High Priority; Download at Medium/High Priority to Competes with LCP Image; Download at Medium/High Priority to Competes with CSS; Download at Medium/High Priority to Competes with Fonts; Competes with LCP Image to Delayed LCP; Competes with CSS to Delayed LCP; Competes with Fonts to Delayed LCP.HTML ParserDiscoversPreloadDownload atMedium/HighPriorityCompetes withLCP ImageCompetes withCSSCompetes withFontsDelayed LCP

Proposed alternative: fetchpriority="high"

Some developers consider using fetchpriority="high" instead:

<!-- ⚠️ Still problematic --> <script async src="https://cdn.web.dev/consent.js" fetchpriority="high"></script>
Why this doesn’t solve the problem
Adding fetchpriority high to an async script raises it to high priority, which makes it compete with the LCP resource, CSS and fonts in the same way as a plain preloadConnections: fetchpriority='high' on async script to Same Issue as Preload; Same Issue as Preload to Elevates to High Priority; Elevates to High Priority to Competes with LCP; Elevates to High Priority to Competes with CSS; Elevates to High Priority to Competes with Fonts.fetchpriority='high' on asyncscriptSame Issue as PreloadElevates to High PriorityCompetes with LCPCompetes with CSSCompetes with Fonts
Adding fetchpriority high to an async script raises it to high priority, which makes it compete with the LCP resource, CSS and fonts in the same way as a plain preloadConnections: fetchpriority='high' on async script to Same Issue as Preload; Same Issue as Preload to Elevates to High Priority; Elevates to High Priority to Competes with LCP; Elevates to High Priority to Competes with CSS; Elevates to High Priority to Competes with Fonts.fetchpriority='high'on async scriptSame Issue asPreloadElevates to HighPriorityCompetes withLCPCompetes withCSSCompetes withFonts

Using fetchpriority="high" on an async script has the same negative effect as preloading it - both artificially escalate the priority and cause bandwidth competition.

Note: This priority escalation applies to scripts discovered during initial HTML parsing. Dynamically inserted scripts (via JavaScript) may behave differently depending on when they’re injected.

Solution 1: Preload with low priority (best balance)

If you truly need early discovery but don’t want priority escalation:

<!-- ✅ Early discovery + low priority --> <link rel="preload" href="https://cdn.web.dev/consent.js" as="script" fetchpriority="low" /> <script async src="https://cdn.web.dev/consent.js" fetchpriority="low"></script>
How this works
A preload hint starts the download early while fetchpriority low keeps the script at low priority, which gives early discovery without competing with the LCPConnections: Preload Hint to Start Download Early; fetchpriority='low' to Keep Low Priority; Start Download Early to Early Discovery; Keep Low Priority to Early Discovery; Early Discovery to No LCP Competition.Preload HintStart DownloadEarlyfetchpriority='low'Keep Low PriorityEarly DiscoveryNo LCPCompetition
A preload hint starts the download early while fetchpriority low keeps the script at low priority, which gives early discovery without competing with the LCPConnections: Preload Hint to Start Download Early; fetchpriority='low' to Keep Low Priority; Start Download Early to Early Discovery; Keep Low Priority to Early Discovery; Early Discovery to No LCP Competition.Preload HintStart DownloadEarlyfetchpriority='low'Keep LowPriorityEarly DiscoveryNo LCPCompetition
Benefits
  • Script discovered and starts downloading early
  • Stays at Low priority (doesn’t compete with LCP)
  • Still loads async (non-blocking)
  • Best of both worlds

Solution 2: Just use async (simplest)

If the script tag is already in <head>, preload may not be necessary:

<!-- ✅ Simple and effective if script is in <head> --> <head> <script async src="https://cdn.web.dev/consent.js"></script> <!-- Other head content --> </head>
When this is sufficient
Decision flow for a script in the head: if the browser discovers it early, async alone is enough; if it appears late in the HTML, consider preload with low priorityConnections: Script in <head> to Discovered Early?; Discovered Early? to Async = Low Priority (Yes); Discovered Early? to Consider Preload + Low Priority (No, late in HTML); Async = Low Priority to No Preload Needed.Script in <head>Discovered Early?Async = Low PriorityConsider Preload + Low PriorityNo Preload NeededYesNo, late in HTML
Decision flow for a script in the head: if the browser discovers it early, async alone is enough; if it appears late in the HTML, consider preload with low priorityConnections: Script in <head> to Discovered Early?; Discovered Early? to Async = Low Priority (Yes); Discovered Early? to Consider Preload + Low Priority (No, late in HTML); Async = Low Priority to No Preload Needed.Script in <head>DiscoveredEarly?Async = LowPriorityConsider Preload+ Low PriorityNo PreloadNeededYesNo, late in HTML
Why this works
  • Script in <head> is discovered immediately
  • Async loads at Lowest/Low priority naturally
  • No preload overhead or complexity needed
ApproachNetwork PriorityLCP ImpactComplexityRecommendation
<script async> in <head>Lowest/Low NoneLowBest for most cases
<link rel=preload> + <script async fetchpriority=low>Lowest/Low NoneMediumGood if script is late in HTML
<link rel=preload> + <script async> Medium/High HighMediumAnti-pattern - avoid
<script async fetchpriority=high> High HighLowAnti-pattern - avoid
Decision tree that picks how to load a consent script: async when it is in the head, preload with async and low fetch priority when it is late in the body and needs an early download, plain async otherwiseConnections: Need consent script to Script in <head>?; Script in <head>? to Use: <script async> (Yes); Script in <head>? to Need early download? (No, late in body); Need early download? to Use: preload + async + fetchpriority='low' (Yes); Need early download? to Use: <script async> (No); Use: <script async> to Loads early, low priority; Use: preload + async + fetchpriority='low' to Loads early, low priority; Use: <script async> to Loads when discovered, low priority.Need consent scriptScript in <head>?Use: <script async>Need early download?Use: preload + async +fetchpriority='low'Use: <script async>Loads early, low priorityLoads when discovered, lowpriorityYesNo, late in bodyYesNo
Decision tree that picks how to load a consent script: async when it is in the head, preload with async and low fetch priority when it is late in the body and needs an early download, plain async otherwiseConnections: Need consent script to Script in <head>?; Script in <head>? to Use: <script async> (Yes); Script in <head>? to Need early download? (No, late in body); Need early download? to Use: preload + async + fetchpriority='low' (Yes); Need early download? to Use: <script async> (No); Use: <script async> to Loads early, low priority; Use: preload + async + fetchpriority='low' to Loads early, low priority; Use: <script async> to Loads when discovered, low priority.Need consentscriptScript in<head>?Use: <scriptasync>Need earlydownload?Use: preload +async +fetchpriority='low'Use: <scriptasync>Loads early, lowpriorityLoads whendiscovered, lowpriorityYesNo, late in bodyYesNo

Note: For scripts late in <body> where early download is not needed, plain <script async> is appropriate because the consent banner is non-critical. The script will load when discovered by the parser, which is acceptable for this use case.

Key takeaway

For consent scripts and other third-party resources:

  1. Don’t use fetchpriority="high" - it causes the same problems as preload
  2. If script is in <head> - simple async is usually enough
  3. If you must preload - always add fetchpriority="low" to the script tag
  4. Remember: Consent banners are not critical for LCP - let them load at low priority

The goal is early start, low priority - not high priority. Users care more about fast LCP than seeing the consent banner 50ms earlier.

Understanding script priorities (Chrome)

Based on Addy Osmani’s research (opens in a new tab), here’s how different script loading strategies affect priorities:

Scripts in <head> (framework/polyfills)

  • Network: Medium/High
  • Execution: VeryHigh (blocks parser)
  • Use for: Scripts affecting FMP layout, must run before other scripts
  • Network: Lowest/Low
  • Execution: VeryLow (after parsing)
  • Use for: Scripts needing DOM, non-critical content, interactive features used by >50% of sessions

<script async> (analytics, ads)

  • Network: Lowest/Low
  • Execution: High (interrupts parser briefly)
  • Use for: Independent scripts, but be careful - execution priority is inconsistent
  • Network: Medium/High
  • Execution: High
  • Use for: Critical content generators (not layout), discovered late
  • Warning: Rarely needed, often misused

<script> at end of <body> (legacy)

  • Network: Medium/High
  • Execution: Low
  • Problem: Still scheduled at high network priority despite appearing late
  • Better alternative: Use defer instead

Key takeaway: Defer gives you Lowest/Low network priority (good for non-critical scripts) while preload forces Medium/High priority (should only be used for truly critical resources).

Common sources of this issue

SourceWhy It Happens
Tag managersAuto-add preload to all scripts including async ones
Performance pluginsGeneric “preload all scripts” rules
Copy-paste optimizationAdding preloads without understanding the script’s loading strategy
Outdated adviceOld articles suggesting “preload everything”
Misunderstanding asyncAssuming async always needs help, not realizing it already loads efficiently

Advanced strategy: server-side conditional loading

For production scenarios with consent scripts, a more sophisticated approach involves server-side logic to conditionally prioritize the script only when needed (first visit). This strategy considers that:

  • First visit: CMP banner IS critical (might even be the LCP element) - prioritize it
  • Returning visits: CMP script can load at low priority (consent already stored in cookie)

Key insight: If you have a consent cookie, the user has already made their choice. The CMP script doesn’t need high priority because the banner won’t be shown. This server-side detection allows you to optimize resource prioritization dynamically based on user state.

This approach is beyond the scope of this validation snippet, as it requires architectural changes to your server-side rendering logic.

Browser support

The snippet needs these features to run.

FeatureChromeEdgeFirefoxSafari
PerformanceResourceTiming.initiatorType (opens in a new tab) 43 12 35 11

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

FeatureChromeEdgeFirefoxSafari
<link rel=“preload”> (opens in a new tab) 50 79 85 11.1
<link rel=“modulepreload”> (opens in a new tab) 66 79 115 17
<script fetchpriority> (opens in a new tab) 101 101 132 17.2

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

Further reading

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