The Nuxt team, in collaboration with the Chrome Aurora team at Google, is excited to announce the public beta release of Nuxt Scripts.
Nuxt Scripts is a better way to work with third-party scripts, providing improved performance, privacy, security, and developer experience.

Over a year ago, Daniel published the initial Nuxt Scripts RFC. The RFC proposed a module that would "allow third-party scripts to be managed and optimized, following best practices for performant and compliant websites".
Having personal experience with solving performance issues related to third-party scripts, I knew how difficult these performance optimizations could be. Nonetheless, I was keen to tackle the problem and took over the project.
With the RFC as the seed of the idea, I started prototyping what it could look like using Unhead.
Thinking about what I wanted to build exactly, I found that the real issue wasn't just how to load "optimized" third-party scripts but how to make working with third-party scripts a better experience overall.
94% of sites use at least one third-party provider, with the average site having five third-party providers.
We know that third-party scripts aren't perfect; they slow down the web, cause privacy and security issues, and are a pain to work with.
However, they are fundamentally useful and aren't going anywhere soon.
By exploring the issues with third-party scripts, we can see where improvements can be made.
Let's walk through adding a third-party script to your Nuxt app using a fictional tracker.js script that adds a track function to the window.
We start by loading the script using useHead.
useHead({ script: [{ src: '/tracker.js', defer: true }] })
However, let's now try to get the script functionality working in our app.
The following steps are common when working with third-party scripts in Nuxt:
<script setup>
// ❌ Oops, window is not defined!
// 💡 The window can't be directly accessed if we use SSR in Nuxt.
// 👉 We need to make this SSR safe
window.track('page_view', useRoute().path)
</script>
<script setup>
if (import.meta.client) {
// ❌ Oops, the script hasn't finished loading yet!
// 💡 A `defer` script may not be available while our Nuxt app hydrates.
// 👉 We need to wait for the script to be loaded
window.track('page_view', useRoute().path)
}
</script>
<script lang="ts" setup>
if (import.meta.client) {
useTimeoutFn(() => {
// ✅ It's working!
// ❌ Oops, types are broken.
// 💡 The `window` has strict types and nothing is defined yet.
// 👉 We need to manually augment the window
window.track('page_view', useRoute().path)
}, 1000 /* should be loaded in 1 second!? */)
}
</script>
<script lang="ts" setup>
declare global {
interface Window {
track: (e: string, p: string) => void
}
}
if (import.meta.client) {
useTimeoutFn(() => {
// ✅ It's working and types are valid!
// ❌ Oops, ad-blockers, GDPR and duplicate scripts
// 💡 There's a lot of hidden complexity in third-party scripts
// 👉 We need a better API
window.track('page_view', useRoute().path)
}, 1000)
}
</script>
For a visitor to start interacting with your Nuxt site, the app bundle needs to be downloaded and Vue needs to hydrate the app instance.
Loading third-party scripts can interfere with this hydration process, even when using async or defer. This slows down the network and blocks the main thread, leading to a degraded user experience and poor Core Web Vitals.
The Chrome User Experience Report shows Nuxt sites with numerous third-party resources typically have poorer Interaction to Next Paint (INP) and Largest Contentful Paint (LCP) scores.
To see how third-party scripts degrade performance, we can look at the Web Almanac 2022. The report shows that the top 10 third-party scripts average median blocking time is 1.4 seconds.
Of the top 10,000 sites, 58% of them have third-party scripts that exchange tracking IDs stored in external cookies, meaning they can track users across sites even with third-party cookies disabled.
While in many cases our hands are tied with the providers we use, we should try to minimize the amount of our end-users' data that we're leaking where possible.
When we do take on the privacy implications, it can then be difficult to accurately convey these in our privacy policies and build the consent management required to comply with regulations such as GDPR.
Security when using third-party scripts is also a concern. Third-party scripts are common attack vectors for malicious actors, most do not provide integrity hashes for their scripts, meaning they can be compromised and inject malicious code into your app at any time.
This composable sits between the <script> tag and the functionality added to window.{thirdPartyKey}.
For the <script> tag, the composable:
crossorigin and referrerpolicy to improve privacy and security.For the scripts API, it:
const { proxy, onLoaded } = useScript('/hello.js', {
trigger: 'onNuxtReady',
use() {
return window.helloWorld
}
})
onLoaded(({ greeting }) => {
// ✅ script is loaded! Hooks into Vue lifecycle
})
// ✅ OR use the proxy API - SSR friendly, called when script is loaded
proxy.greeting() // Hello, World!
declare global {
interface Window {
helloWorld: {
greeting: () => 'Hello World!'
}
}
}
window.helloWorld = {
greeting() {
console.log('Hello, World!')
}
}
The script registry is a collection of first-party integrations for common third-party scripts. As of release, we support 21 scripts, with more to come.

These registry scripts are fine-tuned wrappers around useScript with full type-safety, runtime validation of the script options (dev only) and environment variable support
For example, we can look at the Fathom Analytics script.
const { proxy } = useScriptFathomAnalytics({
// ✅ options are validated at runtime
site: undefined
})
// ✅ typed
proxy.trackPageview()
The registry includes several facade components, such as Google Maps, YouTube and Intercom.
Facade components are "fake" components that get hydrated when the third-party script loads. Facade components have trade-offs but can drastically improve your performance. See the What are Facade Components? guide for more information.
Nuxt Scripts provides facade components as accessible but headless, meaning they are not styled by default but add the necessary a16y data.

<script setup lang="ts">
const isLoaded = ref(false)
const isPlaying = ref(false)
const video = ref()
function play() {
video.value?.player.playVideo()
}
function stateChange(state) {
isPlaying.value = state.data === 1
}
</script>
<template>
<ScriptYouTubePlayer ref="video" video-id="d_IFKP1Ofq0" @ready="isLoaded = true" @state-change="stateChange">
<template #awaitingLoad>
<div class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 h-[48px] w-[68px]">
<svg height="100%" version="1.1" viewBox="0 0 68 48" width="100%"><path d="M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z" fill="#f00" /><path d="M 45,24 27,14 27,34" fill="#fff" /></svg>
</div>
</template>
</ScriptYouTubePlayer>
</template>
The useScript composable gives you full control over how and when your scripts are loaded, by either providing a custom trigger or manually calling the load() function.
Building on top of this, Nuxt Scripts provides advanced triggers to make it even easier.
const cookieConsentTrigger = useScriptTriggerConsent()
const { proxy } = useScript<{ greeting: () => void }>('/hello.js', {
// script will only be loaded once the consent has been accepted
trigger: cookieConsentTrigger
})
// ...
function acceptCookies() {
cookieConsentTrigger.accept()
}
// greeting() is queued until the user accepts cookies
proxy.greeting()
In many cases, we're loading third-party scripts from a domain that we don't control. This can lead to a number of issues:
To mitigate this, Nuxt Scripts provides a way to bundle third-party scripts into your public directory without any extra work.
useScript('https://cdn.jsdelivr.net/npm/js-confetti@latest/dist/js-confetti.browser.js', {
bundle: true,
})
The script will now be served from /_scripts/{hash} on your own domain.
As we saw, there are many opportunities to improve third-party scripts for developers and end-users.
The initial release of Nuxt Scripts has solved some of these issues, but there's still a lot of work ahead of us.
The next items on the roadmap are:
We'd love to have your contribution and support.
To get started with Nuxt Scripts, we've created a tutorial to help you get up and running.
And a big thank you to the early contributors.
