Unhead v2: The full-stack <head> package for any framework.

5 min read
Published

I'm thrilled to announce the release of Unhead v2, a major milestone in Unhead becoming the most performant and feature-complete head manager for all reactive JavaScript frameworks.

TL;DR - What's New in v2

  • Multi-framework Support: Added support for React, Svelte, Solid.js, and Angular
  • Performance: 51% faster SSR, 64% faster page switching, 21% smaller bundle size
  • Capo.js Integration: Automatic tag sorting for optimal page loading
  • Leaner Package: 14kb reduction in node_modules, single dependency
  • New Docs: ๐Ÿ‘‹

State of head management

All JavaScript frameworks will run into the same challenge: how can they effectively manage tags and attributes outside the DOM entry in a server-side rendered world?

<!DOCTYPE html>
<html>
  <head>
    <!-- JavaScript frameworks need to manage tags here server & client-side -->
  </head>
  <body> <!-- And probably body attributes -->
    <!-- But also this -->
    <div id="your-javascript-framework"></div>
    <!-- and this... -->
    <script src="/your-javascript-framework.js"></script>
  </body>
</html>

As each framework has its unique constraints, they have all come up with their own solutions to this problem. Most of them converged on a head provider pattern, where the framework allows you to wrap tags in a specific component (or provider) and they figure out the rest.

ComponentFoo
<HeadProvider>
  <title>My Page</title>
  <meta name="description" content="My page description" />
  <link rel="canonical" href="https://example.com/my-page" />
</HeadProvider>

Seen as: <Helmet>, <Head>, <svelte:head> or more granular tags such as <Title>, <Meta>, <Link>, etc.

Ecosystem

Traditionally frameworks like React, Vue and Angular have left it up to their respective ecosystems to solve this problem. Vue had Vue Meta and VueUse Head and React had and continues to have React Helmet.

We see some frameworks shifting towards providing simple support as part of their core, such as in React v19 and in Angular v14.

These solutions tend to be reliant on the component pattern, which is limited in its capabilities.

A quick recap on Unhead

Unhead started out humbly 5 years ago as @vueuse/head. Since then it's joined UnJS as a high-quality, single-purpose package that works anywhere and is downloaded over ~100k times a day.

Built from bullet-proof primitives such as useHead() and useSeoMeta(), Unhead is building out the ecosystem of head management which is being realized through the v2 release.

v2 Release

For the full changelog of changes please reference the v2 roadmap GitHub Issue.

New Framework Supported

Unhead started as a Vue-focused solution, but with v2, we've expanded to support all major frontend frameworks with deep reactivity integrations.

import { useHead } from '@unhead/typescript'

useHead({
  title: '๐Ÿ‘‹ TypeScript'
})

Each framework integration provides the same powerful features with idiomatic patterns for that ecosystem.

โšก Runtime Performance Improvements

Every tenth of a millisecond counts when it comes to performance. In this release, much of the core has been rewritten to improve performance and tree-shakability.

Rewritten Core: With the core being rewritten, optimizations were done to improve the fast-path times.

Faster INP: Tag resolves are now cached between DOM renders. For large sites, this can lead to lower INP when switching between pages.

Leaner Core: Through aggressive code optimizations and moving some features to opt-in, we see an improvement in the bundle size.

Benchmarks do not reflect real-world performance.

00.1ms0.2ms0.3ms0.4ms0.34ms0.20ms+51%SSR TimeMedium Site0.43ms0.22ms+64%Page Changes10x useHead()13.6kB10.7kB-21%Client SizeMinimal Build10.3kB8.0kB-22%Server SizeMinimal BuildBeforeAfter

Capo.js Sorting

Unhead v2 now applies capo.js sorting to all tags by default which provides optimizations to improve the performance of your site for end users. This sorting follows best practices for resource loading order in the document head.

<head>
<script defer src="defer-script.js"></script>
<script src="sync-script.js"></script>
<style>.sync-style { color: red }</style>
<link rel="modulepreload" href="modulepreload.js">
<script src="async-script.js" async></script>
<link rel="preload" href="preload.js">
<link rel="stylesheet" href="sync-styles.css">
<title>title</title>
<link rel="preconnect" href="https://example.com">
<link rel="dns-prefetch" href="https://example.com">
<link rel="prefetch" href="https://example.com">
<link rel="prerender" href="https://example.com">
<meta name="description" content="description">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

๐Ÿ“ฆ Bundle Improvements

Single dependency Unhead now relies on a single dependency, hookable, used for pluggability.

  • โœ… 5 fewer dependencies

ESM & dropped workspace packages Only ESM is now published, workspace packages are deprecated for subpath exports.

  • โœ… 14kb reduction in node_modules

๐Ÿ”„ Upgrading to v2

The migration path is straightforward; a legacy subpath build is provided to ease the transition.

๐Ÿ”ฎ The Future

Now that Unhead has a battle-tested core and supports all major frameworks, we can start to build out the ecosystem of head management. Here's what's on our roadmap:

Short Term: Improved Tag Validation

For several tags, certain attributes are required when another attribute is used. For example, the <link> tag only requires an as attribute when a rel="preload" is being used.

The type system does not enforce this to avoid potential type juggling; however, it can lead to mistakes.

To catch broken tags we have several options:

  • Improve the type system for optionally required attributes
  • Implement ESLint rules to catch anything the types haven't
  • Use a runtime plugin in development to validate resolved tags

Medium Term: Third-Party Scripts

While Unhead already has a lower-level way to work with third-party scripts useScript(), we can make them easier and more secure to use by introducing higher-level composables, such as useGoogleAnalytics(), useFacebookPixel(), etc.

These would hook into framework lifecycles to ensure optimal performance and security while providing improved developer experience.

const { proxy } = useGoogleAnalytics({
  id: 'UA-123456789',
})

proxy.gtag('page_view', {
  page_path: '/my-page',
})

Exploratory: OG Image

Turn HTML templates into OG images with a simple composable.

useOgImage('<div class="bg-red-500">my image :)</div>')

๐Ÿ™ Thank You

This release wouldn't be possible without our amazing community. Special thanks to all contributors who helped with code, testing, and documentation.