Nuxt 团队与Google 的 Chrome Aurora团队合作,很高兴宣布正式发布Nuxt Scripts.
Nuxt Scripts 是使用第三方脚本的更好方式,提供改进的性能、隐私、安全和开发者体验。

Nuxt Scripts 的发展历程
一年多前,Daniel 发布了最初的Nuxt Scripts RFC。该 RFC 提出了一个模块,旨在“允许第三方脚本按照高性能和合规网站的最佳实践进行管理和优化”。
由于我个人经验中曾解决过与第三方脚本相关的性能问题,我深知这些性能优化有多困难。尽管如此,我仍热衷于解决这个问题并接手了这个项目。
以 RFC 为最初的想法,我开始用原型思考它可能会Unhead.
是什么样子
。在思考我到底想构建什么时,我发现真正的问题不仅仅是如何加载“优化”的第三方脚本,而是如何让使用第三方脚本的整体体验更好。为什么要构建第三方脚本模块?
94% 的网站至少使用一个第三方提供商,平均每个网站有五个第三方提供商.
我们知道第三方脚本并非完美;它们会减慢网络速度,导致隐私和安全问题,并且难以处理。
然而,它们从根本上是有用的,并且短期内不会消失。
通过探索第三方脚本的问题,我们可以看到哪些方面可以改进。
😒 开发者体验:一个全栈的痛点
让我们来看看如何将一个虚构的 tracker.js 脚本添加到您的 Nuxt 应用程序中,该脚本将 track 函数添加到 window 对象中。
我们首先使用 useHead 加载脚本。
useHead({ script: [{ src: '/tracker.js', defer: true }] })
然而,现在让我们尝试让脚本功能在我们的应用程序中运行。
在 Nuxt 中使用第三方脚本时,以下步骤很常见:
- 所有内容都必须包装以确保 SSR 安全。
- 脚本是否已加载的检查不稳定。
- 为类型增强 window 对象。
<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>
🐌 性能:“为什么我的 Lighthouse 分数不能达到 100?”
要让访问者开始与您的 Nuxt 网站互动,需要下载应用程序包,并且 Vue 需要水合(hydrate)应用程序实例。
加载第三方脚本可能会干扰此水合过程,即使使用 async 或 defer。这会减慢网络速度并阻塞主线程,导致用户体验下降和Core Web Vitals.
用于格式化的Chrome 用户体验报告显示,包含大量第三方资源的 Nuxt 网站通常具有较差的Interaction to Next Paint (INP)等等Largest Contentful Paint (LCP)分数。
要了解第三方脚本如何降低性能,我们可以查看Web Almanac 2022。该报告显示,前 10 个第三方脚本的**平均中位阻塞时间为 1.4 秒**。
🛡️ 隐私与安全:不作恶?
在排名前 10,000 的网站中,有 58% 的网站包含的第三方脚本会交换存储在外部 Cookie 中的跟踪 ID,这意味着即使禁用了第三方 Cookie,它们也能在不同网站上跟踪用户。
虽然在许多情况下,我们对使用的提供商别无选择,但我们应该尽可能减少我们泄露的最终用户数据量。
当我们确实承担了隐私影响时,可能难以在我们的隐私政策中准确传达这些信息,并构建遵守 GDPR 等法规所需的同意管理。
使用第三方脚本时的安全性也是一个问题。第三方脚本是恶意行为者的常见攻击向量,大多数脚本不为其提供 integrity 哈希,这意味着它们随时可能被攻破并向您的应用程序注入恶意代码。
Nuxt Scripts 如何解决这些问题?
可组合函数:useScript
此可组合函数位于 <script> 标签和添加到 window.{thirdPartyKey} 的功能之间。
对于 <script> 标签,该可组合函数
- 提供脚本加载和错误状态的完全可见性
- 默认在 Nuxt 水合应用时加载脚本,以获得稍微更好的性能。
- 限制
crossorigin和referrerpolicy以提高隐私和安全性。 - 提供一种方法来延迟加载脚本直到您需要它为止。
对于脚本 API,它
- 提供脚本函数完全的类型安全
- 添加一个代理层,允许您的应用在脚本功能在不安全上下文(SSR、脚本加载前、脚本被阻止)中运行时也能运行
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!')
}
}
脚本注册表
用于格式化的脚本注册表是常见第三方脚本的第一方集成集合。截至发布,我们支持 21 个脚本,未来还会增加更多。

这些注册表脚本是围绕 useScript 的经过微调的包装器,具有完整的类型安全、脚本选项的运行时验证(仅限开发环境)和环境变量支持。
例如,我们可以看看Fathom Analytics脚本。
const { proxy } = useScriptFathomAnalytics({
// ✅ options are validated at runtime
site: undefined
})
// ✅ typed
proxy.trackPageview()
门面组件
注册表包含多个门面组件,例如Google 地图, YouTube等等Intercom.
门面组件是当第三方脚本加载时才被水合的“伪”组件。门面组件有其权衡,但可以显著提高您的性能。有关更多信息,请参阅什么是门面组件?指南。
Nuxt Scripts 提供门面组件,它们易于访问但无头,这意味着它们默认没有样式,但添加了必要的 a16y 数据。

<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>
同意管理与元素事件触发器
useScript 可组合函数通过提供自定义 trigger 或手动调用 load() 函数,让您完全控制脚本的加载方式和时间。
在此基础上,Nuxt Scripts 提供了高级触发器,使其变得更加简单。
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()
捆绑脚本
在许多情况下,我们正在从一个我们无法控制的域加载第三方脚本。这可能导致许多问题:
- 隐私:第三方脚本可以在网站之间跟踪用户。
- 安全:第三方脚本可能被入侵并注入恶意代码。
- 性能:额外的 DNS 查找会减慢页面加载速度。
- 开发者体验:已同意的脚本可能会被广告拦截器阻止。
为了缓解这种情况,Nuxt Scripts 提供了一种无需额外工作即可将第三方脚本捆绑到您的公共目录中的方法。
useScript('https://cdn.jsdelivr.net.cn/npm/js-confetti@latest/dist/js-confetti.browser.js', {
bundle: true,
})
现在,该脚本将从您自己的域的 /_scripts/{hash} 提供。
未完待续
正如我们所见,在第三方脚本方面,为开发者和最终用户提供改进的机会有很多。
Nuxt Scripts 的初次发布解决了其中**一些**问题,但我们前面还有很多工作要做。
路线图上的下一个项目是:
我们非常希望得到您的贡献和支持。
入门
为了帮助您开始使用 Nuxt Scripts,我们创建了一个教程,帮助您快速上手。
鸣谢
并非常感谢早期贡献者。
