预渲染优化
前端预渲染技术及实现方式
问题
什么是预渲染?有哪些实现方式?如何选择合适的预渲染策略?
解答
预渲染是在用户请求之前,提前生成页面 HTML 的技术。目的是减少首屏白屏时间,提升 SEO。
预渲染的类型
| 类型 | 生成时机 | 适用场景 |
|---|---|---|
| SSG(静态站点生成) | 构建时 | 博客、文档、营销页 |
| SSR(服务端渲染) | 请求时 | 动态内容、用户相关页面 |
| ISR(增量静态再生) | 构建时 + 按需更新 | 内容频繁更新的静态页 |
| Prerender | 构建时爬取 SPA | 少量静态页的 SPA |
1. SSG 静态站点生成
// Next.js 中的 SSG
// pages/posts/[id].js
// 构建时获取所有路径
export async function getStaticPaths() {
const posts = await fetchAllPosts()
return {
paths: posts.map(post => ({
params: { id: post.id }
})),
fallback: false // 未匹配路径返回 404
}
}
// 构建时获取页面数据
export async function getStaticProps({ params }) {
const post = await fetchPost(params.id)
return {
props: { post }
}
}
export default function Post({ post }) {
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
2. SSR 服务端渲染
// Next.js 中的 SSR
// pages/dashboard.js
// 每次请求时执行
export async function getServerSideProps({ req, res }) {
// 可以访问请求信息,如 cookie
const token = req.cookies.token
const user = await fetchUser(token)
// 设置缓存头
res.setHeader('Cache-Control', 's-maxage=60, stale-while-revalidate')
return {
props: { user }
}
}
export default function Dashboard({ user }) {
return <div>Welcome, {user.name}</div>
}
3. ISR 增量静态再生
// Next.js 中的 ISR
// pages/products/[id].js
export async function getStaticProps({ params }) {
const product = await fetchProduct(params.id)
return {
props: { product },
revalidate: 60 // 60 秒后重新生成
}
}
export async function getStaticPaths() {
// 只预渲染热门商品
const hotProducts = await fetchHotProducts()
return {
paths: hotProducts.map(p => ({ params: { id: p.id } })),
fallback: 'blocking' // 其他路径首次访问时 SSR,然后缓存
}
}
4. SPA 预渲染(prerender-spa-plugin)
// vue.config.js 或 webpack.config.js
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const path = require('path')
module.exports = {
plugins: [
new PrerenderSPAPlugin({
staticDir: path.join(__dirname, 'dist'),
// 需要预渲染的路由
routes: ['/', '/about', '/contact'],
renderer: new PrerenderSPAPlugin.PuppeteerRenderer({
// 等待特定元素出现后再捕获
renderAfterElementExists: '#app',
// 或等待特定时间
renderAfterTime: 5000,
// 注入变量标识预渲染环境
injectProperty: '__PRERENDER_INJECTED',
inject: { isPrerendering: true }
})
})
]
}
5. 手动实现简单预渲染
// prerender.js
const puppeteer = require('puppeteer')
const fs = require('fs')
const path = require('path')
async function prerender(routes, outputDir) {
const browser = await puppeteer.launch()
for (const route of routes) {
const page = await browser.newPage()
// 访问开发服务器
await page.goto(`http://localhost:3000${route}`, {
waitUntil: 'networkidle0' // 等待网络空闲
})
// 获取渲染后的 HTML
const html = await page.content()
// 写入文件
const filePath = path.join(outputDir, route, 'index.html')
fs.mkdirSync(path.dirname(filePath), { recursive: true })
fs.writeFileSync(filePath, html)
console.log(`Prerendered: ${route}`)
await page.close()
}
await browser.close()
}
// 使用
prerender(['/', '/about', '/pricing'], './dist')
预渲染策略选择
// 根据页面特性选择渲染策略
const renderingStrategy = {
// 纯静态内容 -> SSG
'/about': 'SSG',
'/docs/*': 'SSG',
// 内容更新频率中等 -> ISR
'/blog/*': 'ISR',
'/products/*': 'ISR',
// 用户相关/实时数据 -> SSR
'/dashboard': 'SSR',
'/cart': 'SSR',
// 高度交互/无 SEO 需求 -> CSR
'/app/*': 'CSR'
}
预渲染注意事项
// 1. 处理客户端专属 API
if (typeof window !== 'undefined') {
// 浏览器环境
localStorage.setItem('key', 'value')
}
// 2. 动态导入客户端组件
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('../components/Chart'), {
ssr: false, // 禁用服务端渲染
loading: () => <div>Loading chart...</div>
})
// 3. 处理水合不匹配
function TimeDisplay() {
const [time, setTime] = useState(null)
useEffect(() => {
// 客户端才设置时间,避免水合不匹配
setTime(new Date().toLocaleString())
}, [])
return <span>{time ?? 'Loading...'}</span>
}
关键点
- SSG 构建时生成,适合静态内容,性能最好
- SSR 请求时生成,适合动态/用户相关内容
- ISR 结合 SSG 和 SSR 优点,支持增量更新
- 预渲染需处理水合问题,避免服务端和客户端 HTML 不一致
- 按页面特性选择策略,不必全站统一方案
目录