预渲染优化

前端预渲染技术及实现方式

问题

什么是预渲染?有哪些实现方式?如何选择合适的预渲染策略?

解答

预渲染是在用户请求之前,提前生成页面 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 不一致
  • 按页面特性选择策略,不必全站统一方案