What is Capo.js? Why HTML Head Tag Order Matters · Unhead

[Unhead Home](https://unhead.unjs.io/ "Home")

- [Docs](https://unhead.unjs.io/docs/typescript/head/guides/get-started/overview)
- [Tools](https://unhead.unjs.io/tools)
- [Learn](https://unhead.unjs.io/learn/guides/what-is-capo)

[Releases](https://unhead.unjs.io/releases)

Search…```k`` /`

[Unhead on GitHub](https://github.com/unjs/unhead)

Guides

- [What is Capo.js?](https://unhead.unjs.io/learn/guides/what-is-capo)

Research

- [Capo.js Performance](https://unhead.unjs.io/learn/research/capo-performance-research)
- [State of <head> 2026](https://unhead.unjs.io/learn/research/state-of-head-2026)
- [Streaming SSR SEO](https://unhead.unjs.io/learn/research/streaming-head-performance)

Guide10 min read

# What is Capo.js? Why HTML Head Tag Order Matters

[![Harlan Wilton](https://avatars.githubusercontent.com/u/5326365?v=4)Harlan Wilton](https://x.com/harlan-zw) Published Mar 5, 2026

On this page

- [What is Capo.js?](#what-is-capojs)
- [Why order matters](#why-order-matters)
- [Complete Weight Reference](#complete-weight-reference)
- [Common Mistakes](#common-mistakes)
- [Automatic Sorting with Unhead](#automatic-sorting-with-unhead)
- [Check Your Site](#check-your-site)

## [What is Capo.js?](#what-is-capojs)

[Capo.js](https://github.com/nickvdh/capo.js) is a set of rules by [Rick Viscomi](https://rviscomi.dev/) (Chrome DevRel) defining the optimal order of HTML `<head>` tags for page load performance. Named after the guitar capo - it tunes your `<head>` by putting every tag in the right position.

Browsers parse `<head>` top-to-bottom. Wrong order means delayed rendering, unnecessary re-parsing, and slower LCP.

**Unhead implements Capo.js sorting automatically.** Every tag from `useHead()` is placed in optimal position - zero config.

## [Why order matters](#why-order-matters)

The HTML parser processes `<head>` sequentially:

- `<meta charset>` **after the title** → browser re-parses the document when it discovers a different encoding
- **Sync** `<script>` **before CSS** → blocks both rendering and stylesheet loading
- `<link rel="preconnect">` **after the resource it helps** → arrives too late to matter
- **Render-blocking resources before critical metadata** → delays the browser's understanding of the page

Bad order:

```
<head>
  <title>My Page</title>
  <script src="/app.js"></script>
  <link rel="stylesheet" href="/style.css">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <link rel="preconnect" href="https://fonts.googleapis.com">
</head>
```

Problems: charset after title triggers re-parse, sync script blocks CSS, preconnect comes after the stylesheet that needs it.

With Capo.js ordering:

```
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>My Page</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="stylesheet" href="/style.css">
  <script src="/app.js"></script>
</head>
```

## [Complete Weight Reference](#complete-weight-reference)

Unhead implements all 14 Capo.js weight levels. Lower weight = placed first in `<head>`:

| Priority | Weight | Tag | Why |
| --- | --- | --- | --- |
| 1 | -30 | `<meta http-equiv="Content-Security-Policy">` | **The Preload Scanner Stall:** Must be first. Late CSP tags can [disable the Chromium preload scanner](https://issues.chromium.org/issues/40273969), forcing resources to load sequentially. |
| 2 | -20 | `<meta charset>` | Encoding must be in first 1024 bytes |
| 3 | -15 | `<meta name="viewport">` | Prevents mobile layout shifts |
| 4 | -10 | `<base>` | Affects all relative URLs after it |
| 5 | 10 | `<title>` | First visible content in browser tab |
| 6 | 20 | `<link rel="preconnect">` | Early DNS + TLS for critical origins |
| 7 | 30 | `<script async>` | Fetch ASAP, non-blocking |
| 8 | 40 | `<style>` with `@import` | Render-blocking; must discover imports early |
| 9 | 50 | `<script src>` (sync) | Render-blocking but unavoidable |
| 10 | 60 | `<link rel="stylesheet">` / `<style>` | Render-blocking CSS |
| 11 | 70 | `<link rel="preload">` / `<link rel="modulepreload">` | Hints for soon-needed resources |
| 12 | 80 | `<script defer>` / `<script type="module">` | Non-blocking, post-parse execution |
| 13 | 90 | `<link rel="prefetch">` / `<link rel="dns-prefetch">` | Low-priority future navigation hints |
| 14 | 100 | Everything else | `<meta name="description">`, OG tags, JSON-LD |

### [Weight groups](#weight-groups)

**-30 to -10 (critical metadata)** must appear first - `<meta charset>` after 1024 bytes forces re-parsing, late viewport causes mobile layout shifts.

**10-20 (content & connections):** `<title>` for the browser tab, `<link rel="preconnect">` to establish connections before they're needed.

**30-60 (render-blocking resources):** `<script async>` gets discovered early for fetching. Sync `<script>` and `<link rel="stylesheet">` in their optimal relative order.

**70-100 (deferred & hints):** `<link rel="preload">`, `<script defer>`, `<link rel="prefetch">`, and everything else. These don't affect initial rendering.

## [Common Mistakes](#common-mistakes)

### [The "Head-Breaker"](#the-head-breaker)

A common mistake is placing an invalid tag (like `<img>` or `<iframe>`) inside the `<head>`. The browser's DOM builder implicitly closes the `<head>` and moves everything after the invalid tag to the `<body>`.

According to the **Web Almanac 2025**, invalid head markup remains a significant issue for **22% of all mobile pages**. This "breaks" SEO and performance because the parser discovers critical meta tags and styles too late.

### [Charset after the title](#charset-after-the-title)

```
<!-- Bad -->
<title>My Page</title>
<meta charset="utf-8">

<!-- Good -->
<meta charset="utf-8">
<title>My Page</title>
```

The [HTML spec](https://html.spec.whatwg.org/multipage/semantics.html#charset) requires `<meta charset>` within the first 1024 bytes. Content before it may trigger re-parsing.

### [CSS before preconnects](#css-before-preconnects)

```
<!-- Bad: preconnect arrives too late -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter">
<link rel="preconnect" href="https://fonts.googleapis.com">

<!-- Good -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter">
```

### [Sync scripts blocking CSS](#sync-scripts-blocking-css)

```
<!-- Bad: script blocks CSS loading -->
<script src="/analytics.js"></script>
<link rel="stylesheet" href="/style.css">

<!-- Good -->
<link rel="stylesheet" href="/style.css">
<script src="/analytics.js"></script>
```

Better - make analytics async:

```
<script async src="/analytics.js"></script>
<link rel="stylesheet" href="/style.css">
```

### [Preloads after what they preload](#preloads-after-what-they-preload)

```
<!-- Bad: preload discovered after stylesheet -->
<link rel="stylesheet" href="/style.css">
<link rel="preload" href="/font.woff2" as="font" crossorigin>

<!-- Good -->
<link rel="preload" href="/font.woff2" as="font" crossorigin>
<link rel="stylesheet" href="/style.css">
```

## [Automatic Sorting with Unhead](#automatic-sorting-with-unhead)

Every `useHead()` call produces Capo.js-ordered output:

```
import { useHead } from 'unhead'

useHead({
  meta: [
    { charset: 'utf-8' }, // weight: -20
    { name: 'viewport', content: 'width=device-width' }, // weight: -15
    { name: 'description', content: 'My page' }, // weight: 100
  ],
  title: 'My Page', // weight: 10
  link: [
    { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, // weight: 20
    { rel: 'stylesheet', href: '/style.css' }, // weight: 60
  ],
})
```

Unhead sorts the output by weight regardless of input order.

### [Custom priority](#custom-priority)

Override the default ordering with `tagPriority`:

```
useHead({
  script: [
    {
      src: '/critical-polyfill.js',
      tagPriority: 'critical', // -8 offset (higher priority)
    },
    {
      src: '/analytics.js',
      tagPriority: 'low', // +2 offset (lower priority)
    },
  ],
})
```

Priority aliases (applied as offsets to the tag's calculated weight):

- `'critical'` - subtracts 8 from weight
- `'high'` - subtracts 1
- `'low'` - adds 2
- Any number - exact weight override

Relative positioning with `before:` and `after:` prefixes:

```
useHead({
  script: [
    {
      src: '/my-script.js',
      tagPriority: 'before:script:analytics',
    },
  ],
})
```

## [Check Your Site](#check-your-site)

[Capo Analyzer](https://unhead.unjs.io/tools/capo-analyzer)Paste HTML or enter a URL to get instant feedback on your head tag ordering, with specific suggestions for improvement.

For the measured performance impact of head tag ordering, including the **"Nuxt Paradox"** where automatic ordering isn't enough to fix slow LCP, see [Does Head Tag Order Affect Performance?](https://unhead.unjs.io/learn/research/capo-performance-research). For how streaming SSR interacts with head management, see [Streaming SSR SEO](https://unhead.unjs.io/learn/research/streaming-head-performance).

Did this page help you?

  [Capo.js Performance](https://unhead.unjs.io/learn/research/capo-performance-research)

On this page

- [What is Capo.js?](#what-is-capojs)
- [Why order matters](#why-order-matters)
- [Complete Weight Reference](#complete-weight-reference)
- [Common Mistakes](#common-mistakes)
- [Automatic Sorting with Unhead](#automatic-sorting-with-unhead)
- [Check Your Site](#check-your-site)

[GitHub](https://github.com/unjs/unhead) [ Discord](https://discord.com/invite/275MBUBvgP)

[ /llms.txt](https://unhead.unjs.io/llms.txt)

[Part of the UnJS ecosystem](https://unjs.io/)

### Head Management

- [Getting Started](https://unhead.unjs.io/docs/typescript/head/guides/get-started/overview)
- [useHead](https://unhead.unjs.io/docs/typescript/head/api/composables/use-head)
- [useSeoMeta](https://unhead.unjs.io/docs/typescript/head/api/composables/use-seo-meta)
- [useHeadSafe](https://unhead.unjs.io/docs/typescript/head/api/composables/use-head-safe)
- [useScript](https://unhead.unjs.io/docs/typescript/head/api/composables/use-script)

### Schema.org

- [Getting Started](https://unhead.unjs.io/docs/typescript/schema-org/guides/get-started/overview)
- [useSchemaOrg](https://unhead.unjs.io/docs/typescript/schema-org/api/composables/use-schema-org)
- [Nodes](https://unhead.unjs.io/docs/typescript/schema-org/guides/core-concepts/nodes)
- [Recipes](https://unhead.unjs.io/docs/typescript/schema-org/guides/recipes/identity)

### Guides

- [Titles](https://unhead.unjs.io/docs/typescript/head/guides/core-concepts/titles)
- [Streaming SSR](https://unhead.unjs.io/docs/typescript/head/guides/core-concepts/streaming)
- [DOM Events](https://unhead.unjs.io/docs/typescript/head/guides/core-concepts/dom-event-handling)
- [Plugins](https://unhead.unjs.io/docs/typescript/head/guides/plugins/template-params)

### Tools

- [Meta Tag Generator](https://unhead.unjs.io/tools/meta-tag-generator)
- [OG Image Generator](https://unhead.unjs.io/tools/og-image-generator)
- [Schema.org Generator](https://unhead.unjs.io/tools/schema-generator)
- [Capo.js Analyzer](https://unhead.unjs.io/tools/capo-analyzer)

### Articles

- [What is Capo.js?](https://unhead.unjs.io/learn/guides/what-is-capo)

### Research

- [State of <head> in 2026](https://unhead.unjs.io/learn/research/state-of-head-2026)
- [Streaming Head Performance](https://unhead.unjs.io/learn/research/streaming-head-performance)
- [Capo.js Performance Research](https://unhead.unjs.io/learn/research/capo-performance-research)

Copyright © 2025-2026 Harlan Wilton - [MIT License](https://github.com/unjs/unhead/blob/main/license)