Index of every page on this site: llms.txt
Research12 min read

Does Head Tag Order Actually Affect Performance? Original Research

Harlan Wilton Published
Does Head Tag Order Actually Affect Performance? Original Research

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.

Lighthouse's default "simulated" throttling does not model byte-level HTML parsing. It loads the document instantly from localhost and applies a mathematical model to the results after the fact. Because the preload scanner "sees" all tags instantly in this model, tag ordering has zero effect on simulated scores. CDP throttling forces the browser to wait for bytes to arrive over the wire, triggering the real-world behavior of the preload scanner and resource scheduler.

Results

Median First Contentful Paint:

ComplexityThrottleOptimalCommon-BadRandomWorstMax Delta
Minimalfast-4g460ms456ms456ms464ms+4ms (1%)
Mediumfast-4g468ms468ms468ms468ms0ms
Heavyfast-4g496ms488ms504ms528ms+32ms (6%)
Minimalslow-3g984ms992ms992ms980ms+8ms (1%)
Mediumslow-3g1068ms1040ms1068ms1080ms+12ms (1%)
Heavyslow-3g1228ms1228ms1228ms1440ms+212ms (17%)
FCP by Head Ordering
Complexity / ThrottleOptimal (ms)Common-Bad (ms)Random (ms)Worst (ms)
Min 4G460456456464
Med 4G468468468468
Heavy 4G496488504528
Min 3G984992992980
Med 3G1068104010681080
Heavy 3G1228122812281440

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.

The DCL Paradox: Painting vs Loading
OrderingFCP (ms)DCL (ms)
Optimal Ordering496533.1
Worst Ordering528522.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.

One edge case we identified in research but excluded from the 120-run matrix is the CSP Meta Stall. Placing a <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.
Worst-case ordering on a heavy page over slow-3g costs 212ms (17%) of FCP. On simple pages or fast connections, the difference is noise - under 12ms.

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):

ResourceOptimalWorstChange
vendor.js (sync)515ms944ms+84% slower
app.js (sync)543ms858ms+58% slower
Google Fonts CSS658ms630ms-4% (same)
interactions.js (defer)1058ms457ms-57% (faster)
lazy-components.js (defer)1030ms429ms-58% (faster)
Resource Loading Waterfall
OrderingResourceTypeStart (ms)End (ms)
Optimal orderingFonts CSScss0658
Optimal orderingvendor.jssync30545
Optimal orderingapp.jssync60603
Optimal orderinginteractions.jsdefer5501608
Optimal orderinglazy-comp.jsdefer5701600
Worst orderinginteractions.jsdefer0457
Worst orderinglazy-comp.jsdefer20449
Worst orderingvendor.jssync1801124
Worst orderingapp.jssync2201078
Worst orderingFonts CSScss260890

Resource Loading Waterfall

heavy page, slow-3g
Parser-blocking Stylesheet Deferred First paint possible
Optimal ordering FASTER
Fonts CSS
658ms
vendor.js
515ms
app.js
543ms
interactions.js
1058ms
lazy-comp.js
1030ms
658ms
0
0.4s
0.8s
1.2s
1.6s
Worst ordering
interactions.js
457ms
lazy-comp.js
429ms
vendor.js
944ms
app.js
858ms
Fonts CSS
630ms
1124ms
0
0.4s
0.8s
1.2s
1.6s
+466ms
Delayed first paint
-572ms
Deferred scripts (wasted)

Wrong 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.

Head tag ordering doesn't change how fast resources download. It changes which resources get bandwidth first. When non-blocking scripts steal priority from blocking ones, the parser stalls.

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.

The Nuxt Paradox: LCP vs Head Optimization
FrameworkHead OptimizationLCP (ms)TTFB (ms)Good LCP (%)
ShopifyNone168553487.3
WixPartial172479781.6
SquarespacePartial196958676.6
WordPressManual/Plugins2663155055.2
Next.jsPartial (Auto-hoist)279095650.9
NuxtFull (Auto Capo.js)305198944.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.

LCP Trend: 8 Months
MonthNuxt.js (ms)Next.js (ms)WordPress (ms)
Jun '25305928062696
Jul304228112695
Aug302828212681
Sep309128482707
Oct304927842669
Nov307028012671
Dec308628512689
Jan '26305127902662

When It Matters

While head ordering isn't a silver bullet, it is a critical optimization for specific scenarios.

Heavy Head (30+ tags)
Critical
GTM, multiple CSS/JS, many social tags
High (+200ms FCP on 3G)
Slow/Unstable Networks
High
3G, degraded mobile, or rural users
Significant (Resource contention)
Sync Parser-Blocking Scripts
Critical
Legacy analytics or necessary polyfills
High (Blocks rendering completely)
Simple Page (5-10 tags)
Optional
Few styles, no heavy analytics
Negligible (Preload scanner wins)
Fast 4G / 5G / Broadband
Nice-to-have
Modern devices and fast pipes
Low (<10ms difference)
Head tag ordering affects performance when: your head has 20+ tags including sync and deferred scripts, your users are on slow connections (3G or degraded mobile), and you have parser-blocking <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.

Check your site's current head tag ordering with instant feedback and suggestions.

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.

Head tag health across 660,000 sites - including the social sharing gap, head-breakers, and third-party pollution.
How streaming SSR drops JSON-LD, canonicals, and OG tags - and the strategies each framework uses to prevent it.
Did this page help you?