最近学完了官网 tutorial(中文官网),作为入门尝鲜教程质量还是不错的,而且教程初始 code template 也很容易,按部就班就能熟悉 Next.js 里面一些设计理念与开发模式,最终实现并在 Vercel 部署一个 dashboard 项目,看看我的 Demo 👉 入口
Next.js 的概念特别多,官网有很详尽的说明,学习新技术的第一手资料永远来自官方文档。本文不做翻译工作,而是把涉及性能/体验优化相关的 feature 单拎出来汇总,好了,下面进入正文
Next.js 是一款基于 React 的前端应用开发框架,旨在开发出兼具 high-quality & high-performance 前端应用,功能非常强大也如其官网一样非常 cool:
恰逢 Next.js 2024开发者峰会,我去搞了一张虚拟入场证件 😬:
上面提到的“内置性能优化相关组件”主要包含下面几种:
Web Almanac 网站统计到,互联网上的静态资源中,图片资源占比远比 HTML、CSS、Javascript、Font 资源高,而且图片又往往决定着一个网站的 LCP 性能指标,因此,Next.js 扩展了<img>
标签,提供next/image组件内置实现了以下优化:
使用:
// 本地图片
import Image from 'next/image'
import profilePic from './me.png'
export default function Page() {
return (
<Image
src={profilePic}
alt="Picture of the author"
// width={500} automatically provided
// height={500} automatically provided
// blurDataURL="data:..." automatically provided
// placeholder="blur" // Optional blur-up while loading
priority // fetchPriority="high" 提高加载优先级
/>
)
}
注:】不支持await import
或require
,静态import
是为了构建时分析图片信息
// 网络图片
import Image from 'next/image'
export default function Page() {
return (
<Image
src="https://s3.amazonaws.com/my-bucket/profile.png"
alt="Picture of the author"
width={500} // manual set
height={500} // manual set
/>
)
}
注:】需手动设置宽高
最佳实践:
video 资源 self-hosted 好处:
Next.js 提供的方案:@vercel/blob
在 Next.js 中引用字体使用,构建阶段将下载好字体资源与其他静态资源一齐托管在自己的服务器上,不需要再向 Google 发出额外网络请求下载字体,这有利于隐私保护和性能提升。组件提供的优化能力:
// app/fonts.ts
import { Inter, Roboto_Mono } from 'next/font/google'
export const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
export const roboto_mono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
})
// app/layout.tsx
import { inter } from './fonts'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.className}>
<body>
<div>{children}</div>
</body>
</html>
)
}
// app/page.tsx
import { roboto_mono } from './fonts'
export default function Page() {
return (
<>
<h1 className={roboto_mono.className}>My page</h1>
</>
)
}
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
const roboto_mono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={`${inter.variable} ${roboto_mono.variable}`}>
<body>{children}</body>
</html>
)
}
// app/global.css
html {
font-family: var(--font-inter);
}
h1 {
font-family: var(--font-roboto-mono);
}
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./app/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)'],
mono: ['var(--font-roboto-mono)'],
},
},
},
plugins: [],
}
Metadata 提供以下功能:
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '...',
description: '...',
}
export default function Page() {}
import type { Metadata, ResolvingMetadata } from 'next'
type Props = {
params: { id: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params, searchParams }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
// read route params
const id = params.id
// fetch data
const product = await fetch(`https://.../${id}`).then((res) => res.json())
// optionally access and extend (rather than replace) parent metadata
const previousImages = (await parent).openGraph?.images || []
return {
title: product.title,
openGraph: {
images: ['/some-specific-page-image.jpg', ...previousImages],
},
}
}
export default function Page({ params, searchParams }: Props) {}
注:】通过异步 generateMetadata
生成的 Metadata
仅支持在 Server Components 中配置,Next.js 会等异步执行完后再将 UI 流式返回,这确保流式第一段响应能包含正确的 <head>
标签
组件提供的优化能力:
beforeInteractive
、afterInteractive
、lazyLoad
、worker
(利用 partytown 在 Web worker 中加载第三方依赖)// app/dashboard/layout.tsx
import Script from 'next/script'
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<>
<section>{children}</section>
<Script src="https://example.com/script.js" />
</>
)
}
beforeInteractive
:页面 hydration 之前afterInteractive
:页面 hydration 之后lazyLoad
:浏览器 Idle 时间加载worker
(实验中):开启 Web worker 加载,非CRP的JS应当放在Worker中执行,让步主线程:
id
prop required,便于 Next.js 跟踪优化 Scripts<Script id="show-banner">
{`document.getElementById('banner').classList.remove('hidden')`}
</Script>
// OR
<Script
id="show-banner"
dangerouslySetInnerHTML={{
__html: `document.getElementById('banner').classList.remove('hidden')`,
}}
/>
onLoad
:脚本加载完成onReady
:脚本加载完成后,每次组件挂载完成onError
:脚本加载失败类似于 webpack-bundle-analyzer,Next.js 也提供了@next/bundle-analyzer
供产物分析,使用如下:
// pnpm add @next/bundle-analyzer -D
/** @type {import('next').NextConfig} */
const nextConfig = {}
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer(nextConfig)
// ANALYZE=true pnpm build
Next.js 已优化的三方依赖列表,按需引入,非全量引入
Next.js 已 external 掉的三方依赖列表,类似于 webpack externals ,指定依赖以 CDN 形式引入,从而减少构建产物体积、控制加载时机等
按需加载组件或第三方依赖,能加快页面加载速度
next/dynamic
// app/page.ts
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
// Client Components:
const ComponentA = dynamic(() => import('../components/A'))
const ComponentB = dynamic(() => import('../components/B'))
// 主动关闭SSR
const ComponentC = dynamic(() => import('../components/C'), { ssr: false })
// custom loading component
const WithCustomLoading = dynamic(
() => import('../components/WithCustomLoading'),
{
loading: () => <p>Loading...</p>,
}
)
export default function ClientComponentExample() {
const [showMore, setShowMore] = useState(false)
return (
<div>
{/* Load immediately, but in a separate client bundle */}
<ComponentA />
{/* Load on demand, only when/if the condition is met */}
{showMore && <ComponentB />}
<button onClick={() => setShowMore(!showMore)}>Toggle</button>
{/* Load only on the client side */}
<ComponentC />
{/* custom loading */}
<WithCustomLoading />
</div>
)
}
React.lazy
& Suspense
import()
动态引入'use client'
import { useState } from 'react'
const names = ['Tim', 'Joe', 'Bel', 'Lee']
export default function Page() {
const [results, setResults] = useState()
return (
<div>
<input
type="text"
placeholder="Search"
onChange={async (e) => {
const { value } = e.currentTarget
// Dynamically load fuse.js
const Fuse = (await import('fuse.js')).default
const fuse = new Fuse(names)
setResults(fuse.search(value))
}}
/>
<pre>Results: {JSON.stringify(results, null, 2)}</pre>
</div>
)
}
// components/hello.js
'use client'
export function Hello() {
return <p>Hello!</p>
}
// app/page.ts
import dynamic from 'next/dynamic'
const HelloComponent = dynamic(() =>
import('../components/hello').then((mod) => mod.Hello)
)
内置支持性能指标衡量与上报,太细了我的哥!!
// app/_components/web-vitals.js
'use client'
import { useReportWebVitals } from 'next/web-vitals'
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric)
})
}
// app/layout.js
import { WebVitals } from './_components/web-vitals'
export default function Layout({ children }) {
return (
<html>
<body>
<WebVitals />
{children}
</body>
</html>
)
}
Web Vitals 已无需多言
随着应用迭代,功能越来越丰富,我们在开发和构建的过程中,项目运行会消耗越来越多的系统资源,Next.js 提供了一些策略方法去优化:
experimental.webpackMemoryOptimizations: true
next build --experimental-debug-memory-usage
node --heap-prof node_modules/next/dist/bin/next build
// instrumentation.ts
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel('next-app')
}
// app/layout.tsx
import { GoogleTagManager } from '@next/third-parties/google'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">Google Analytics - gtag.js
<GoogleTagManager gtmId="GTM-XYZ" />
<body>{children}</body>
</html>
)
}
// app/layout.tsx
import { GoogleAnalytics } from '@next/third-parties/google'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
<GoogleAnalytics gaId="G-XYZ" />
</html>
)
}
SSR 能带来诸多收益:
默认情况下,Next.js 中的组件都是服务端组件。需要注意的是 SSR 不能用以下 API:
首先是服务端:
React Server Component Payload
(RSC Payload
)RSC Payload
和 Client Component Javascript 在服务端渲染 HTML(类似 renderToString
?renderToPipeableStream
?)到了客户端:
RSC Payload
拿来调和客户端、服务端组件树,更新 DOMServer Components 的渲染方式分为 Static Render、Dynamic Render、Stream Render
构建时在服务端被渲染为静态内容,缓存下来,应对接下来的请求时直接返回,适用于纯静态的展示型 UI 或数据不变、用户无差别的 UI
与静态渲染相反,如果数据有千人千面特征,依赖每次请求要拿到如 Cookie、searchParams 等数据,就需要每次请求时实时渲染结果
动态渲染还是静态渲染的切换是 Next.js 自动完成的,会根据开发者所用 API 自行选择相应的渲染策略,比如 Dynamic Functions:
cookies()
headers()
unstable_noStore()
unstable_after()
searchParams prop
使用这些动态渲染 API 的路由,Next.js 将采用动态渲染策略
SSR 必须经历 A、B、C、D 序列化的、先后阻塞的步骤。服务器只有拿到所有数据才能开始渲染HTML,客户端只有拿到完整 JS 才能开始 hydration
将页面不同部分划分为不同 chunks,服务器渐进式返回,先返回先渲染先显示,不用等待所有的数据准备完毕。React Component 天然就是独立的 chunk,不依赖异步数据获取的 chunk 可直接返回,剩下的依次逐个返回
客户端渲染的好处:
'use client'
指令告知 Next.js 该组件为客户端组件即可:'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
不声明客户端组件默认服务端组件,使用了客户端API将报错:
对于整页初始加载:与 Server Component 一致
对于后续导航:类似于客户端 SPA 导航,不向服务器发起新的生成 HTML 的请求,而是利用 RSC Payload 去完成导航
Partial Prerender,部分预渲染,Next.js 在构建时会尽可能多得去预渲染组件,碰到被 Suspense 包裹的异步组件,fallback 中的 UI 也将先被预渲染。这样做的好处是合并多个请求,减少浏览器 Network 请求瀑布流
loading.tsx
是 Next.js 中的一种特殊文件,基于 Suspense
实现路由、组件级别加载态,可以自实现SkeletonComponent
或 SpinnerComponent
作为异步组件加载时的 fallback UI,能够有效提高 UX
Next.js 官网还有非常多的设计哲学与理念,值得反复推敲与思考。最近在推上也看到不少独立开发者基于 Next.js 快速开发一个产品出来,将自己的思路落地,它是一个从开发、部署、性能到后端提供一条龙服务的 Next Gen Web Framework,有点全栈的味儿了
完结