Skip to content

脚本优化

🙋 第三方脚本拖慢页面加载?next/script 提供多种加载策略,让你精确控制脚本优先级。

为什么使用 next/script#

问题原生 scriptnext/script
阻塞渲染常见✅ 可控
加载时机固定✅ 多策略
重复加载可能✅ 自动去重
错误处理✅ 内置回调

基础用法#

app/layout.tsx
import Script from 'next/script'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body>
{children}
<Script src="https://example.com/analytics.js" />
</body>
</html>
)
}

加载策略#

beforeInteractive#

页面可交互前加载,用于关键脚本:

<Script
src="https://polyfill.io/v3/polyfill.min.js"
strategy="beforeInteractive"
/>

🔶 注意:只能在 app/layout.tsxpages/_document.tsx 中使用。

适用场景:

afterInteractive(默认)#

页面可交互后立即加载:

<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
strategy="afterInteractive"
/>

适用场景:

lazyOnload#

浏览器空闲时加载:

<Script src="https://connect.facebook.net/en_US/sdk.js" strategy="lazyOnload" />

适用场景:

worker(实验性)#

在 Web Worker 中运行:

next.config.ts
const config: NextConfig = {
experimental: {
nextScriptWorkers: true,
},
}
<Script src="https://example.com/heavy-script.js" strategy="worker" />

内联脚本#

使用 dangerouslySetInnerHTML#

<Script id="inline-script" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
`}
</Script>

使用模板字符串#

<Script
id="structured-data"
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Organization',
'name': 'My Company',
'url': 'https://example.com',
}),
}}
/>

🔶 重要:内联脚本必须有唯一的 id 属性。

事件回调#

onLoad#

<Script
src="https://maps.googleapis.com/maps/api/js"
onLoad={() => {
console.log('Google Maps 加载完成')
initMap()
}}
/>

onReady#

每次组件挂载时触发(包括客户端导航后):

<Script
src="https://example.com/widget.js"
onReady={() => {
console.log('脚本已就绪')
// 初始化插件
window.Widget.init()
}}
/>

onError#

<Script
src="https://example.com/script.js"
onError={(e) => {
console.error('脚本加载失败:', e)
// 上报错误或显示回退 UI
}}
/>

常见第三方脚本#

Google Analytics#

app/layout.tsx
import Script from 'next/script'
const GA_ID = process.env.NEXT_PUBLIC_GA_ID
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body>
{children}
{GA_ID && (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_ID}');
`}
</Script>
</>
)}
</body>
</html>
)
}

Google Tag Manager#

app/layout.tsx
import Script from 'next/script'
const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body>
{GTM_ID && (
<noscript>
<iframe
src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
height="0"
width="0"
style={{ display: 'none', visibility: 'hidden' }}
/>
</noscript>
)}
{children}
{GTM_ID && (
<Script id="gtm" strategy="afterInteractive">
{`
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${GTM_ID}');
`}
</Script>
)}
</body>
</html>
)
}

百度统计#

app/layout.tsx
import Script from 'next/script'
const BAIDU_ID = process.env.NEXT_PUBLIC_BAIDU_ID
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body>
{children}
{BAIDU_ID && (
<Script id="baidu-analytics" strategy="afterInteractive">
{`
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?${BAIDU_ID}";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
`}
</Script>
)}
</body>
</html>
)
}

在线客服(如 Crisp)#

components/CrispChat.tsx
'use client'
import Script from 'next/script'
const CRISP_ID = process.env.NEXT_PUBLIC_CRISP_ID
export function CrispChat() {
if (!CRISP_ID) return null
return (
<Script
id="crisp-chat"
strategy="lazyOnload"
dangerouslySetInnerHTML={{
__html: `
window.$crisp=[];
window.CRISP_WEBSITE_ID="${CRISP_ID}";
(function(){
d=document;
s=d.createElement("script");
s.src="https://client.crisp.chat/l.js";
s.async=1;
d.getElementsByTagName("head")[0].appendChild(s);
})();
`,
}}
/>
)
}
app/layout.tsx
import { CrispChat } from '@/components/CrispChat'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body>
{children}
<CrispChat />
</body>
</html>
)
}

地图(Google Maps)#

components/GoogleMap.tsx
'use client'
import { useRef, useEffect } from 'react'
import Script from 'next/script'
interface GoogleMapProps {
center: { lat: number; lng: number }
zoom: number
}
export function GoogleMap({ center, zoom }: GoogleMapProps) {
const mapRef = useRef<HTMLDivElement>(null)
function initMap() {
if (mapRef.current && window.google) {
new window.google.maps.Map(mapRef.current, {
center,
zoom,
})
}
}
return (
<>
<Script
src={`https://maps.googleapis.com/maps/api/js?key=${process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY}`}
strategy="lazyOnload"
onReady={initMap}
/>
<div ref={mapRef} className="w-full h-96" />
</>
)
}

社交分享#

components/TwitterShare.tsx
'use client'
import Script from 'next/script'
export function TwitterShare() {
return (
<>
<Script
src="https://platform.twitter.com/widgets.js"
strategy="lazyOnload"
/>
<a
className="twitter-share-button"
href="https://twitter.com/intent/tweet"
data-size="large"
>
Tweet
</a>
</>
)
}

条件加载#

根据环境#

{
process.env.NODE_ENV === 'production' && (
<Script
src="https://analytics.example.com/script.js"
strategy="afterInteractive"
/>
)
}

根据用户同意#

components/Analytics.tsx
'use client'
import Script from 'next/script'
import { useConsent } from '@/hooks/useConsent'
export function Analytics() {
const { hasConsent } = useConsent()
if (!hasConsent) return null
return (
<Script
src="https://analytics.example.com/script.js"
strategy="afterInteractive"
/>
)
}

路由特定脚本#

app/checkout/layout.tsx
import Script from 'next/script'
export default function CheckoutLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<>
{children}
{/* 只在结账页面加载支付脚本 */}
<Script src="https://js.stripe.com/v3/" strategy="afterInteractive" />
</>
)
}

性能优化#

推迟非关键脚本#

// ❌ 所有脚本同时加载
<Script src="/analytics.js" />
<Script src="/chat.js" />
<Script src="/social.js" />
// ✅ 分优先级加载
<Script src="/analytics.js" strategy="afterInteractive" />
<Script src="/chat.js" strategy="lazyOnload" />
<Script src="/social.js" strategy="lazyOnload" />

使用 DNS 预解析#

app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<head>
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
<link rel="preconnect" href="https://www.google-analytics.com" />
</head>
<body>{children}</body>
</html>
)
}

自托管脚本#

// 将第三方脚本下载到本地
<Script src="/scripts/analytics.js" strategy="afterInteractive" />

脚本组件封装#

components/ThirdPartyScripts.tsx
import Script from 'next/script'
interface ThirdPartyScriptsProps {
gtmId?: string
gaId?: string
crispId?: string
}
export function ThirdPartyScripts({
gtmId,
gaId,
crispId,
}: ThirdPartyScriptsProps) {
return (
<>
{/* Google Tag Manager */}
{gtmId && (
<Script id="gtm" strategy="afterInteractive">
{`(function(w,d,s,l,i){...})(window,document,'script','dataLayer','${gtmId}');`}
</Script>
)}
{/* Google Analytics */}
{gaId && (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
strategy="afterInteractive"
/>
<Script id="ga" strategy="afterInteractive">
{`window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${gaId}');`}
</Script>
</>
)}
{/* Crisp Chat */}
{crispId && (
<Script id="crisp" strategy="lazyOnload">
{`window.$crisp=[];window.CRISP_WEBSITE_ID="${crispId}";...`}
</Script>
)}
</>
)
}
app/layout.tsx
import { ThirdPartyScripts } from '@/components/ThirdPartyScripts'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body>
{children}
<ThirdPartyScripts
gtmId={process.env.NEXT_PUBLIC_GTM_ID}
gaId={process.env.NEXT_PUBLIC_GA_ID}
crispId={process.env.NEXT_PUBLIC_CRISP_ID}
/>
</body>
</html>
)
}

常见问题#

🤔 Q: 脚本重复加载怎么办?

next/script 自动去重,确保使用相同的 srcid

// 这两个只会加载一次
<Script src="https://example.com/script.js" />
<Script src="https://example.com/script.js" />

🤔 Q: 如何在脚本加载后执行代码?

使用 onReady 回调:

<Script
src="https://example.com/sdk.js"
onReady={() => {
window.SDK.init({ key: 'xxx' })
}}
/>

🤔 Q: 开发环境和生产环境加载不同脚本?

const isDev = process.env.NODE_ENV === 'development'
<Script
src={isDev
? 'https://sandbox.example.com/sdk.js'
: 'https://example.com/sdk.js'
}
/>

下一篇将介绍元数据与 SEO,学习如何优化网站的搜索引擎表现。

-EOF-