服务端渲染SSR优化
如果应用的首屏渲染时间较长,可以考虑使用服务端渲染(SSR)。
SSR 可以减少客户端渲染的工作量,并显著提升首屏加载速度。Next.js 是一个用于构建 React SSR 应用的优秀框架。
SSR 实现框架与最佳实践
Next.js 全面实现
Next.js 提供了多种渲染模式,包括 SSR、SSG、ISR 等,可以根据不同场景选择最优方案。
渲染模式对比
flowchart LR
A[用户请求] --> B{页面类型}
B -->|SSG| C[预渲染静态页面]
C --> D[CDN 缓存]
D --> E[极速响应]
B -->|SSR| F[服务端实时渲染]
F --> G[动态内容]
G --> H[个性化响应]
B -->|ISR| I[增量静态再生]
I --> J[定时更新]
J --> K[兼备性能和灵活性]
B -->|CSR| L[客户端渲染]
L --> M[交互体验]
style C fill:#e1f5fe
style F fill:#f3e5f5
style I fill:#e8f5e8
style L fill:#fff3e0
页面级别配置
// pages/products/[id].js - SSR 配置
export async function getServerSideProps(context) {
const { params, req, res, query } = context;
const productId = params.id;
// 设置缓存头
res.setHeader(
'Cache-Control',
'public, s-maxage=60, stale-while-revalidate=300'
);
try {
// 并行获取数据
const [product, reviews, recommendations] = await Promise.all([
fetchProduct(productId),
fetchProductReviews(productId),
fetchRecommendations(productId)
]);
return {
props: {
product,
reviews,
recommendations,
timestamp: Date.now(), // 用于调试
},
};
} catch (error) {
console.error('SSR Error:', error);
// 错误处理
if (error.status === 404) {
return { notFound: true };
}
// 回退到客户端渲染
return {
props: {
error: true,
fallbackData: await getCachedProductData(productId),
},
};
}
}
// pages/blog/[slug].js - SSG 配置
export async function getStaticProps({ params }) {
const post = await fetchBlogPost(params.slug);
if (!post) {
return { notFound: true };
}
return {
props: {
post,
},
revalidate: 3600, // ISR: 每小时更新一次
};
}
export async function getStaticPaths() {
// 只预渲染热门文章
const popularPosts = await fetchPopularBlogPosts();
const paths = popularPosts.map((post) => ({
params: { slug: post.slug },
}));
return {
paths,
fallback: 'blocking', // 其他页面首次访问时 SSR
};
}
中间件配置
// middleware.js - 全局中间件
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
export async function middleware(request) {
const token = await getToken({ req: request });
const { pathname } = request.nextUrl;
// A/B 测试配置
if (pathname.startsWith('/experiment')) {
const variant = request.cookies.get('ab-variant')?.value ||
(Math.random() > 0.5 ? 'A' : 'B');
const response = NextResponse.next();
response.cookies.set('ab-variant', variant, {
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 天
});
// 重写 URL 根据变体
if (variant === 'B') {
return NextResponse.rewrite(
new URL(pathname.replace('/experiment', '/experiment-b'), request.url)
);
}
return response;
}
// 地理位置重定向
const country = request.geo?.country || 'US';
if (pathname === '/' && country === 'CN') {
return NextResponse.redirect(new URL('/cn', request.url));
}
// 身份验证
if (pathname.startsWith('/admin') && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
自定义 SSR 解决方案
Express + React SSR
// server/app.js
import express from 'express';
import React from 'react';
import { renderToString, renderToPipeableStream } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom/server';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import App from '../src/App';
import { createServerRenderer } from './renderer';
const app = express();
const PORT = process.env.PORT || 3000;
// 静态资源服务
app.use('/static', express.static('dist/static'));
// 服务端渲染中间件
app.get('*', async (req, res) => {
try {
const context = {};
const store = createStore(rootReducer, initialState);
// 数据预加载
await preloadData(req.path, store.dispatch);
const renderer = createServerRenderer({
url: req.url,
context,
store,
});
// 流式渲染 (React 18+)
const { pipe, abort } = renderToPipeableStream(
renderer,
{
bootstrapScripts: ['/static/js/client.js'],
onShellReady() {
res.statusCode = context.statusCode || 200;
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
onError(error) {
console.error('SSR Error:', error);
res.statusCode = 500;
res.end('Server Error');
},
}
);
// 超时处理
setTimeout(abort, 10000);
} catch (error) {
console.error('SSR Error:', error);
res.status(500).send('Server Error');
}
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
服务端渲染器
// server/renderer.js
import React from 'react';
import { StaticRouter } from 'react-router-dom/server';
import { Provider } from 'react-redux';
import { HelmetProvider } from 'react-helmet-async';
import App from '../src/App';
export function createServerRenderer({ url, context, store }) {
const helmetContext = {};
return (
<HelmetProvider context={helmetContext}>
<Provider store={store}>
<StaticRouter location={url} context={context}>
<App />
</StaticRouter>
</Provider>
</HelmetProvider>
);
}
// 数据预加载
export async function preloadData(path, dispatch) {
const routes = [
{
path: '/',
component: 'Home',
loadData: () => dispatch(fetchHomeData()),
},
{
path: '/products/:id',
component: 'Product',
loadData: (params) => Promise.all([
dispatch(fetchProduct(params.id)),
dispatch(fetchProductReviews(params.id)),
]),
},
];
const matchedRoute = routes.find(route =>
matchPath(path, { path: route.path, exact: true })
);
if (matchedRoute && matchedRoute.loadData) {
await matchedRoute.loadData();
}
}
HTML 模板生成
// server/template.js
export function createHTMLTemplate({
content,
state,
helmet,
stylesheets = [],
scripts = [],
preloadLinks = [],
}) {
return `
<!DOCTYPE html>
<html ${helmet.htmlAttributes.toString()}>
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
<!-- 预加载资源 -->
${preloadLinks.join('\n ')}
<!-- 关键 CSS -->
${stylesheets.map(href =>
`<link rel="stylesheet" href="${href}" />`
).join('\n ')}
<!-- 内联关键 CSS -->
<style id="critical-css">
/* 关键样式将在构建时提取 */
</style>
<!-- 结构化数据 -->
${helmet.script.toString()}
</head>
<body ${helmet.bodyAttributes.toString()}>
<!-- 页面内容 -->
<div id="root">${content}</div>
<!-- 状态数据 -->
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(state).replace(
/</g,
'\\u003c'
)};
</script>
<!-- 加载客户端 JavaScript -->
${scripts.map(src =>
`<script src="${src}" defer></script>`
).join('\n ')}
<!-- 非关键 CSS 延迟加载 -->
<script>
// 加载非关键 CSS
const loadCSS = function(href) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
document.head.appendChild(link);
};
// 页面加载完成后加载
window.addEventListener('load', function() {
loadCSS('/static/css/non-critical.css');
});
</script>
</body>
</html>
`;
}
SSR 带来的主要收益有:
React SSR (Server-Side Rendering)即服务端渲染,在 React 应用中,SSR 可以带来多个方面的收益:
收益
提升首屏渲染速度
SSR 的核心优势是将原本在客户端执行的渲染工作转移到服务端,使用户可以更快地看到页面内容。
传统 CSR vs SSR 渲染流程对比
flowchart TD
A[用户访问页面] --> B{渲染方式}
B -->|CSR| C[下载空白HTML]
C --> D[下载JS Bundle]
D --> E[执行JS代码]
E --> F[渲染组件]
F --> G[显示完整页面]
B -->|SSR| H[服务端渲染HTML]
H --> I[返回完整HTML]
I --> J[用户看到内容]
J --> K[下载JS Bundle]
K --> L[Hydration]
L --> M[交互功能就绪]
style C fill:#ffcccc
style D fill:#ffcccc
style E fill:#ffcccc
style F fill:#ffcccc
style H fill:#ccffcc
style I fill:#ccffcc
style J fill:#ccffcc
性能指标对比
| 指标 | CSR | SSR | 提升幅度 |
|---|---|---|---|
| FCP (First Contentful Paint) | 2.5s | 0.8s | 68% |
| LCP (Largest Contentful Paint) | 3.2s | 1.2s | 62.5% |
| TTI (Time to Interactive) | 4.1s | 2.8s | 31.7% |
| TTFB (Time to First Byte) | 200ms | 800ms | -300% |
Next.js SSR 实现示例
// pages/products/[id].js
import { GetServerSideProps } from 'next';
interface Product {
id: string;
name: string;
description: string;
price: number;
images: string[];
}
interface ProductPageProps {
product: Product;
recommendations: Product[];
}
// 服务端渲染组件
export default function ProductPage({ product, recommendations }: ProductPageProps) {
return (
<div className="product-page">
<div className="product-header">
<h1>{product.name}</h1>
<p className="price">${product.price}</p>
</div>
<div className="product-images">
{product.images.map((image, index) => (
<img
key={index}
src={image}
alt={`${product.name} - ${index + 1}`}
loading={index === 0 ? 'eager' : 'lazy'}
/>
))}
</div>
<div className="product-description">
<p>{product.description}</p>
</div>
<div className="recommendations">
<h3>推荐商品</h3>
<div className="recommendation-grid">
{recommendations.map(item => (
<ProductCard key={item.id} product={item} />
))}
</div>
</div>
</div>
);
}
// 服务端数据获取
export const getServerSideProps: GetServerSideProps<ProductPageProps> = async ({ params, req }) => {
const productId = params?.id as string;
// 并行获取数据以优化性能
const [productResponse, recommendationsResponse] = await Promise.all([
fetch(`${process.env.API_BASE_URL}/api/products/${productId}`, {
headers: {
'User-Agent': req.headers['user-agent'] || '',
'Accept': 'application/json',
},
}),
fetch(`${process.env.API_BASE_URL}/api/products/${productId}/recommendations`, {
headers: {
'User-Agent': req.headers['user-agent'] || '',
},
})
]);
if (!productResponse.ok) {
return {
notFound: true,
};
}
const product = await productResponse.json();
const recommendations = await recommendationsResponse.json();
return {
props: {
product,
recommendations: recommendations || [],
},
};
};
React 18 并发特性优化 SSR
// server/render.js
import { renderToPipeableStream } from 'react-dom/server';
import { Suspense } from 'react';
// 流式渲染实现
export function renderApp(url, res) {
const { pipe, abort } = renderToPipeableStream(
<Suspense fallback={<PageSkeleton />}>
<App url={url} />
</Suspense>,
{
bootstrapScripts: ['/client.js'],
onShellReady() {
// 立即发送 HTML shell
res.statusCode = 200;
res.setHeader('Content-type', 'text/html');
pipe(res);
},
onError(err) {
console.error('SSR Error:', err);
res.statusCode = 500;
res.end('Server Error');
},
}
);
// 5秒超时
setTimeout(abort, 5000);
}
// components/PageSkeleton.js
function PageSkeleton() {
return (
<div className="page-skeleton">
<div className="skeleton-header" />
<div className="skeleton-content">
<div className="skeleton-line" />
<div className="skeleton-line" />
<div className="skeleton-line short" />
</div>
</div>
);
}
有利于 SEO(搜索引擎优化)
搜索引擎爬虫可以直接读取 SSR 生成的 HTML 内容,而不需要执行 JavaScript。这显著提升了网站的搜索排名和收录效果。
SEO 关键要素实现
// pages/_document.js - Next.js 文档组件
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="zh-CN">
<Head>
{/* 基础 SEO 配置 */}
<meta name="robots" content="index,follow" />
<meta name="googlebot" content="index,follow" />
<link rel="canonical" href={process.env.NEXT_PUBLIC_SITE_URL} />
{/* 结构化数据 */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
"name": "电商平台",
"url": process.env.NEXT_PUBLIC_SITE_URL,
"potentialAction": {
"@type": "SearchAction",
"target": `${process.env.NEXT_PUBLIC_SITE_URL}/search?q={search_term_string}`,
"query-input": "required name=search_term_string"
}
})
}}
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
// pages/products/[slug].js - 产品页面 SEO 优化
import Head from 'next/head';
import { GetServerSideProps } from 'next';
interface ProductSEOProps {
product: {
id: string;
name: string;
description: string;
price: number;
images: string[];
category: string;
brand: string;
sku: string;
availability: 'InStock' | 'OutOfStock';
reviews: {
averageRating: number;
reviewCount: number;
};
};
}
export default function ProductPage({ product }: ProductSEOProps) {
// 生成结构化数据
const productStructuredData = {
"@context": "https://schema.org",
"@type": "Product",
"name": product.name,
"description": product.description,
"sku": product.sku,
"brand": {
"@type": "Brand",
"name": product.brand
},
"category": product.category,
"image": product.images,
"offers": {
"@type": "Offer",
"price": product.price,
"priceCurrency": "CNY",
"availability": `https://schema.org/${product.availability}`,
"seller": {
"@type": "Organization",
"name": "电商平台"
}
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": product.reviews.averageRating,
"reviewCount": product.reviews.reviewCount
}
};
return (
<>
<Head>
{/* 基础 Meta 标签 */}
<title>{`${product.name} - ${product.brand} | 电商平台`}</title>
<meta
name="description"
content={`${product.description.substring(0, 160)}...`}
/>
<meta name="keywords" content={`${product.name}, ${product.brand}, ${product.category}`} />
{/* Open Graph 标签 */}
<meta property="og:type" content="product" />
<meta property="og:title" content={`${product.name} - ${product.brand}`} />
<meta property="og:description" content={product.description} />
<meta property="og:image" content={product.images[0]} />
<meta property="og:url" content={`${process.env.NEXT_PUBLIC_SITE_URL}/products/${product.id}`} />
<meta property="product:price:amount" content={product.price.toString()} />
<meta property="product:price:currency" content="CNY" />
{/* Twitter Card */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={`${product.name} - ${product.brand}`} />
<meta name="twitter:description" content={product.description} />
<meta name="twitter:image" content={product.images[0]} />
{/* 结构化数据 */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(productStructuredData)
}}
/>
{/* Canonical URL */}
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/products/${product.id}`} />
</Head>
<div className="product-page">
{/* 产品内容 */}
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* 其他产品信息 */}
</div>
</>
);
}
export const getServerSideProps: GetServerSideProps = async ({ params, res }) => {
const productId = params?.slug as string;
try {
const response = await fetch(`${process.env.API_BASE_URL}/api/products/${productId}`);
if (!response.ok) {
return { notFound: true };
}
const product = await response.json();
// 设置缓存头以优化 SEO 和性能
res.setHeader(
'Cache-Control',
'public, s-maxage=86400, stale-while-revalidate=604800'
);
return {
props: {
product,
},
};
} catch (error) {
console.error('Failed to fetch product:', error);
return { notFound: true };
}
};
动态 Sitemap 生成
// pages/sitemap.xml.js
import { GetServerSideProps } from 'next';
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL;
function generateSiteMap(products) {
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<!-- 静态页面 -->
<url>
<loc>${SITE_URL}</loc>
<lastmod>${new Date().toISOString()}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>${SITE_URL}/about</loc>
<lastmod>${new Date().toISOString()}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<!-- 产品页面 -->
${products
.map(({ id, updatedAt }) => {
return `
<url>
<loc>${SITE_URL}/products/${id}</loc>
<lastmod>${updatedAt}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
`;
})
.join('')}
</urlset>
`;
}
export const getServerSideProps: GetServerSideProps = async ({ res }) => {
// 获取所有产品数据
const productsResponse = await fetch(`${process.env.API_BASE_URL}/api/products/sitemap`);
const products = await productsResponse.json();
const sitemap = generateSiteMap(products);
res.setHeader('Content-Type', 'text/xml');
res.setHeader('Cache-Control', 'public, s-maxage=86400, stale-while-revalidate=604800');
res.write(sitemap);
res.end();
return {
props: {},
};
};
export default function SiteMap() {
// getServerSideProps 会在此之前返回响应
}
更好的社交媒体分享
SSR 允许在服务端动态生成 Open Graph 和 Twitter Card 元数据,使得在社交媒体平台分享链接时能够显示丰富的卡片信息。
Open Graph 等社交媒体元标签
// components/SocialMetaTags.js
import Head from 'next/head';
interface SocialMetaTagsProps {
title: string;
description: string;
image: string;
url: string;
type?: 'website' | 'article' | 'product';
author?: string;
publishedTime?: string;
modifiedTime?: string;
}
export function SocialMetaTags({
title,
description,
image,
url,
type = 'website',
author,
publishedTime,
modifiedTime
}: SocialMetaTagsProps) {
const siteName = '电商平台';
const twitterHandle = '@myecommerce';
return (
<Head>
{/* Open Graph / Facebook */}
<meta property="og:type" content={type} />
<meta property="og:site_name" content={siteName} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content={title} />
<meta property="og:url" content={url} />
<meta property="og:locale" content="zh_CN" />
{/* 文章类型的额外元数据 */}
{type === 'article' && (
<>
{author && <meta property="article:author" content={author} />}
{publishedTime && <meta property="article:published_time" content={publishedTime} />}
{modifiedTime && <meta property="article:modified_time" content={modifiedTime} />}
</>
)}
{/* Twitter Card */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={twitterHandle} />
<meta name="twitter:creator" content={twitterHandle} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
<meta name="twitter:image:alt" content={title} />
{/* LinkedIn */}
<meta property="linkedin:owner" content="company-id" />
{/* 微信分享 */}
<meta name="wechat:title" content={title} />
<meta name="wechat:description" content={description} />
<meta name="wechat:image" content={image} />
</Head>
);
}
动态社交分享图片生成
// pages/api/og-image.js - 动态生成分享图片
import { ImageResponse } from '@vercel/og';
export const config = {
runtime: 'edge',
};
export default async function handler(req) {
const { searchParams } = new URL(req.url);
const title = searchParams.get('title') || '默认标题';
const description = searchParams.get('description') || '默认描述';
const productImage = searchParams.get('image');
try {
return new ImageResponse(
(
<div
style={{
height: '630px',
width: '1200px',
display: 'flex',
flexDirection: 'column',
backgroundColor: '#1f2937',
color: 'white',
fontFamily: 'Inter, sans-serif',
}}
>
{/* 背景渐变 */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
opacity: 0.8,
}}
/>
{/* 内容区域 */}
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: '80px',
height: '100%',
position: 'relative',
}}
>
{/* 文字区域 */}
<div
style={{
display: 'flex',
flexDirection: 'column',
maxWidth: '60%',
}}
>
<h1
style={{
fontSize: '64px',
fontWeight: 'bold',
lineHeight: 1.2,
marginBottom: '24px',
}}
>
{title}
</h1>
<p
style={{
fontSize: '24px',
opacity: 0.9,
lineHeight: 1.4,
}}
>
{description}
</p>
</div>
{/* 产品图片 */}
{productImage && (
<div
style={{
width: '300px',
height: '300px',
borderRadius: '16px',
overflow: 'hidden',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
}}
>
<img
src={productImage}
alt={title}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
</div>
)}
</div>
{/* Logo 和站点名 */}
<div
style={{
position: 'absolute',
bottom: '40px',
left: '80px',
display: 'flex',
alignItems: 'center',
fontSize: '20px',
opacity: 0.8,
}}
>
🛒 电商平台
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: await fetch(
new URL('../../assets/Inter-Bold.woff', import.meta.url)
).then((res) => res.arrayBuffer()),
style: 'normal',
weight: 700,
},
],
}
);
} catch (error) {
console.error('OG Image generation failed:', error);
return new Response('Failed to generate image', { status: 500 });
}
}
社交分享预览组件
// components/SocialSharePreview.js
import { useState, useEffect } from 'react';
interface SocialSharePreviewProps {
url: string;
title: string;
description: string;
image: string;
}
export function SocialSharePreview({ url, title, description, image }: SocialSharePreviewProps) {
const [previewData, setPreviewData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
// 预览社交分享效果
const generatePreview = async () => {
setIsLoading(true);
try {
// 可以调用第三方 API 来验证 OG 数据
const response = await fetch(`/api/social-preview?url=${encodeURIComponent(url)}`);
const data = await response.json();
setPreviewData(data);
} catch (error) {
console.error('Failed to generate social preview:', error);
} finally {
setIsLoading(false);
}
};
return (
<div className="social-share-preview">
<h3>社交分享预览</h3>
{/* Facebook 预览 */}
<div className="platform-preview facebook-preview">
<h4>Facebook</h4>
<div className="preview-card">
<img src={image} alt={title} className="preview-image" />
<div className="preview-content">
<h5>{title}</h5>
<p>{description}</p>
<span className="preview-url">{new URL(url).hostname}</span>
</div>
</div>
</div>
{/* Twitter 预览 */}
<div className="platform-preview twitter-preview">
<h4>Twitter</h4>
<div className="preview-card">
<div className="preview-content">
<h5>{title}</h5>
<p>{description}</p>
<span className="preview-url">{url}</span>
</div>
<img src={image} alt={title} className="preview-image" />
</div>
</div>
{/* LinkedIn 预览 */}
<div className="platform-preview linkedin-preview">
<h4>LinkedIn</h4>
<div className="preview-card">
<img src={image} alt={title} className="preview-image" />
<div className="preview-content">
<h5>{title}</h5>
<p>{description}</p>
<span className="preview-url">{new URL(url).hostname}</span>
</div>
</div>
</div>
<button
onClick={generatePreview}
disabled={isLoading}
className="generate-preview-btn"
>
{isLoading ? '生成中...' : '生成预览'}
</button>
</div>
);
}
社交分享辅助函数
// utils/socialSharing.js
export const socialPlatforms = {
facebook: (url, title) =>
`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
twitter: (url, title, description) =>
`https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}&hashtags=ecommerce`,
linkedin: (url, title, description) =>
`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`,
wechat: (url) => {
// 微信分享需要生成二维码
return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(url)}`;
},
weibo: (url, title) =>
`https://service.weibo.com/share/share.php?url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}`,
};
export function generateSocialShareUrls(url, title, description) {
return Object.keys(socialPlatforms).reduce((acc, platform) => {
acc[platform] = socialPlatforms[platform](url, title, description);
return acc;
}, {});
}
// 验证 OG 数据是否正确
export async function validateOGData(url) {
try {
const response = await fetch(url);
const html = await response.text();
const ogTitle = html.match(/<meta property="og:title" content="([^"]*)"/)?.[1];
const ogDescription = html.match(/<meta property="og:description" content="([^"]*)"/)?.[1];
const ogImage = html.match(/<meta property="og:image" content="([^"]*)"/)?.[1];
return {
title: ogTitle,
description: ogDescription,
image: ogImage,
isValid: !!(ogTitle && ogDescription && ogImage)
};
} catch (error) {
console.error('OG validation failed:', error);
return { isValid: false };
}
}
支持旧版浏览器
SSR 生成的静态 HTML 可以在不支持现代 JavaScript 特性的老旧浏览器中正常显示,提供更广泛的兼容性。
渐进式增强策略
// utils/browserSupport.js
export function getBrowserSupport() {
// 检测浏览器支持情况
const support = {
es6: typeof Symbol !== 'undefined',
es2017: typeof Promise !== 'undefined',
modules: 'noModule' in document.createElement('script'),
intersectionObserver: 'IntersectionObserver' in window,
webp: false, // 需要异步检测
};
// WebP 支持检测
const webpTest = new Image();
webpTest.onload = webpTest.onerror = function() {
support.webp = webpTest.height === 2;
};
webpTest.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
return support;
}
export function loadPolyfills() {
const polyfills = [];
// Promise polyfill
if (!window.Promise) {
polyfills.push('https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.min.js');
}
// IntersectionObserver polyfill
if (!('IntersectionObserver' in window)) {
polyfills.push('https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver');
}
// 动态加载 polyfills
return Promise.all(
polyfills.map(src => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
})
);
}
分层加载策略
// pages/_app.js - Next.js App 组件
import { useEffect, useState } from 'react';
import { getBrowserSupport, loadPolyfills } from '../utils/browserSupport';
function MyApp({ Component, pageProps }) {
const [isEnhanced, setIsEnhanced] = useState(false);
const [browserSupport, setBrowserSupport] = useState(null);
useEffect(() => {
// 检测浏览器能力
const support = getBrowserSupport();
setBrowserSupport(support);
// 加载必要的 polyfills
loadPolyfills().then(() => {
setIsEnhanced(true);
}).catch(error => {
console.warn('Failed to load polyfills:', error);
// 即使 polyfills 加载失败,也要尝试启用增强功能
setIsEnhanced(true);
});
}, []);
// 为老旧浏览器提供基础版本
if (!isEnhanced && typeof window !== 'undefined') {
return (
<div className="basic-layout">
<noscript>
<style dangerouslySetInnerHTML={{
__html: `
.enhanced-only { display: none !important; }
.basic-fallback { display: block !important; }
`
}} />
</noscript>
<Component {...pageProps} />
</div>
);
}
return (
<div className={`app-container ${browserSupport?.es6 ? 'modern' : 'legacy'}`}>
<Component {...pageProps} browserSupport={browserSupport} />
</div>
);
}
export default MyApp;
兼容性组件封装
// components/CompatibleImage.js
import { useState, useEffect } from 'react';
interface CompatibleImageProps {
src: string;
webpSrc?: string;
alt: string;
width?: number;
height?: number;
loading?: 'lazy' | 'eager';
fallback?: string;
}
export function CompatibleImage({
src,
webpSrc,
alt,
width,
height,
loading = 'lazy',
fallback
}: CompatibleImageProps) {
const [imageSrc, setImageSrc] = useState(src);
const [hasError, setHasError] = useState(false);
const [supportsWebP, setSupportsWebP] = useState(false);
useEffect(() => {
// WebP 支持检测
if (webpSrc) {
const webpTest = new Image();
webpTest.onload = () => {
if (webpTest.height === 2) {
setSupportsWebP(true);
setImageSrc(webpSrc);
}
};
webpTest.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
}
}, [webpSrc]);
const handleError = () => {
setHasError(true);
if (fallback) {
setImageSrc(fallback);
}
};
// 老旧浏览器不支持 loading 属性
const imgProps = {
src: imageSrc,
alt,
width,
height,
onError: handleError,
};
// 现代浏览器支持 lazy loading
if ('loading' in HTMLImageElement.prototype) {
imgProps.loading = loading;
}
return (
<picture>
{/* 现代浏览器使用 WebP */}
{webpSrc && supportsWebP && (
<source srcSet={webpSrc} type="image/webp" />
)}
{/* 回退到传统格式 */}
<img
{...imgProps}
style={{
maxWidth: '100%',
height: 'auto',
...(hasError && { backgroundColor: '#f0f0f0' })
}}
/>
</picture>
);
}
CSS 兼容性策略
/* styles/compatibility.css */
/* 使用 @supports 规则提供回退方案 */
/* Flexbox 回退 */
.container {
/* 老旧浏览器回退 */
display: block;
width: 100%;
}
@supports (display: flex) {
.container {
display: flex;
align-items: center;
justify-content: space-between;
}
}
/* Grid 回退 */
.grid-container {
/* 传统浏层方案 */
overflow: hidden;
}
.grid-container .grid-item {
float: left;
width: 33.333%;
padding: 10px;
box-sizing: border-box;
}
@supports (display: grid) {
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
overflow: visible;
}
.grid-container .grid-item {
float: none;
width: auto;
padding: 0;
}
}
/* CSS 变量回退 */
.theme-colors {
background-color: #ffffff;
color: #333333;
}
@supports (color: var(--primary)) {
.theme-colors {
background-color: var(--bg-primary, #ffffff);
color: var(--text-primary, #333333);
}
}
/* 现代字体回退 */
.modern-font {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
@supports (font-variation-settings: normal) {
.modern-font {
font-family: 'Inter var', -apple-system, BlinkMacSystemFont, sans-serif;
font-variation-settings: 'wght' 400;
}
}
减少客户端负担
SSR 将复杂的渲染计算转移到服务端,减少了客户端的 CPU 和内存消耗,特别适合低端设备和移动端。
资源优化策略
// server/resourceOptimization.js
import { gzipSync, brotliCompressSync } from 'zlib';
import { minify } from 'html-minifier';
// HTML 压缩优化
export function optimizeHTML(html) {
const minified = minify(html, {
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
sortClassName: true,
useShortDoctype: true,
collapseWhitespace: true,
conservativeCollapse: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
});
return minified;
}
// 动态压缩选择
export function compressResponse(content, acceptEncoding) {
// 优先使用 Brotli 压缩
if (acceptEncoding.includes('br')) {
return {
content: brotliCompressSync(content),
encoding: 'br'
};
}
// 回退到 Gzip
if (acceptEncoding.includes('gzip')) {
return {
content: gzipSync(content),
encoding: 'gzip'
};
}
return {
content,
encoding: 'identity'
};
}
// 关键资源预加载
export function generatePreloadLinks(criticalResources) {
return criticalResources.map(resource => {
const { href, as, type, crossorigin } = resource;
let link = `<link rel="preload" href="${href}" as="${as}"`;
if (type) link += ` type="${type}"`;
if (crossorigin) link += ` crossorigin="${crossorigin}"`;
return link + '>';
}).join('\n');
}
渐进式 Web 应用 (PWA) 策略
// next.config.js - PWA 配置
const withPWA = require('next-pwa');
module.exports = withPWA({
pwa: {
dest: 'public',
disable: process.env.NODE_ENV === 'development',
register: true,
skipWaiting: true,
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 300, // 5 分钟
},
},
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/i,
handler: 'CacheFirst',
options: {
cacheName: 'images-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 86400, // 24 小时
},
},
},
],
},
// 其他 Next.js 配置
experimental: {
modern: true,
},
// 打包优化
webpack: (config, { isServer }) => {
if (!isServer) {
// 客户端 bundle 优化
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
};
}
return config;
},
});
智能资源加载
// components/SmartResourceLoader.js
import { useState, useEffect, useCallback } from 'react';
import { useIntersectionObserver } from '../hooks/useIntersectionObserver';
interface SmartResourceLoaderProps {
children: React.ReactNode;
threshold?: number;
rootMargin?: string;
fallback?: React.ReactNode;
}
export function SmartResourceLoader({
children,
threshold = 0.1,
rootMargin = '50px',
fallback
}: SmartResourceLoaderProps) {
const [shouldLoad, setShouldLoad] = useState(false);
const [isLoading, setIsLoading] = useState(false);
// 使用 Intersection Observer 检测可见性
const [ref, isIntersecting] = useIntersectionObserver({
threshold,
rootMargin,
});
// 检测网络状态
const [networkInfo, setNetworkInfo] = useState({
effectiveType: '4g',
saveData: false,
});
useEffect(() => {
if ('connection' in navigator) {
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
setNetworkInfo({
effectiveType: connection.effectiveType || '4g',
saveData: connection.saveData || false,
});
const handleConnectionChange = () => {
setNetworkInfo({
effectiveType: connection.effectiveType || '4g',
saveData: connection.saveData || false,
});
};
connection.addEventListener('change', handleConnectionChange);
return () => connection.removeEventListener('change', handleConnectionChange);
}
}, []);
// 智能加载策略
useEffect(() => {
if (isIntersecting && !shouldLoad && !isLoading) {
// 根据网络条件决定是否加载
const shouldLoadBasedOnNetwork = () => {
if (networkInfo.saveData) return false; // 数据节省模式
if (networkInfo.effectiveType === 'slow-2g') return false;
if (networkInfo.effectiveType === '2g') return false;
return true;
};
if (shouldLoadBasedOnNetwork()) {
setIsLoading(true);
// 模拟异步加载
setTimeout(() => {
setShouldLoad(true);
setIsLoading(false);
}, 100);
}
}
}, [isIntersecting, shouldLoad, isLoading, networkInfo]);
return (
<div ref={ref}>
{shouldLoad ? children : (fallback || <div className="loading-placeholder" />)}
{isLoading && (
<div className="loading-indicator">
加载中...
</div>
)}
</div>
);
}
// hooks/useIntersectionObserver.js
import { useEffect, useRef, useState } from 'react';
export function useIntersectionObserver({
threshold = 0,
root = null,
rootMargin = '0%',
}) {
const [entry, setEntry] = useState(null);
const [isIntersecting, setIsIntersecting] = useState(false);
const elementRef = useRef(null);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
setEntry(entry);
setIsIntersecting(entry.isIntersecting);
},
{ threshold, root, rootMargin }
);
observer.observe(element);
return () => observer.unobserve(element);
}, [threshold, root, rootMargin]);
return [elementRef, isIntersecting, entry];
}
服务端渲染缓存策略
// server/caching.js
import Redis from 'ioredis';
import { LRUCache } from 'lru-cache';
const redis = new Redis(process.env.REDIS_URL);
// 多层缓存策略
class MultiLayerCache {
constructor() {
// 内存缓存 (L1)
this.memoryCache = new LRUCache({
max: 500,
ttl: 1000 * 60 * 5, // 5 分钟
});
// Redis 缓存 (L2)
this.redisCache = redis;
}
async get(key) {
// 先检查内存缓存
const memoryValue = this.memoryCache.get(key);
if (memoryValue) {
return memoryValue;
}
// 再检查 Redis 缓存
const redisValue = await this.redisCache.get(key);
if (redisValue) {
const parsed = JSON.parse(redisValue);
// 回填到内存缓存
this.memoryCache.set(key, parsed);
return parsed;
}
return null;
}
async set(key, value, ttl = 300) {
// 同时设置两层缓存
this.memoryCache.set(key, value);
await this.redisCache.setex(key, ttl, JSON.stringify(value));
}
async invalidate(pattern) {
// 清理匹配的缓存
this.memoryCache.clear();
const keys = await this.redisCache.keys(pattern);
if (keys.length > 0) {
await this.redisCache.del(...keys);
}
}
}
const cache = new MultiLayerCache();
// SSR 页面缓存中间件
export function ssrCacheMiddleware(options = {}) {
const {
ttl = 300,
keyGenerator = (req) => `ssr:${req.url}`,
shouldCache = () => true,
} = options;
return async (req, res, next) => {
if (req.method !== 'GET' || !shouldCache(req)) {
return next();
}
const cacheKey = keyGenerator(req);
// 尝试从缓存获取
const cached = await cache.get(cacheKey);
if (cached) {
res.setHeader('X-Cache', 'HIT');
res.setHeader('Content-Type', 'text/html');
return res.send(cached);
}
// 拦截响应
const originalSend = res.send;
res.send = function(html) {
// 缓存响应
cache.set(cacheKey, html, ttl);
res.setHeader('X-Cache', 'MISS');
return originalSend.call(this, html);
};
next();
};
}
提升应用的可靠性
SSR 提供了更强的容错能力和稳定性,即使在 JavaScript 执行失败或网络状态不佳的情况下,用户仍能看到基本内容。
容错设计策略
// components/ErrorBoundary.js
import React from 'react';
import * as Sentry from '@sentry/nextjs';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.setState({
error,
errorInfo
});
// 上报错误到 Sentry
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack,
},
},
});
// 本地错误日志
console.error('React Error Boundary caught an error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
const { fallback: Fallback, level = 'page' } = this.props;
// 自定义回退组件
if (Fallback) {
return <Fallback error={this.state.error} />;
}
// 根据错误级别返回不同的回退界面
if (level === 'component') {
return (
<div className="error-fallback-component">
<p>组件加载失败,请刷新页面重试。</p>
<button onClick={() => window.location.reload()}>
刷新页面
</button>
</div>
);
}
return (
<div className="error-fallback-page">
<h2>页面加载遇到问题</h2>
<p>抱歉,页面出现了一些问题。请尝试刷新页面或返回首页。</p>
<div className="error-actions">
<button onClick={() => window.location.reload()}>
刷新页面
</button>
<button onClick={() => window.location.href = '/'}>
返回首页
</button>
</div>
{process.env.NODE_ENV === 'development' && (
<details className="error-details">
<summary>错误详情 (仅开发环境)</summary>
<pre>{this.state.error && this.state.error.toString()}</pre>
<pre>{this.state.errorInfo.componentStack}</pre>
</details>
)}
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
网络失败处理
// hooks/useNetworkRecovery.js
import { useState, useEffect, useCallback } from 'react';
export function useNetworkRecovery() {
const [isOnline, setIsOnline] = useState(true);
const [retryCount, setRetryCount] = useState(0);
const [isRetrying, setIsRetrying] = useState(false);
useEffect(() => {
const handleOnline = () => {
setIsOnline(true);
setRetryCount(0);
};
const handleOffline = () => {
setIsOnline(false);
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
// 初始化检查
setIsOnline(navigator.onLine);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
const retryWithBackoff = useCallback(async (fn, maxRetries = 3) => {
if (isRetrying) return;
setIsRetrying(true);
for (let i = 0; i < maxRetries; i++) {
try {
const result = await fn();
setRetryCount(0);
setIsRetrying(false);
return result;
} catch (error) {
const delay = Math.min(1000 * Math.pow(2, i), 10000); // 指数退避,最大 10 秒
setRetryCount(i + 1);
if (i === maxRetries - 1) {
setIsRetrying(false);
throw error;
}
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}, [isRetrying]);
return {
isOnline,
retryCount,
isRetrying,
retryWithBackoff,
};
}
稳健的 API 请求
// utils/resilientFetch.js
import { useNetworkRecovery } from '../hooks/useNetworkRecovery';
// 带有重试和缓存的 fetch 封装
export class ResilientFetch {
constructor(options = {}) {
this.baseURL = options.baseURL || '';
this.timeout = options.timeout || 10000;
this.retries = options.retries || 3;
this.cache = new Map();
this.retryDelay = options.retryDelay || 1000;
}
async fetch(url, options = {}) {
const fullUrl = `${this.baseURL}${url}`;
const cacheKey = `${fullUrl}_${JSON.stringify(options)}`;
// 检查缓存
if (options.useCache && this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey);
if (Date.now() - cached.timestamp < (options.cacheTime || 300000)) {
return cached.data;
}
}
let lastError;
for (let i = 0; i < this.retries; i++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(fullUrl, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const data = await response.json();
// 缓存成功的响应
if (options.useCache) {
this.cache.set(cacheKey, {
data,
timestamp: Date.now()
});
}
return data;
} catch (error) {
lastError = error;
// 最后一次重试失败后抛出错误
if (i === this.retries - 1) {
// 尝试从缓存返回过期数据
if (this.cache.has(cacheKey)) {
console.warn('API 请求失败,返回缓存数据');
return this.cache.get(cacheKey).data;
}
throw lastError;
}
// 等待一定时间后重试
await new Promise(resolve =>
setTimeout(resolve, this.retryDelay * Math.pow(2, i))
);
}
}
}
// 清理过期缓存
clearExpiredCache() {
const now = Date.now();
for (const [key, value] of this.cache.entries()) {
if (now - value.timestamp > 3600000) { // 1 小时
this.cache.delete(key);
}
}
}
}
const apiClient = new ResilientFetch({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 8000,
retries: 3,
});
export default apiClient;
数据降级策略
// components/DataWithFallback.js
import { useState, useEffect } from 'react';
import apiClient from '../utils/resilientFetch';
interface DataWithFallbackProps {
endpoint: string;
fallbackData?: any;
renderData: (data: any) => React.ReactNode;
renderLoading?: () => React.ReactNode;
renderError?: (error: Error) => React.ReactNode;
renderFallback?: (fallbackData: any) => React.ReactNode;
}
export function DataWithFallback({
endpoint,
fallbackData,
renderData,
renderLoading,
renderError,
renderFallback
}: DataWithFallbackProps) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [usingFallback, setUsingFallback] = useState(false);
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const result = await apiClient.fetch(endpoint, {
useCache: true,
cacheTime: 300000, // 5 分钟缓存
});
if (isMounted) {
setData(result);
setUsingFallback(false);
}
} catch (err) {
if (isMounted) {
setError(err);
// 使用回退数据
if (fallbackData) {
setData(fallbackData);
setUsingFallback(true);
}
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchData();
return () => {
isMounted = false;
};
}, [endpoint, fallbackData]);
if (loading && !data) {
return renderLoading ? renderLoading() : <div>加载中...</div>;
}
if (error && !data) {
return renderError ? renderError(error) : (
<div className="error-state">
<p>数据加载失败</p>
<button onClick={() => window.location.reload()}>
重新加载
</button>
</div>
);
}
if (usingFallback && renderFallback) {
return renderFallback(data);
}
return (
<>
{renderData(data)}
{usingFallback && (
<div className="fallback-notice">
ℹ️ 当前显示的是缓存数据
</div>
)}
</>
);
}
SSR 性能优化技巧
数据获取优化
并行数据加载
// 优化前:串行获取数据
export async function getServerSideProps({ params }) {
const user = await fetchUser(params.userId);
const posts = await fetchUserPosts(params.userId); // 等待上一个完成
const followers = await fetchUserFollowers(params.userId); // 等待上一个完成
return { props: { user, posts, followers } };
}
// 优化后:并行获取数据
export async function getServerSideProps({ params }) {
const [user, posts, followers] = await Promise.all([
fetchUser(params.userId),
fetchUserPosts(params.userId),
fetchUserFollowers(params.userId),
]);
return { props: { user, posts, followers } };
}
// 高级优化:批量请求
export async function getServerSideProps({ params }) {
// 使用 DataLoader 批量获取
const [userWithCounts, posts] = await Promise.all([
userLoader.load(params.userId), // 自动批量处理
postLoader.loadMany(await getUserPostIds(params.userId)),
]);
return {
props: {
user: userWithCounts,
posts,
},
};
}
DataLoader 实现
// utils/dataLoader.js
import DataLoader from 'dataloader';
import { batchFetchUsers, batchFetchPosts } from './api';
// 用户数据加载器
export const userLoader = new DataLoader(
async (userIds) => {
const users = await batchFetchUsers(userIds);
// 保证返回顺序与输入一致
return userIds.map(id =>
users.find(user => user.id === id) || new Error(`User ${id} not found`)
);
},
{
cache: true,
maxBatchSize: 100,
batchScheduleFn: callback => setTimeout(callback, 10), // 10ms 批量窗口
}
);
// 帖子数据加载器
export const postLoader = new DataLoader(
async (postIds) => {
const posts = await batchFetchPosts(postIds);
return postIds.map(id =>
posts.filter(post => post.id === id)
);
},
{
cache: true,
cacheKeyFn: (key) => `post:${key}`,
}
);
// GraphQL-style 批量获取
export async function batchFetchUsers(ids) {
const query = `
query BatchFetchUsers($ids: [ID!]!) {
users(ids: $ids) {
id
name
avatar
postsCount
followersCount
}
}
`;
const response = await fetch(process.env.GRAPHQL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables: { ids } }),
});
const { data } = await response.json();
return data.users;
}
缓存策略优化
多层缓存架构
flowchart TD
A[用户请求] --> B[CDN/Edge Cache]
B -->|Cache Miss| C[Load Balancer]
C --> D[Server Instance]
D --> E[Memory Cache L1]
E -->|Cache Miss| F[Redis Cache L2]
F -->|Cache Miss| G[Database]
G --> H[数据返回]
H --> F
F --> E
E --> D
D --> I[SSR Rendering]
I --> J[HTML Response]
J --> C
C --> B
B --> K[用户获得响应]
style B fill:#e3f2fd
style E fill:#f3e5f5
style F fill:#e8f5e8
style I fill:#fff3e0
智能缓存实现
// cache/smartCache.js
import { Redis } from 'ioredis';
import { LRUCache } from 'lru-cache';
class SmartCache {
constructor(options = {}) {
this.redis = new Redis(process.env.REDIS_URL);
this.memory = new LRUCache({
max: options.memoryMax || 1000,
ttl: options.memoryTTL || 60000, // 1 分钟
});
this.stats = {
hits: 0,
misses: 0,
memoryHits: 0,
redisHits: 0,
};
}
async get(key, options = {}) {
const { fallback, ttl = 300 } = options;
// L1: 内存缓存
const memoryValue = this.memory.get(key);
if (memoryValue) {
this.stats.hits++;
this.stats.memoryHits++;
return memoryValue;
}
// L2: Redis 缓存
const redisValue = await this.redis.get(key);
if (redisValue) {
this.stats.hits++;
this.stats.redisHits++;
const parsed = JSON.parse(redisValue);
// 回填 L1 缓存
this.memory.set(key, parsed);
return parsed;
}
// 缓存未命中
this.stats.misses++;
if (fallback) {
const value = await fallback();
await this.set(key, value, ttl);
return value;
}
return null;
}
async set(key, value, ttl = 300) {
// 同时设置两层缓存
this.memory.set(key, value);
await this.redis.setex(key, ttl, JSON.stringify(value));
}
// 智能失效
async invalidatePattern(pattern) {
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
// 批量删除 Redis
await this.redis.del(...keys);
// 清理内存缓存中匹配的键
for (const [memKey] of this.memory.entries()) {
if (this.matchPattern(memKey, pattern)) {
this.memory.delete(memKey);
}
}
}
}
// 缓存统计
getStats() {
const hitRate = this.stats.hits / (this.stats.hits + this.stats.misses) * 100;
return {
...this.stats,
hitRate: hitRate.toFixed(2) + '%',
memorySize: this.memory.size,
};
}
matchPattern(key, pattern) {
return new RegExp(pattern.replace(/\*/g, '.*')).test(key);
}
}
const cache = new SmartCache();
export default cache;
源码分割与加载优化
动态导入和代码分割
// next.config.js - 高级打包优化
module.exports = {
webpack: (config, { isServer, dev, defaultLoaders }) => {
if (!isServer && !dev) {
// 客户端生产环境优化
config.optimization.splitChunks = {
chunks: 'all',
minSize: 20000,
maxSize: 244000,
cacheGroups: {
// 框架代码
framework: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'framework',
chunks: 'all',
priority: 40,
enforce: true,
},
// 库代码
lib: {
test: /[\\/]node_modules[\\/]/,
name: 'lib',
chunks: 'all',
priority: 30,
minChunks: 2,
},
// 公共组件
commons: {
name: 'commons',
minChunks: 2,
priority: 20,
reuseExistingChunk: true,
},
// 页面特定代码
pages: {
test: /[\\/]pages[\\/]/,
name: 'pages',
priority: 10,
minChunks: 2,
},
},
};
}
return config;
},
// 实验性特性
experimental: {
modern: true, // 现代浏览器优化
optimizeServerReact: true, // 服务端 React 优化
serverComponents: true, // React Server Components
},
};
动态组件加载
// components/DynamicLoader.js
import dynamic from 'next/dynamic';
import { Suspense } from 'react';
// 基础动态加载
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <div>加载中...</div>,
ssr: false, // 禁用 SSR
});
// 条件加载
const AdminPanel = dynamic(
() => import('./AdminPanel'),
{
loading: () => <div>加载管理面板...</div>,
ssr: false,
}
);
// 多组件批量加载
const DynamicComponents = {
Chart: dynamic(() => import('./Chart'), {
loading: () => <div>加载图表...</div>,
}),
DataTable: dynamic(() => import('./DataTable'), {
loading: () => <div>加载表格...</div>,
}),
Map: dynamic(() => import('./Map'), {
loading: () => <div>加载地图...</div>,
ssr: false, // 地图组件通常不需要 SSR
}),
};
// 智能组件加载器
export function SmartComponentLoader({ componentName, condition, fallback, ...props }) {
if (!condition) {
return fallback || null;
}
const Component = DynamicComponents[componentName];
if (!Component) {
console.warn(`Component ${componentName} not found`);
return fallback || <div>组件不存在</div>;
}
return (
<Suspense fallback={fallback || <div>加载中...</div>}>
<Component {...props} />
</Suspense>
);
}
// 使用示例
export default function Dashboard({ user, showChart, showMap }) {
return (
<div className="dashboard">
<h1>仪表盘</h1>
{/* 条件性加载组件 */}
<SmartComponentLoader
componentName="Chart"
condition={showChart}
data={user.chartData}
fallback={<div className="chart-placeholder">图表占位符</div>}
/>
<SmartComponentLoader
componentName="Map"
condition={showMap}
locations={user.locations}
/>
{/* 管理员功能 */}
{user.isAdmin && (
<SmartComponentLoader
componentName="AdminPanel"
condition={user.isAdmin}
userId={user.id}
/>
)}
</div>
);
}
性能监控与调优
性能指标收集
// utils/performanceMonitor.js
class SSRPerformanceMonitor {
constructor() {
this.metrics = new Map();
this.thresholds = {
ttfb: 800, // Time to First Byte
renderTime: 2000, // SSR 渲染时间
memoryUsage: 1024 * 1024 * 100, // 100MB
};
}
startTiming(label) {
this.metrics.set(label, {
startTime: process.hrtime.bigint(),
startMemory: process.memoryUsage(),
});
}
endTiming(label) {
const metric = this.metrics.get(label);
if (!metric) return null;
const endTime = process.hrtime.bigint();
const endMemory = process.memoryUsage();
const result = {
label,
duration: Number(endTime - metric.startTime) / 1000000, // 转换为毫秒
memoryDelta: endMemory.heapUsed - metric.startMemory.heapUsed,
timestamp: new Date().toISOString(),
};
this.metrics.delete(label);
this.checkThresholds(result);
return result;
}
checkThresholds(result) {
const { label, duration, memoryDelta } = result;
if (label.includes('render') && duration > this.thresholds.renderTime) {
console.warn(`渲染耗时警告: ${label} 耗时 ${duration}ms`);
}
if (Math.abs(memoryDelta) > this.thresholds.memoryUsage) {
console.warn(`内存使用警告: ${label} 内存变化 ${memoryDelta / 1024 / 1024}MB`);
}
}
// 收集系统指标
getSystemMetrics() {
const memUsage = process.memoryUsage();
const cpuUsage = process.cpuUsage();
return {
memory: {
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024),
external: Math.round(memUsage.external / 1024 / 1024),
rss: Math.round(memUsage.rss / 1024 / 1024),
},
cpu: {
user: cpuUsage.user / 1000, // 转换为毫秒
system: cpuUsage.system / 1000,
},
uptime: process.uptime(),
};
}
}
const monitor = new SSRPerformanceMonitor();
export default monitor;
Express 中间件集成
// middleware/performanceMiddleware.js
import monitor from '../utils/performanceMonitor';
import { Analytics } from '@segment/analytics-node';
const analytics = new Analytics({
writeKey: process.env.SEGMENT_WRITE_KEY,
});
export function performanceMiddleware() {
return (req, res, next) => {
const requestId = req.headers['x-request-id'] || Math.random().toString(36);
const startTime = Date.now();
// 开始监控
monitor.startTiming(`request:${requestId}`);
// 拦截响应
const originalSend = res.send;
res.send = function(body) {
const metrics = monitor.endTiming(`request:${requestId}`);
const ttfb = Date.now() - startTime;
// 记录性能数据
const performanceData = {
requestId,
url: req.url,
method: req.method,
userAgent: req.headers['user-agent'],
ttfb,
renderTime: metrics?.duration || 0,
contentLength: body.length,
statusCode: res.statusCode,
...monitor.getSystemMetrics(),
};
// 发送到分析平台
analytics.track({
userId: req.user?.id || 'anonymous',
event: 'SSR Performance',
properties: performanceData,
});
// 设置响应头
res.setHeader('X-Response-Time', `${ttfb}ms`);
res.setHeader('X-Render-Time', `${metrics?.duration || 0}ms`);
return originalSend.call(this, body);
};
next();
};
}
最佳实践与总结
SSR 开发最佳实践
1. 渲染策略选择
flowchart LR
A[页面类型分析] --> B{内容特性}
B -->|静态内容| C[SSG]
B -->|个性化内容| D[SSR]
B -->|半静态内容| E[ISR]
B -->|高交互性| F[CSR]
C --> G[博客文章\n产品介绍\n营销页面]
D --> H[用户仪表盘\n购物车\n个人资料]
E --> I[产品列表\n新闻列表\n评论区]
F --> J[在线编辑器\n游戏界面\n实时聊天]
style C fill:#e8f5e8
style D fill:#f3e5f5
style E fill:#e3f2fd
style F fill:#fff3e0
2. 性能优化检查清单
// utils/ssrOptimizationChecklist.js
export const SSROptimizationChecklist = {
// 数据获取优化
dataFetching: {
parallelRequests: "✅ 使用 Promise.all 并行获取数据",
dataLoaderPattern: "✅ 实现 DataLoader 批量处理",
caching: "✅ 多层缓存策略",
errorHandling: "✅ 优雅的错误处理和降级",
},
// 渲染优化
rendering: {
componentSplitting: "✅ 合理的组件拆分",
lazyLoading: "✅ 非关键组件懒加载",
memoization: "✅ 使用 React.memo 和 useMemo",
streaming: "✅ 支持流式渲染",
},
// 资源优化
assets: {
codeSplitting: "✅ 代码分割和按需加载",
imageOptimization: "✅ 图片优化和懒加载",
cssOptimization: "✅ 关键 CSS 内联",
fontOptimization: "✅ 字体预加载和优化",
},
// 缓存策略
caching: {
httpCaching: "✅ 合适的 HTTP 缓存头",
edgeCaching: "✅ CDN/Edge 缓存配置",
applicationCaching: "✅ 应用级缓存",
browserCaching: "✅ 浏览器缓存优化",
},
// 监控和调试
monitoring: {
performanceMetrics: "✅ 性能指标收集",
errorTracking: "✅ 错误监控和上报",
realUserMonitoring: "✅ 真实用户监控",
loadTesting: "✅ 负载测试验证",
},
};
// 自动化检查函数
export function checkSSROptimization(app) {
const issues = [];
// 检查是否实现了关键优化
if (!app.config.experimental?.optimizeServerReact) {
issues.push("建议启用 optimizeServerReact");
}
if (!app.config.webpack?.optimization?.splitChunks) {
issues.push("未配置代码分割");
}
if (!app.middleware?.includes('compression')) {
issues.push("未启用 gzip/brotli 压缩");
}
return {
score: Math.max(0, 100 - issues.length * 10),
issues,
recommendations: generateRecommendations(issues),
};
}
function generateRecommendations(issues) {
const recommendations = {
"未配置代码分割": "配置 webpack splitChunks 以减少 bundle 大小",
"未启用 gzip/brotli 压缩": "启用 compression 中间件以减少传输大小",
"建议启用 optimizeServerReact": "在 next.config.js 中启用 experimental.optimizeServerReact",
};
return issues.map(issue => recommendations[issue] || "请查阅文档获取优化建议");
}
3. 错误处理和监控
// utils/ssrErrorHandler.js
export class SSRErrorHandler {
constructor(options = {}) {
this.isDevelopment = process.env.NODE_ENV === 'development';
this.sentryDsn = options.sentryDsn;
this.fallbackComponent = options.fallbackComponent;
this.onError = options.onError;
}
// 全局错误处理
handleRenderError(error, context) {
const errorInfo = {
message: error.message,
stack: this.isDevelopment ? error.stack : undefined,
url: context.req?.url,
userAgent: context.req?.headers?.['user-agent'],
timestamp: new Date().toISOString(),
pid: process.pid,
};
// 记录错误
console.error('SSR Render Error:', errorInfo);
// 发送到监控服务
if (this.sentryDsn) {
this.reportToSentry(error, context);
}
// 自定义错误处理
if (this.onError) {
this.onError(error, context);
}
// 返回降级内容
return this.getFallbackResponse(error, context);
}
getFallbackResponse(error, context) {
// 根据错误类型返回不同的降级方案
if (error.name === 'TimeoutError') {
return {
props: {
error: 'timeout',
fallbackData: this.getCachedData(context.req.url),
},
};
}
if (error.status === 404) {
return { notFound: true };
}
// 通用降级
return {
props: {
error: 'general',
message: this.isDevelopment ? error.message : '服务暂时不可用',
},
};
}
reportToSentry(error, context) {
// Sentry 错误上报
if (typeof window === 'undefined') {
// 服务端错误上报
import('@sentry/node').then(Sentry => {
Sentry.captureException(error, {
tags: {
component: 'ssr',
url: context.req?.url,
},
extra: {
userAgent: context.req?.headers?.['user-agent'],
referer: context.req?.headers?.referer,
},
});
});
}
}
getCachedData(url) {
// 尝试获取缓存数据作为降级方案
try {
return cache.get(`fallback:${url}`) || null;
} catch {
return null;
}
}
}
// 使用示例
export const errorHandler = new SSRErrorHandler({
sentryDsn: process.env.SENTRY_DSN,
onError: (error, context) => {
// 自定义错误处理逻辑
console.log('Custom error handling:', error.message);
},
});
4. 开发和调试工具
// utils/ssrDebugger.js
export class SSRDebugger {
constructor() {
this.isDevelopment = process.env.NODE_ENV === 'development';
this.debugMode = process.env.SSR_DEBUG === 'true';
}
// 性能分析
analyzeSSRPerformance(metrics) {
if (!this.debugMode) return;
const analysis = {
renderTime: metrics.renderTime,
dataFetchTime: metrics.dataFetchTime,
hydrationTime: metrics.hydrationTime,
recommendations: [],
};
// 渲染时间分析
if (metrics.renderTime > 2000) {
analysis.recommendations.push("渲染时间过长,考虑组件优化或数据预加载");
}
// 数据获取分析
if (metrics.dataFetchTime > 1000) {
analysis.recommendations.push("数据获取耗时过长,考虑并行请求或缓存优化");
}
// Hydration 分析
if (metrics.hydrationTime > 1000) {
analysis.recommendations.push("Hydration 时间过长,检查客户端代码分割");
}
console.table(analysis);
return analysis;
}
// 组件渲染分析
analyzeComponentRender(componentName, renderTime) {
if (!this.debugMode) return;
console.log(`🔍 Component Render Analysis:`, {
component: componentName,
renderTime: `${renderTime}ms`,
status: renderTime > 100 ? '⚠️ Slow' : '✅ Fast',
});
}
// 数据获取分析
analyzeDataFetching(endpoint, duration, cacheHit) {
if (!this.debugMode) return;
console.log(`📊 Data Fetching Analysis:`, {
endpoint,
duration: `${duration}ms`,
cacheHit: cacheHit ? '✅ Hit' : '❌ Miss',
performance: duration > 500 ? '⚠️ Slow' : '✅ Fast',
});
}
// Bundle 分析
analyzeBundleSize(bundles) {
if (!this.debugMode) return;
const analysis = bundles.map(bundle => ({
name: bundle.name,
size: `${(bundle.size / 1024).toFixed(2)}KB`,
gzipped: `${(bundle.gzipped / 1024).toFixed(2)}KB`,
recommendation: bundle.size > 250000 ? '考虑进一步拆分' : '大小合适',
}));
console.table(analysis);
}
}
export const ssrDebugger = new SSRDebugger();
总结
通过本文的深入探讨,我们全面了解了 React SSR 的优势、实现方案和优化策略:
核心收益
- 首屏性能提升 - 显著减少 FCP 和 LCP 时间
- SEO 友好 - 搜索引擎可直接索引内容
- 更好的用户体验 - 即使 JavaScript 失败也能展示内容
- 社交媒体分享优化 - 动态生成 Open Graph 数据
技术要点
- 渲染策略选择 - 根据内容特性选择 SSR/SSG/ISR
- 性能优化 - 数据获取并行化、缓存策略、代码分割
- 错误处理 - 优雅降级、监控告警
- 开发体验 - 调试工具、性能分析
最佳实践
- 渐进式采用 - 从关键页面开始,逐步扩展
- 性能监控 - 持续监控关键指标,及时优化
- 缓存策略 - 多层缓存,智能失效
- 错误容错 - 完善的降级机制
SSR 不是银弹,需要根据具体场景权衡成本和收益。正确实施 SSR 可以显著提升应用的性能和用户体验,但也需要投入相应的开发和维护成本。通过本文提供的实践方案和优化技巧,可以帮助团队更好地实施和优化 SSR 应用。