🙋 第三方脚本拖慢页面加载?next/script 提供多种加载策略,让你精确控制脚本优先级。
为什么使用 next/script#
| 问题 | 原生 script | next/script |
|---|---|---|
| 阻塞渲染 | 常见 | ✅ 可控 |
| 加载时机 | 固定 | ✅ 多策略 |
| 重复加载 | 可能 | ✅ 自动去重 |
| 错误处理 | 无 | ✅ 内置回调 |
基础用法#
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.tsx 或 pages/_document.tsx 中使用。
适用场景:
- Polyfills
- Bot 检测
- Cookie 同意管理
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 中运行:
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#
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#
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> )}百度统计#
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)#
'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); })(); `, }} /> )}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)#
'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" /> </> )}社交分享#
'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" /> )}根据用户同意#
'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" /> )}路由特定脚本#
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 预解析#
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" />脚本组件封装#
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> )} </> )}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 自动去重,确保使用相同的 src 或 id:
// 这两个只会加载一次<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-