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/npm/js-confetti@latest/dist/js-confetti.browser.js', {
bundle: true,
})
現在,該指令碼將從您自己的域的 /_scripts/{hash}
提供。
未完待續
正如我們所見,在第三方指令碼方面,為開發者和終端使用者提供改進的機會有很多。
Nuxt Scripts 的初次釋出解決了其中**一些**問題,但我們前面還有很多工作要做。
路線圖上的下一個專案是:
我們非常希望得到您的貢獻和支援。
入門
為了幫助您開始使用 Nuxt Scripts,我們建立了一個教程,幫助您快速上手。
鳴謝
並非常感謝早期貢獻者。