告别白屏加载!React 应用性能与 SEO 的终极提升方案
各位 React 开发者,你是否曾经面临过这些困扰:
- 首屏加载时间过长,用户体验差?
- 搜索引擎无法有效索引你的应用内容?
- 大型应用的性能与可扩展性问题?
- 复杂应用架构的选型困惑?
在过去的九篇文章中,我们详细探讨了 React 客户端渲染(CSR)的各种优化方法。今天,我们将揭开全新的篇章,探索服务端渲染(SSR)与静态站点生成(SSG)这两种强大的渲染策略,帮助你的 React 应用更快、更强、对 SEO 更友好!
1. 为什么需要 SSR 与 SSG?
在深入技术细节前,让我们先理解这些渲染策略解决的核心问题:
scss
┌────────────────────────────────────────────────────┐
│ 渲染策略比较 │
├────────────┬───────────────┬───────────┬───────────┤
│ │ CSR(客户端渲染)│SSR(服务端渲染)│SSG(静态生成)│
├────────────┼───────────────┼───────────┼───────────┤
│ 首屏加载 │ 慢 │ 快 │ 最快 │
│ SEO支持 │ 差 │ 好 │ 好 │
│ 服务器负载 │ 低 │ 高 │ 低 │
│ 内容更新 │ 实时更新 │ 每次请求 │ 构建时生成 │
│ 交互性 │ 高 │ 中 │ 中 │
└────────────┴───────────────┴───────────┴───────────┘
Next.js:React SSR/SSG 的首选框架
Next.js 是 React SSR/SSG 应用开发的事实标准,它提供了:
- 零配置的服务端渲染
- 自动代码分割
- 基于文件系统的路由
- API 路由内置支持
- 多种渲染模式灵活切换
- 增量静态再生(ISR)
让我们从头开始构建一个 Next.js 应用,体验这一切的美妙:
bash
# 创建新的Next.js应用
npx create-next-app@latest my-nextjs-app
cd my-nextjs-app
npm run dev
2. Next.js 应用核心结构与配置
理解 Next.js 的项目结构是掌握它的第一步:
ruby
my-nextjs-app/
├── app/ # App Router (Next.js 13+)
│ ├── layout.tsx # 根布局组件
│ ├── page.tsx # 首页
│ ├── about/ # 关于页面路由
│ │ └── page.tsx # 关于页面
│ ├── blog/ # 博客路由
│ │ ├── [slug]/ # 动态路由
│ │ │ └── page.tsx # 博客文章页面
│ │ └── page.tsx # 博客列表页面
│ └── api/ # API路由
│ └── hello/
│ └── route.ts # API端点
├── components/ # 共享组件
├── public/ # 静态资源
├── styles/ # 样式文件
├── lib/ # 工具库
├── next.config.js # Next.js配置
├── package.json # 项目依赖
└── tsconfig.json # TypeScript配置
jsx
// app/layout.tsx - 根布局组件
import { Inter } from "next/font/google";
import { Metadata } from "next";
import "./globals.css";
// 使用Google字体
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "我的Next.js应用",
description: "使用Next.js构建的现代React应用",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode,
}) {
return (
<html lang="zh-CN">
<body className={inter.className}>
<header className="bg-blue-600 text-white p-4">
<div className="container mx-auto flex justify-between items-center">
<h1 className="text-2xl font-bold">我的Next.js应用</h1>
<nav>
<ul className="flex space-x-4">
<li>
<a href="/" className="hover:underline">
首页
</a>
</li>
<li>
<a href="/about" className="hover:underline">
关于
</a>
</li>
<li>
<a href="/blog" className="hover:underline">
博客
</a>
</li>
</ul>
</nav>
</div>
</header>
<main className="container mx-auto py-8 px-4">{children}</main>
<footer className="bg-gray-100 p-4 mt-8">
<div className="container mx-auto text-center text-gray-600">
© {new Date().getFullYear()} 我的Next.js应用 | 使用Next.js构建
</div>
</footer>
</body>
</html>
);
}
jsx
// app/page.tsx - 首页(使用服务端组件)
export default function Home() {
return (
<div className="space-y-8">
<section className="text-center py-12">
<h1 className="text-4xl font-bold mb-4">欢迎来到Next.js世界</h1>
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
探索服务端渲染、静态站点生成和React应用开发的全新可能性
</p>
</section>
<section className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="border rounded-lg p-6 shadow-sm">
<h2 className="text-2xl font-bold mb-3">服务端渲染</h2>
<p className="text-gray-600">
每次请求时在服务器渲染页面,适合动态内容
</p>
</div>
<div className="border rounded-lg p-6 shadow-sm">
<h2 className="text-2xl font-bold mb-3">静态站点生成</h2>
<p className="text-gray-600">
在构建时预渲染页面,提供极快的加载速度
</p>
</div>
<div className="border rounded-lg p-6 shadow-sm">
<h2 className="text-2xl font-bold mb-3">增量静态再生</h2>
<p className="text-gray-600">静态生成与按需更新的完美结合</p>
</div>
</section>
<section className="text-center">
<a
href="/blog"
className="inline-block bg-blue-600 text-white px-6 py-3 rounded-md hover:bg-blue-700 transition"
>
浏览博客文章
</a>
</section>
</div>
);
}
3. 不同渲染模式的实现方式
Next.js 13 的 App Router 提供了强大的服务端组件(RSC),但也支持多种渲染模式:
静态站点生成(SSG)
jsx
// app/blog/page.tsx - 静态生成的博客列表页
import Link from "next/link";
import { getBlogPosts } from "@/lib/blog";
// 这个函数在构建时执行,用于静态生成页面
export async function generateStaticParams() {
return [{}]; // 静态生成博客列表页
}
// 默认情况下,App Router中的页面组件是服务端组件
// 当使用generateStaticParams时,它们会在构建时静态生成
export default async function BlogListPage() {
// 在构建时获取博客文章
const posts = await getBlogPosts();
return (
<div>
<h1 className="text-3xl font-bold mb-6">博客文章</h1>
<div className="grid gap-6">
{posts.map((post) => (
<article key={post.slug} className="border rounded-lg p-6 shadow-sm">
<h2 className="text-2xl font-bold mb-2">
<Link
href={`/blog/${post.slug}`}
className="hover:text-blue-600 transition"
>
{post.title}
</Link>
</h2>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
<div className="text-sm text-gray-500">
发布于 {new Date(post.date).toLocaleDateString("zh-CN")}
</div>
</article>
))}
</div>
</div>
);
}
jsx
// app/blog/[slug]/page.tsx - 动态博客文章页面
import { getBlogPosts, getBlogPostBySlug } from "@/lib/blog";
import { notFound } from "next/navigation";
import { Metadata } from "next";
// 为每篇文章生成静态页面
export async function generateStaticParams() {
const posts = await getBlogPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
// 动态生成元数据
export async function generateMetadata({
params,
}: {
params: { slug: string },
}): Promise<Metadata> {
const post = await getBlogPostBySlug(params.slug);
if (!post) {
return {
title: "文章未找到",
};
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.date,
authors: [post.author.name],
},
};
}
export default async function BlogPostPage({
params,
}: {
params: { slug: string },
}) {
const post = await getBlogPostBySlug(params.slug);
if (!post) {
notFound();
}
return (
<article className="max-w-3xl mx-auto">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center space-x-4 mb-8 text-gray-600">
<div className="flex items-center">
<img
src={post.author.avatar}
alt={post.author.name}
className="w-10 h-10 rounded-full mr-3"
/>
<span>{post.author.name}</span>
</div>
<span>•</span>
<time dateTime={post.date}>
{new Date(post.date).toLocaleDateString("zh-CN", {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
</div>
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
</article>
);
}
增量静态再生(ISR)
jsx
// lib/blog.ts - 博客数据获取逻辑
import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
import { cache } from 'react';
export interface BlogPost {
slug: string;
title: string;
date: string;
excerpt: string;
content: string;
author: {
name: string;
avatar: string;
};
}
// 使用React的cache包装函数避免重复获取
export const getBlogPosts = cache(async (): Promise<BlogPost[]> => {
const postsDirectory = path.join(process.cwd(), 'content/blog');
const filenames = await fs.readdir(postsDirectory);
const posts = await Promise.all(
filenames.map(async (filename) => {
const filePath = path.join(postsDirectory, filename);
const fileContent = await fs.readFile(filePath, 'utf8');
const { data, content } = matter(fileContent);
const slug = filename.replace(/\.md$/, '');
// 处理Markdown转HTML
const processedContent = await remark()
.use(html)
.process(content);
const contentHtml = processedContent.toString();
return {
slug,
title: data.title,
date: data.date,
excerpt: data.excerpt || '',
content: contentHtml,
author: data.author || {
name: '博客作者',
avatar: '/images/default-avatar.png',
},
} as BlogPost;
})
);
// 按日期排序
return posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
});
export async function getBlogPostBySlug(slug: string): Promise<BlogPost | null> {
const posts = await getBlogPosts();
return posts.find((post) => post.slug === slug) || null;
}
jsx
// next.config.js - 配置ISR
/** @type {import('next').NextConfig} */
const nextConfig = {
// App Router模式下,可以通过页面导出的revalidate属性配置ISR
// 或者通过fetch的next.revalidate选项
experimental: {
// 启用实验性功能(根据Next.js版本可能不需要)
serverActions: true,
},
};
module.exports = nextConfig;
jsx
// app/products/[id]/page.tsx - 使用ISR的产品页面
import { getProduct, getProductIds } from "@/lib/products";
import { notFound } from "next/navigation";
import Image from "next/image";
import AddToCartButton from "@/components/AddToCartButton";
// 在构建时静态生成一部分产品页面
export async function generateStaticParams() {
// 获取热门产品ID,只预渲染这些页面
const popularProductIds = await getPopularProductIds();
return popularProductIds.map((id) => ({
id: id.toString(),
}));
}
// 设置该页面每60秒可以重新验证(ISR)
export const revalidate = 60;
export default async function ProductPage({
params,
}: {
params: { id: string },
}) {
// 使用fetch API时也可以设置revalidate
const product = await getProduct(params.id);
if (!product) {
notFound();
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<Image
src={product.imageUrl}
alt={product.name}
width={600}
height={600}
className="rounded-lg"
priority
/>
</div>
<div>
<h1 className="text-3xl font-bold mb-4">{product.name}</h1>
<div className="text-2xl font-semibold text-blue-600 mb-4">
¥{product.price.toFixed(2)}
</div>
<div className="mb-6">
<div className="bg-green-50 text-green-700 px-4 py-2 rounded-md inline-block">
有货 - 预计3-5天送达
</div>
</div>
<p className="text-gray-700 mb-6">{product.description}</p>
<AddToCartButton productId={product.id} />
</div>
</div>
);
}
客户端组件与服务端数据交互
jsx
// components/AddToCartButton.tsx - 客户端组件
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
// 服务器操作
import { addToCart } from "@/app/actions";
interface AddToCartButtonProps {
productId: string;
}
export default function AddToCartButton({ productId }: AddToCartButtonProps) {
const [quantity, setQuantity] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const handleAddToCart = async () => {
setIsLoading(true);
try {
// 调用服务器操作
await addToCart(productId, quantity);
// 刷新服务器组件
router.refresh();
// 显示成功消息
showSuccessToast("商品已添加到购物车");
} catch (error) {
console.error("添加到购物车失败", error);
showErrorToast("添加到购物车失败,请重试");
} finally {
setIsLoading(false);
}
};
return (
<div className="space-y-4">
<div className="flex items-center">
<button
onClick={() => setQuantity(Math.max(1, quantity - 1))}
className="px-3 py-1 border rounded-l-md bg-gray-100"
aria-label="减少数量"
>
-
</button>
<span className="px-4 py-1 border-t border-b text-center w-16">
{quantity}
</span>
<button
onClick={() => setQuantity(quantity + 1)}
className="px-3 py-1 border rounded-r-md bg-gray-100"
aria-label="增加数量"
>
+
</button>
</div>
<button
onClick={handleAddToCart}
disabled={isLoading}
className="w-full bg-blue-600 text-white py-3 px-6 rounded-md hover:bg-blue-700 transition disabled:opacity-70"
>
{isLoading ? "添加中..." : "添加到购物车"}
</button>
</div>
);
}
// 显示消息的辅助函数
function showSuccessToast(message: string) {
// 实现toast通知
}
function showErrorToast(message: string) {
// 实现错误toast通知
}
jsx
// app/actions.ts - 服务端操作
"use server";
import { cookies } from "next/headers";
import { revalidatePath } from "next/cache";
// 服务器操作:添加到购物车
export async function addToCart(productId: string, quantity: number) {
// 获取当前购物车
const cookieStore = cookies();
const cartCookie = cookieStore.get("cart");
let cart = cartCookie ? JSON.parse(cartCookie.value) : [];
// 查找商品是否已在购物车
const existingItemIndex = cart.findIndex(
(item: any) => item.productId === productId
);
if (existingItemIndex >= 0) {
// 更新数量
cart[existingItemIndex].quantity += quantity;
} else {
// 添加新商品
cart.push({ productId, quantity });
}
// 保存购物车到cookie
cookieStore.set("cart", JSON.stringify(cart), {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24 * 7, // 一周
path: "/",
});
// 重新验证购物车相关页面
revalidatePath("/cart");
revalidatePath("/products/[id]");
return { success: true };
}
4. API 路由与全栈应用开发
Next.js 不只用于渲染页面,还可以作为 API 服务器:
jsx
// app/api/products/route.ts - API路由
import { NextResponse } from "next/server";
import { getProducts } from "@/lib/products";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const page = parseInt(searchParams.get("page") || "1");
const limit = parseInt(searchParams.get("limit") || "10");
try {
const products = await getProducts({ category, page, limit });
return NextResponse.json({
products: products.items,
pagination: {
total: products.total,
page,
limit,
pages: Math.ceil(products.total / limit),
},
});
} catch (error) {
console.error("获取产品列表失败", error);
return NextResponse.json({ error: "获取产品列表失败" }, { status: 500 });
}
}
jsx
// app/api/products/[id]/route.ts - 动态API路由
import { NextResponse } from "next/server";
import { getProduct } from "@/lib/products";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const product = await getProduct(params.id);
if (!product) {
return NextResponse.json({ error: "产品未找到" }, { status: 404 });
}
return NextResponse.json(product);
} catch (error) {
console.error("获取产品详情失败", error);
return NextResponse.json({ error: "获取产品详情失败" }, { status: 500 });
}
}
5. 数据获取与优化策略
Next.js 提供了强大的数据获取机制:
jsx
// lib/database.ts - 数据库连接
import { Pool } from "pg";
import { cache } from "react";
// 创建数据库连接池
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl:
process.env.NODE_ENV === "production"
? { rejectUnauthorized: false }
: false,
});
// 缓存数据库查询函数
export const queryDB = cache(async (text: string, params?: any[]) => {
const client = await pool.connect();
try {
const result = await client.query(text, params);
return result.rows;
} finally {
client.release();
}
});
jsx
// app/dashboard/page.tsx - 带缓存控制的数据获取
import { Suspense } from "react";
import SalesChart from "@/components/SalesChart";
import ProductsTable from "@/components/ProductsTable";
import { getSalesData, getTopProducts } from "@/lib/analytics";
// 控制服务器端数据获取
export const dynamic = "force-dynamic"; // 或 'auto' | 'force-static'
export const revalidate = 3600; // 1小时重新验证
export default async function DashboardPage() {
// 并行数据获取
const [salesData, topProducts] = await Promise.all([
getSalesData(),
getTopProducts(),
]);
return (
<div className="space-y-8">
<h1 className="text-3xl font-bold">仪表盘</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="col-span-2 bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">销售趋势</h2>
<Suspense fallback={<div>加载销售数据...</div>}>
<SalesChart data={salesData} />
</Suspense>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">概览</h2>
<dl className="space-y-4">
<div>
<dt className="text-gray-600">今日销售额</dt>
<dd className="text-3xl font-bold">
¥{salesData.today.toLocaleString()}
</dd>
</div>
<div>
<dt className="text-gray-600">本月销售额</dt>
<dd className="text-3xl font-bold">
¥{salesData.month.toLocaleString()}
</dd>
</div>
<div>
<dt className="text-gray-600">订单完成率</dt>
<dd className="text-3xl font-bold">
{salesData.completionRate}%
</dd>
</div>
</dl>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">热门产品</h2>
<Suspense fallback={<div>加载产品数据...</div>}>
<ProductsTable products={topProducts} />
</Suspense>
</div>
</div>
);
}
6. 部署与性能优化
jsx
// next.config.js - 生产优化配置
/** @type {import('next').NextConfig} */
const nextConfig = {
// 图像优化
images: {
domains: ["example.com", "cdn.example.com"],
formats: ["image/avif", "image/webp"],
},
// 国际化
i18n: {
locales: ["zh-CN", "en-US"],
defaultLocale: "zh-CN",
},
// 压缩优化
compress: true,
// 响应头
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "Cache-Control",
value:
"public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-XSS-Protection",
value: "1; mode=block",
},
],
},
];
},
// 重定向
async redirects() {
return [
{
source: "/old-blog/:slug",
destination: "/blog/:slug",
permanent: true,
},
];
},
// 输出优化
output: "standalone", // 适合容器化部署
};
module.exports = nextConfig;
yaml
# .github/workflows/deploy.yml - 部署工作流
name: Deploy Next.js
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: "--prod"
下一篇预告:《【React 与微前端】大型应用的模块化与微服务化架构》
在系列的下一篇中,我们将探索如何处理大型企业级 React 应用的架构挑战:
- 微前端架构的优势与实现方案
- 前端微服务设计模式
- 基于 Module Federation 的代码共享与独立部署
- 微前端的性能优化与状态管理
- 团队协作与开发效率的平衡
随着企业应用规模不断扩大,如何保持代码可维护性和团队并行开发效率成为关键问题。下一篇,我们将为你揭开构建高效大型前端应用的秘密!
敬请期待!
关于作者
Hi,我是 hyy,一位热爱技术的全栈开发者:
- 🚀 专注 TypeScript 全栈开发,偏前端技术栈
- 💼 多元工作背景(跨国企业、技术外包、创业公司)
- 📝 掘金活跃技术作者
- 🎵 电子音乐爱好者
- 🎮 游戏玩家
- 💻 技术分享达人
加入我们
欢迎加入前端技术交流圈,与 10000+开发者一起:
- 探讨前端最新技术趋势
- 解决开发难题
- 分享职场经验
- 获取优质学习资源
添加方式:掘金摸鱼沸点 👈 扫码进群