React、Next.js 和 TypeScript 已经成为专业 Web 应用的默认技术栈。React 提供组件模型,Next.js 在此之上加入路由、渲染和数据获取,TypeScript 则能在代码运行之前就捕获一整类错误。用得好,它们能做出对访客来说很快、对开发者来说易于维护的网站。
用得不好,同样的技术栈也会做出塞满数 MB JavaScript 的缓慢页面。本文介绍决定成败的关键理念,并提供可直接用于 Next.js App Router 项目的代码。
为什么这套技术栈成为默认选择
- React 把界面描述为一棵可复用组件的树。按钮、卡片乃至整个页面都是其数据的函数,让界面可预测、易测试。
- Next.js 把 React 变成完整的框架:基于文件的路由、服务端渲染、缓存、图片和字体优化,以及生产构建流程。
- TypeScript 为 JavaScript 加上静态类型。编辑器知道每个对象的结构,重构更安全,许多运行时错误变成了编译期错误。
三者结合,能覆盖从营销网站到复杂仪表盘的各种需求,前端和服务端使用同一种语言。
Server Components 改变了默认方式
在 Next.js App Router 中,除非你主动选择,否则每个组件都是 React Server Component。Server Components 只在服务端运行,可以直接读取数据库或调用内部 API,而且它们的 JavaScript 永远不会发送到浏览器,发送的只有渲染结果。
只有需要交互的组件才需要加上 'use client' 指令:状态、副作用、事件处理或浏览器 API。最佳实践是让这些客户端组件保持小巧,并放在组件树的叶子节点上:
// app/blog/page.tsx: a Server Component (the default in the App Router)
import { getPosts } from '@/lib/posts'
import { LikeButton } from './like-button'
export default async function BlogPage() {
const posts = await getPosts() // runs on the server, so no extra API route is needed
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
<LikeButton />
</li>
))}
</ul>
)
}// app/blog/like-button.tsx: only this small component ships JavaScript
'use client'
import { useState } from 'react'
export function LikeButton() {
const [liked, setLiked] = useState(false)
return (
<button type="button" aria-pressed={liked} onClick={() => setLiked((value) => !value)}>
{liked ? 'Liked' : 'Like'}
</button>
)
}列表、数据访问和页面结构不会给浏览器带来任何负担,只有按钮需要 hydration。在内容为主的页面上,这通常能去掉大部分客户端 JavaScript。
选择合适的渲染策略
Next.js 允许你按路由决定 HTML 在何时生成。选对方案是你所做的最重要的性能决策:
- 静态渲染:HTML 在构建时生成,从缓存或 CDN 提供。最适合营销页面和文档。
- 增量静态再生成(ISR):静态页面在一段时间后或通过
revalidatePath()按需刷新。最适合博客、商品目录和由 CMS 驱动的页面。 - 动态渲染:每个请求都生成 HTML。个性化页面、仪表盘以及依赖 cookie 的内容必须使用它。
- 流式渲染:把页面中较慢的部分包裹在
<Suspense>中,页面框架立即显示,慢数据准备好后再流式传入。
下面是一个带缓存、每五分钟刷新一次、并为每篇文章生成 SEO 元数据的动态博客路由。注意,在当前的 Next.js 版本中 params 是一个 Promise:
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getPost } from '@/lib/posts'
type Props = { params: Promise<{ slug: string }> }
// Serve the cached page, regenerating it at most every five minutes
export const revalidate = 300
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
if (!post) return {}
return {
title: post.title,
description: post.excerpt,
openGraph: { type: 'article', title: post.title, images: [post.coverUrl] },
}
}
export default async function PostPage({ params }: Props) {
const { slug } = await params
const post = await getPost(slug)
if (!post) notFound() // renders the 404 page with the correct status code
return (
<article>
<h1>{post.title}</h1>
{/* … */}
</article>
)
}物有所值的 TypeScript
从第一次提交就开启 strict 模式,之后再补开会非常痛苦。noUncheckedIndexedAccess 也值得开启,因为它会迫使你处理数组元素或映射项不存在的情况:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true
}
}记住,类型在运行时会消失。跨越边界的数据(表单输入、API 响应、URL 参数、环境变量)必须经过校验,而不仅仅是标注类型。像 zod 这样的 schema 库可以用一个定义同时提供两者:
import { z } from 'zod'
export const PostSchema = z.object({
slug: z.string().regex(/^[a-z0-9-]+$/),
title: z.string().min(1).max(200),
publishedAt: z.coerce.date(),
tags: z.array(z.string()).default([]),
})
// One source of truth for the runtime check and the compile-time type
export type Post = z.infer<typeof PostSchema>
export function parsePost(input: unknown): Post {
return PostSchema.parse(input) // throws a descriptive error for invalid data
}用可辨识联合类型(discriminated union)显式地建模状态,例如 { status: 'loading' } | { status: 'error'; message: string } | { status: 'ready'; data: Post[] }。这样编译器会强制每个组件处理加载和错误状态,而不是假设一切顺利。
使用 Server Actions 修改数据
Server Actions 让表单可以直接调用服务端函数,无需手写 API 路由。它们很方便,但底层仍然是公开的 HTTP 端点。请像对待 API 一样对待每个 action:校验输入,在服务端检查身份认证和权限,并返回表单可以展示的结构化错误。在界面上隐藏一个按钮并不等于安全。
性能与无障碍检查清单
- 使用
next/image并设置准确的sizes属性,让手机永远不会下载桌面尺寸的图片;只把首屏的主图标记为priority。 - 使用
next/font加载字体,实现自托管并避免布局偏移。 - 用
Promise.all并行获取相互独立的数据,而不是一连串顺序执行的await。 - 让客户端组件保持小巧并放在叶子节点,延迟加载地图、图表和编辑器等重量级组件。
- 在生产环境中跟踪 Core Web Vitals。“良好”意味着在第 75 百分位上,Largest Contentful Paint 低于 2.5 秒、Interaction to Next Paint 低于 200 毫秒、Cumulative Layout Shift 低于 0.1。
- 使用语义化 HTML(
<nav>、<main>、<article>、真正的<button>),为每个表单字段添加标签,保持焦点状态可见,并检查颜色对比度。
要点回顾
- 默认使用 Server Components,只在需要交互的地方添加
'use client'。 - 按路由选择渲染策略:能静态就静态,内容页用 ISR,只有必要时才用动态渲染。
- 将严格的 TypeScript 与每个边界上的运行时校验结合起来。
- 衡量真实用户的性能,并把无障碍视为“完成”定义的一部分。
总结
现代 React 技术栈会回报那些清楚自己代码在哪里运行的团队。默认在服务端渲染,只为真正的交互发送 JavaScript,并在边界处强制类型和校验,你的应用就能加载迅速、排名靠前,并在不断成长中依然易于修改。
延伸阅读
- #Next.js
- #React
- #TypeScript
- #Web Performance




