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 |
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:
Snippet
// Resource Hints Analysis
// https://webperf-snippets.nucliweb.net
(() => {
const formatBytes = (bytes) => {
if (!bytes || bytes === 0) return "-";
const k = 1024;
const sizes = ["B", "KB", "MB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(1) + " " + sizes[i];
};
// 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(
(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");
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();
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;
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();
return {
script: "Resource-Hints",
status: "ok",
count: allHints.length,
details: {
byType: Object.fromEntries(hintTypes.map(t => [t.rel, allHints.filter(h => h.rel === t.rel).length])),
missingPreconnectsCount: missingPreconnects.length,
},
items: allHints.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 |
Further Reading
- Preload critical assets (opens in a new tab) | web.dev
- Establish network connections early (opens in a new tab) | web.dev
- Prefetch resources (opens in a new tab) | Chrome Developers
- Resource Hints specification (opens in a new tab) | W3C