Unhead v2: The full-stack <head> package for any framework.
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.
<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'
})
- Vue -
@unhead/vue
: Installation & Reactivity Guide - React -
@unhead/react
: Installation & Reactivity Guide - Svelte -
@unhead/svelte
: Installation & Reactivity Guide - Solid.js -
@unhead/solid-js
: Installation & Reactivity Guide - Angular -
@unhead/angular
: Installation & Reactivity Guide
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.
- โ SSR +51% Faster 0.34ms โข 0.20ms (Benchmark: Medium Site)
Faster INP: Tag resolves are now cached between DOM renders. For large sites, this can lead to lower INP when switching between pages.
- โ CSR Page Switching +64% Faster 0.43ms โข 0.22ms (Benchmark: 10 Page Changes x useHead())
Leaner Core: Through aggressive code optimizations and moving some features to opt-in, we see an improvement in the bundle size.
- โ Client: 21% smaller - 13.6 kB (gz 5.3 kB) โข 10.7 kB (gz 4.5 kB) (Minimal: useHead() + createUnhead() )
- โ Server: 22% smaller - 10.3 kB (gz 4.1 kB) โข 8 kB (gz 3.4 kB) (Minimal: useHead() + createUnhead())
Benchmarks do not reflect real-world performance.
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.