Validate Preload on Async/Defer Scripts
Overview
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:
| Issue | Impact | Explanation |
|---|---|---|
| Preload + async/defer | Wrong priority escalation | Elevates async/defer scripts from Low to Medium/High priority, competing with critical resources |
| Bandwidth competition | Delays LCP | Preloaded scripts compete with CSS, fonts, and LCP images for bandwidth |
| Wasted bytes | Extra requests | Browser may fetch the resource twice if preload doesn't match script attributes |
| False optimization | Maintenance burden | Adds complexity without performance benefit |
Script loading priorities in Chrome:
| Strategy | Network Priority | Execution Priority | Blocks Parser | Use Case |
|---|---|---|---|---|
<script> in <head> | Medium/High | VeryHigh | ✅ Yes | Critical scripts affecting FMP layout |
<link rel=preload> + <script async> | Medium/High ⚠️ | High | Briefly | ⚠️ Anti-pattern for most cases |
<link rel=preload> + <script async fetchpriority=low> | Lowest/Low | High | Briefly | ✅ Early discovery + low priority (valid) |
<script async> | Lowest/Low | High | Briefly | Independent scripts (analytics, ads) |
<script defer> | Lowest/Low | VeryLow | ❌ No | Scripts needing DOM, running in order |
<script> at end of <body> | Medium/High | Low | Briefly | Legacy pattern (use defer instead) |
<script defer> at end of <body> | Lowest/Low | VeryLow | ❌ No | Non-critical occasional features |
Key insight: Adding
preloadto anasyncordeferscript 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:
Diagram comparing script loading strategies. Source: JavaScript modules - module vs script | V8 (opens in a new tab)
Why not to preload async/defer scripts:
- Async scripts naturally load at Lowest/Low network priority (good!)
- Defer scripts also load at Lowest/Low network priority (also good!)
- Adding preload escalates them to Medium/High priority (bad!)
- The result: They compete with critical CSS, fonts, and LCP images for bandwidth
- 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
asyncordefer, it's already declared as "not critical for initial render". Preloading contradicts that by elevating its priority to compete with truly critical resources!
Snippet
// Validate Preload on Async/Defer Scripts
// https://webperf-snippets.nucliweb.net
(() => {
// 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;
}
// Get performance entries for scripts
const performanceScripts = performance
.getEntriesByType("resource")
.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";
let executionPriority = "Low";
if (isAsync) {
naturalPriority = "Lowest/Low";
executionPriority = "High";
} else if (isDefer) {
naturalPriority = "Lowest/Low";
executionPriority = "VeryLow";
} else if (inHead) {
naturalPriority = "Medium/High";
executionPriority = "VeryHigh";
}
// 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();
})();
Understanding the Results
Summary Section:
- Total count of preloaded scripts
- Valid preloads (blocking scripts that may benefit)
- Issues detected
Issue Types:
| Type | Severity | Description |
|---|---|---|
| Preload + async/defer | Error | Anti-pattern: script already loads non-blocking without fetchpriority |
| Module preload | Warning | ES module using rel="preload" instead of rel="modulepreload" |
| Orphan preload | Warning | Preloaded script not found or loaded |
Valid Preload Types:
| Type | Description |
|---|---|
| Blocking scripts | Scripts without async/defer that may benefit from preload |
| Mitigated pattern | Preload + async/defer with fetchpriority="low" (early discovery, low priority) |
| Dynamic scripts | Scripts 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:
| Column | Description |
|---|---|
| Script | Filename of the preloaded script |
| Location | Where the script is placed (head or body) |
| Attributes | async, defer, or type='module' attributes found |
| Natural Priority | The script's network priority without preload |
| Preload Priority | The elevated priority caused by preload |
| fetchPriority | Value of fetchpriority attribute (shows if mitigation is missing) |
| Issue | Type of problem detected |
Recommendations:
The snippet provides specific guidance on:
- Which preloads to remove (async/defer scripts without fetchpriority="low")
- How to mitigate if preload is needed (add fetchpriority="low" to script tag)
- What to preload instead (critical resources like fonts, CSS, LCP images)
- 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:
Valid use case: <link rel=preload> + <script async>
For scripts that:
- Generate critical content needed for First Meaningful Paint
- Shouldn't affect above-the-fold layout
- 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 dependenciesrel="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)
Real-World Case: Third-Party Consent Scripts
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.example.com/consent.js" as="script" />
<script async src="https://cdn.example.com/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:
Proposed Alternative: fetchpriority="high"
Some developers consider using fetchpriority="high" instead:
<!-- ⚠️ Still problematic -->
<script async src="https://cdn.example.com/consent.js" fetchpriority="high"></script>Why this doesn't solve the problem:
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.example.com/consent.js" as="script" fetchpriority="low" />
<script async src="https://cdn.example.com/consent.js" fetchpriority="low"></script>How this works:
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.example.com/consent.js"></script>
<!-- Other head content -->
</head>When this is sufficient:
Why this works:
- Script in
<head>is discovered immediately - Async loads at Lowest/Low priority naturally
- No preload overhead or complexity needed
Priority Comparison: Consent Script Scenarios
| Approach | Network Priority | LCP Impact | Complexity | Recommendation |
|---|---|---|---|---|
<script async> in <head> | Lowest/Low | ✅ None | Low | Best for most cases |
<link rel=preload> + <script async fetchpriority=low> | Lowest/Low | ✅ None | Medium | Good if script is late in HTML |
<link rel=preload> + <script async> | ⚠️ Medium/High | ❌ High | Medium | Anti-pattern - avoid |
<script async fetchpriority=high> | ⚠️ High | ❌ High | Low | Anti-pattern - avoid |
Decision Tree for Consent Scripts
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:
- Don't use
fetchpriority="high"- it causes the same problems as preload - If script is in
<head>- simpleasyncis usually enough - If you must preload - always add
fetchpriority="low"to the script tag - 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
<script defer> (recommended for most 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
<link rel=preload> + <script async> (rare case):
- 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
deferinstead
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
| Source | Why It Happens |
|---|---|
| Tag managers | Auto-add preload to all scripts including async ones |
| Performance plugins | Generic "preload all scripts" rules |
| Copy-paste optimization | Adding preloads without understanding the script's loading strategy |
| Outdated advice | Old articles suggesting "preload everything" |
| Misunderstanding async | Assuming 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.
For a complete implementation guide: A detailed blog post is coming soon on joanleon.dev
Further Reading
- JavaScript Loading Priorities (opens in a new tab) | Addy Osmani - Detailed breakdown of network and execution priorities
- Negative performance impact from preloading (opens in a new tab) | DebugBear
- Preload critical assets (opens in a new tab) | web.dev
- Script loading strategies (opens in a new tab) | Chrome Developers
- Resource prioritization (opens in a new tab) | web.dev
- Andy Davies on font preloading (opens in a new tab) | Andy Davies