
Nuxt auto-sorts <head> tags via Capo.js weights and scores 44% good LCP across 88K CrUX origins. Shopify does zero head optimization and scores 87%. The framework doing the most work on head ordering has the worst LCP. That gap isn't explained by ordering - it's TTFB, JavaScript payload, hydration cost.
So does head tag ordering matter? We ran 120 controlled benchmarks to find out.
What We Tested
We built static test pages with four <head> orderings:
- Optimal - Capo.js order (
<meta charset>, viewport,<title>, preconnects, async scripts, sync scripts, stylesheets, deferred scripts, meta) - Common-bad - Typical mistakes: charset after title, preconnects after stylesheets, sync scripts before CSS
- Random - Shuffled tags
- Worst - Reverse Capo.js order (deferred scripts first, charset last)
Each ordering was tested at three complexity levels - minimal (8 tags), medium (15 tags), and heavy (36 tags including GTM, multiple stylesheets, preloads, and full social meta). All pages loaded Google Fonts; heavy pages also loaded GTM.
We used Chrome DevTools Protocol (CDP) network throttling (not Lighthouse simulated throttling) at two profiles: fast-4g (1.44 Mbps, 150ms RTT) and slow-3g (400 Kbps, 400ms RTT). Each configuration ran 5 times in headless Chromium via Puppeteer - 120 page loads total.
Results
Median First Contentful Paint:
| Complexity | Throttle | Optimal | Common-Bad | Random | Worst | Max Delta |
|---|---|---|---|---|---|---|
| Minimal | fast-4g | 460ms | 456ms | 456ms | 464ms | +4ms (1%) |
| Medium | fast-4g | 468ms | 468ms | 468ms | 468ms | 0ms |
| Heavy | fast-4g | 496ms | 488ms | 504ms | 528ms | +32ms (6%) |
| Minimal | slow-3g | 984ms | 992ms | 992ms | 980ms | +8ms (1%) |
| Medium | slow-3g | 1068ms | 1040ms | 1068ms | 1080ms | +12ms (1%) |
| Heavy | slow-3g | 1228ms | 1228ms | 1228ms | 1440ms | +212ms (17%) |
| Complexity / Throttle | Optimal (ms) | Common-Bad (ms) | Random (ms) | Worst (ms) |
|---|---|---|---|---|
| Min 4G | 460 | 456 | 456 | 464 |
| Med 4G | 468 | 468 | 468 | 468 |
| Heavy 4G | 496 | 488 | 504 | 528 |
| Min 3G | 984 | 992 | 992 | 980 |
| Med 3G | 1068 | 1040 | 1068 | 1080 |
| Heavy 3G | 1228 | 1228 | 1228 | 1440 |
The DCL Paradox
While FCP and LCP suffer under "Worst" ordering, DOMContentLoaded (DCL) often improves. In our heavy + fast-4g test, "Worst" ordering achieved a DCL of 522ms compared to "Optimal" at 533ms.
| Ordering | FCP (ms) | DCL (ms) |
|---|---|---|
| Optimal Ordering | 496 | 533.1 |
| Worst Ordering | 528 | 522.5 |
This happens because the browser discovers and finishes downloading deferred scripts (placed at the top in "Worst" order) earlier. While this improves the DCL timestamp, it's a "false" optimization: it steals bandwidth from the sync scripts needed for the initial paint.
<meta http-equiv="Content-Security-Policy"> tag deep in the head (or after any scripts) can cause the Chromium Preload Scanner to stall or disable itself entirely. This forces the browser to load resources sequentially, which can add seconds to the load time regardless of other ordering.Head ordering matters on heavy pages over slow connections. Everywhere else, it's irrelevant.
The Mechanism: Sync Script Starvation
The 212ms gap isn't CSS. Google Fonts finishes at the same time in both orderings - within 0.7ms.
The actual mechanism is sync script bandwidth contention:
<!-- Worst: deferred scripts discovered first -->
<head>
<script defer src="/assets/js/lazy-components.js"></script>
<script defer src="/assets/js/interactions.js"></script>
<!-- ...other tags... -->
<script src="/assets/js/vendor.js"></script>
<script src="/assets/js/app.js"></script>
</head>
The preload scanner finds <script defer> tags first and starts downloading them. On a throttled connection, they consume bandwidth. When the parser finally discovers the parser-blocking sync scripts, they compete for the same constrained pipe.
Resource timing (heavy page, slow-3g):
| Resource | Optimal | Worst | Change |
|---|---|---|---|
| vendor.js (sync) | 515ms | 944ms | +84% slower |
| app.js (sync) | 543ms | 858ms | +58% slower |
| Google Fonts CSS | 658ms | 630ms | -4% (same) |
| interactions.js (defer) | 1058ms | 457ms | -57% (faster) |
| lazy-components.js (defer) | 1030ms | 429ms | -58% (faster) |
| Ordering | Resource | Type | Start (ms) | End (ms) |
|---|---|---|---|---|
| Optimal ordering | Fonts CSS | css | 0 | 658 |
| Optimal ordering | vendor.js | sync | 30 | 545 |
| Optimal ordering | app.js | sync | 60 | 603 |
| Optimal ordering | interactions.js | defer | 550 | 1608 |
| Optimal ordering | lazy-comp.js | defer | 570 | 1600 |
| Worst ordering | interactions.js | defer | 0 | 457 |
| Worst ordering | lazy-comp.js | defer | 20 | 449 |
| Worst ordering | vendor.js | sync | 180 | 1124 |
| Worst ordering | app.js | sync | 220 | 1078 |
| Worst ordering | Fonts CSS | css | 260 | 890 |
Resource Loading Waterfall
heavy page, slow-3gWrong ordering lets deferred scripts "steal" bandwidth from critical resources. They finish faster, but parser-blocking scripts are delayed by ~466ms, pushing back the first paint.
In worst ordering, deferred scripts finish faster (they got a head start) but that's wasted priority - they don't block rendering. The parser is stuck waiting for vendor.js, which is stuck waiting for bandwidth.
CrUX: 10.7 Million Origins
Benchmarks show ordering matters under specific conditions. CrUX data (Jan 2026, 10.7M mobile origins) tells a broader story about the real-world impact of head optimization compared to other performance factors.
| Framework | Head Optimization | LCP (ms) | TTFB (ms) | Good LCP (%) |
|---|---|---|---|---|
| Shopify | None | 1685 | 534 | 87.3 |
| Wix | Partial | 1724 | 797 | 81.6 |
| Squarespace | Partial | 1969 | 586 | 76.6 |
| WordPress | Manual/Plugins | 2663 | 1550 | 55.2 |
| Next.js | Partial (Auto-hoist) | 2790 | 956 | 50.9 |
| Nuxt | Full (Auto Capo.js) | 3051 | 989 | 44.2 |
The Nuxt Paradox is the strongest evidence that head ordering alone is not a dominant performance factor at population scale. Nuxt implements the "gold standard" of automatic head ordering, yet it lags behind platforms like Shopify that perform no such optimization. The gap is driven by TTFB and hydration cost: Shopify's edge network and server-rendered architecture provide a baseline performance that head ordering cannot overcome.
8-Month Trend
All three major frameworks were flat over the last 8 months. No framework-level optimization, including head ordering, has moved LCP at population scale. TTFB, image optimization, and JS payload size continue to dominate.
| Month | Nuxt.js (ms) | Next.js (ms) | WordPress (ms) |
|---|---|---|---|
| Jun '25 | 3059 | 2806 | 2696 |
| Jul | 3042 | 2811 | 2695 |
| Aug | 3028 | 2821 | 2681 |
| Sep | 3091 | 2848 | 2707 |
| Oct | 3049 | 2784 | 2669 |
| Nov | 3070 | 2801 | 2671 |
| Dec | 3086 | 2851 | 2689 |
| Jan '26 | 3051 | 2790 | 2662 |
When It Matters
While head ordering isn't a silver bullet, it is a critical optimization for specific scenarios.
| Scenario | Impact of Head Ordering | Verdict |
|---|---|---|
| Heavy Head (30+ tags) GTM, multiple CSS/JS, many social tags | High (+200ms FCP on 3G) | Critical |
| Slow/Unstable Networks 3G, degraded mobile, or rural users | Significant (Resource contention) | High |
| Sync Parser-Blocking Scripts Legacy analytics or necessary polyfills | High (Blocks rendering completely) | Critical |
| Simple Page (5-10 tags) Few styles, no heavy analytics | Negligible (Preload scanner wins) | Optional |
| Fast 4G / 5G / Broadband Modern devices and fast pipes | Low (<10ms difference) | Nice-to-have |
<script> tags. Remove any one condition and the preload scanner neutralizes the difference.On fast networks, resources download quickly regardless of discovery order. On simple pages, there aren't enough resources to create contention. Without sync scripts, nothing blocks the parser.
External CSS (like Google Fonts) is unaffected - the browser prioritizes render-blocking CSS at the network layer independent of HTML position.
How Unhead Handles This
Unhead sorts tags using Capo.js weights automatically. Every useHead() call places sync scripts before deferred, preconnects before the resources they serve, <meta charset> and viewport first:
useHead({
script: [
{ src: '/vendor.js' }, // weight: 50 (sync, placed early)
{ src: '/lazy.js', defer: true } // weight: 80 (deferred, placed late)
],
link: [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' } // weight: 20
]
})
See What is Capo.js? for the complete weight reference.
Reproduce This
Benchmark test pages and Puppeteer scripts are in test/capo-benchmarks. Each test page uses identical external resources - only <head> tag order changes. Network throttling via CDP (Network.emulateNetworkConditions), not Lighthouse simulation.
CrUX data queried from BigQuery's public chrome-ux-report dataset, filtered by framework technology detection. All benchmark results are median values across 5 runs per configuration.