页面提速:Puppeteer 预渲染

# 页面提速:Puppeteer 预渲染

# 背景

H5营销活动页面主要采用Vue/React SPA架构,面临以下性能问题:

首屏加载慢

用户访问流程:
1. 下载HTML(空白页面)          100-200ms
2. 下载JS Bundle(200KB+)       500-1000ms
3. 执行JS,Vue/React初始化       300-500ms
4. API请求获取数据               500-800ms
5. 渲染页面                      100-200ms

总计首屏时间:1500-2700ms
1
2
3
4
5
6
7
8

SEO不友好

  • 搜索引擎爬虫获取到空白HTML
  • 页面Meta信息无法动态设置
  • 社交分享链接无法展示正确的标题和图片

用户体验差

  • 白屏时间长达1.5-2秒
  • 弱网环境下体验更差
  • 低端设备JS执行慢

需要通过**预渲染(Prerender)**技术,在构建时生成静态HTML,大幅降低首屏时间。

# 问题分析

# 1. 渲染方案对比

# CSR(客户端渲染)- 现状

// index.html(构建产物)
<!DOCTYPE html>
<html>
<head>
  <title>活动页</title>
</head>
<body>
  <div id="app"></div>  <!-- 空白! -->
  <script src="/js/chunk-vendors.js"></script>
  <script src="/js/app.js"></script>
</body>
</html>

// 用户看到页面的流程:
// HTML(空) -> 下载JS -> 执行JS -> 请求API -> 渲染
// 首屏时间:1500-2700ms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# SSR(服务端渲染)

// 服务器实时渲染
app.get('*', async (req, res) => {
  const app = createApp();
  const html = await renderToString(app);
  res.send(`
    <!DOCTYPE html>
    <html>
      <body>
        <div id="app">${html}</div>
        <script src="/js/app.js"></script>
      </body>
    </html>
  `);
});

// 优点:首屏快,SEO友好
// 缺点:
// 1. 需要Node.js服务器
// 2. 服务器压力大
// 3. 维护成本高
// 4. 不适合静态部署(CDN)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# Prerender(预渲染)- 方案选择

// 构建时使用Puppeteer渲染生成HTML
// index.html(构建产物)
<!DOCTYPE html>
<html>
<head>
  <title>2024春节活动</title>
  <meta name="description" content="参与活动赢大奖">
</head>
<body>
  <div id="app">
    <div class="banner">...</div>  <!-- 已渲染! -->
    <div class="products">...</div>
  </div>
  <script src="/js/app.js"></script>
</body>
</html>

// 用户看到页面的流程:
// HTML(含内容) -> 下载JS -> Hydrate(激活)
// 首屏时间:200-500ms(降低75%+)

// 优点:
// 1. 首屏极快
// 2. SEO友好
// 3. 静态部署,CDN加速
// 4. 服务器无压力
// 5. 开发体验同SPA

// 缺点:
// 1. 不适合用户相关的动态内容
// 2. 构建时间增加
// 3. 只适合页面数量有限的场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 2. 技术选型对比

方案 工作原理 优点 缺点
prerender-spa-plugin Webpack插件,Puppeteer 配置简单 灵活性差,配置受限
react-snap CLI工具,Puppeteer 零配置 不支持Vue 3,已停止维护
rendertron Google方案,headless Chrome 企业级 需要独立服务,复杂
自建Puppeteer方案 完全自定义 灵活可控 需要自己实现

最终选择:自建Puppeteer方案

  • 完全控制渲染流程
  • 支持复杂场景(多路由、动态数据)
  • 可以集成到CI/CD
  • 便于调试和优化

# 解决方案

# 1. 核心实现 - 预渲染脚本

// scripts/prerender.js
const puppeteer = require('puppeteer');
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');

/**
 * 预渲染配置
 */
const config = {
  // 构建输出目录
  distDir: path.resolve(__dirname, '../dist'),

  // 需要预渲染的路由
  routes: [
    '/',
    '/activity',
    '/product/123',
    '/about'
  ],

  // 等待渲染完成的条件
  renderAfterDocumentEvent: 'prerender-ready',

  // 超时时间
  timeout: 30000,

  // 视口大小
  viewport: {
    width: 375,
    height: 667,
    deviceScaleFactor: 2
  },

  // 是否注入预渲染标记
  injectProperty: '__PRERENDER_INJECTED__',

  // 需要移除的脚本/样式
  removeScripts: true,
  removeStyles: false
};

/**
 * 预渲染主函数
 */
async function prerender() {
  console.log(chalk.blue('开始预渲染...'));

  const browser = await puppeteer.launch({
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });

  try {
    for (const route of config.routes) {
      await prerenderRoute(browser, route);
    }

    console.log(chalk.green('预渲染完成!'));
  } catch (error) {
    console.error(chalk.red('预渲染失败:'), error);
    throw error;
  } finally {
    await browser.close();
  }
}

/**
 * 预渲染单个路由
 */
async function prerenderRoute(browser, route) {
  console.log(chalk.cyan(`渲染路由: ${route}`));

  const page = await browser.newPage();

  try {
    // 设置视口
    await page.setViewport(config.viewport);

    // 注入预渲染标记(可选)
    await page.evaluateOnNewDocument((prop) => {
      window[prop] = true;
    }, config.injectProperty);

    // 访问页面
    const url = `http://localhost:8080${route}`;
    await page.goto(url, {
      waitUntil: 'networkidle0',
      timeout: config.timeout
    });

    // 等待特定事件(如Vue mounted后触发)
    await page.evaluate((eventName) => {
      return new Promise((resolve) => {
        if (document.readyState === 'complete') {
          // 检查是否已触发事件
          if (window.__PRERENDER_READY__) {
            resolve();
          } else {
            document.addEventListener(eventName, resolve);
          }
        }
      });
    }, config.renderAfterDocumentEvent);

    // 额外等待确保动画完成
    await page.waitForTimeout(500);

    // 获取渲染后的HTML
    let html = await page.content();

    // 后处理HTML
    html = postprocessHtml(html);

    // 保存HTML文件
    await saveHtml(route, html);

    console.log(chalk.green(`✓ ${route} 渲染完成`));
  } catch (error) {
    console.error(chalk.red(`✗ ${route} 渲染失败:`), error.message);
    throw error;
  } finally {
    await page.close();
  }
}

/**
 * HTML后处理
 */
function postprocessHtml(html) {
  // 移除Puppeteer注入的脚本
  if (config.removeScripts) {
    // 保留必要的脚本(如app.js),移除内联脚本
    html = html.replace(
      /<script(?![^>]*src=["'][^"']*\.js["'])[^>]*>[\s\S]*?<\/script>/gi,
      ''
    );
  }

  // 移除data-server-rendered属性(Vue SSR会添加)
  html = html.replace(/\s*data-server-rendered="true"/g, '');

  // 移除预渲染标记脚本
  html = html.replace(
    new RegExp(`<script[^>]*>${config.injectProperty}[\\s\\S]*?<\\/script>`, 'gi'),
    ''
  );

  // 压缩HTML
  html = minifyHtml(html);

  return html;
}

/**
 * HTML压缩(简单版)
 */
function minifyHtml(html) {
  return html
    .replace(/\s*\n\s*/g, '') // 移除换行和缩进
    .replace(/>\s+</g, '><') // 移除标签间空白
    .replace(/<!--[\s\S]*?-->/g, ''); // 移除HTML注释
}

/**
 * 保存HTML到文件
 */
async function saveHtml(route, html) {
  // 计算文件路径
  let filePath;
  if (route === '/') {
    filePath = path.join(config.distDir, 'index.html');
  } else {
    // /activity -> /activity/index.html
    filePath = path.join(config.distDir, route, 'index.html');
  }

  // 确保目录存在
  await fs.ensureDir(path.dirname(filePath));

  // 写入文件
  await fs.writeFile(filePath, html, 'utf-8');

  console.log(chalk.gray(`  保存至: ${filePath}`));
}

/**
 * 启动开发服务器(用于预渲染)
 */
async function startDevServer() {
  const express = require('express');
  const app = express();

  app.use(express.static(config.distDir));

  // SPA fallback
  app.get('*', (req, res) => {
    res.sendFile(path.join(config.distDir, 'index.html'));
  });

  const server = app.listen(8080);

  return {
    close: () => server.close()
  };
}

/**
 * 主流程
 */
async function main() {
  // 1. 启动静态服务器
  console.log(chalk.blue('启动静态服务器...'));
  const server = await startDevServer();

  try {
    // 2. 执行预渲染
    await prerender();
  } finally {
    // 3. 关闭服务器
    server.close();
  }
}

// 执行
main().catch((error) => {
  console.error(error);
  process.exit(1);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229

# 2. Vue应用适配

// main.js
import Vue from 'vue';
import App from './App.vue';
import router from './router';

const app = new Vue({
  router,
  render: h => h(App),

  // 预渲染钩子:mounted后通知预渲染可以截取HTML了
  mounted() {
    // 检测预渲染环境
    if (window.__PRERENDER_INJECTED__) {
      // 等待所有异步组件和数据加载完成
      this.$nextTick(() => {
        // 触发预渲染就绪事件
        window.__PRERENDER_READY__ = true;
        document.dispatchEvent(new Event('prerender-ready'));
      });
    }
  }
});

app.$mount('#app');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const router = new VueRouter({
  mode: 'history', // 必须使用history模式
  routes: [
    {
      path: '/',
      component: () => import('@/views/Home.vue')
    },
    {
      path: '/activity',
      component: () => import('@/views/Activity.vue')
    }
  ]
});

export default router;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- views/Activity.vue -->
<template>
  <div class="activity">
    <h1>{{ title }}</h1>
    <div v-if="loaded" class="content">
      <!-- 内容 -->
    </div>
    <Loading v-else />
  </div>
</template>

<script>
export default {
  name: 'Activity',

  data() {
    return {
      title: '春节活动',
      loaded: false,
      products: []
    };
  },

  // 预渲染时会执行
  async mounted() {
    await this.fetchData();
  },

  methods: {
    async fetchData() {
      // 预渲染时使用mock数据
      if (window.__PRERENDER_INJECTED__) {
        this.products = this.getMockData();
        this.loaded = true;
        return;
      }

      // 正常环境请求真实API
      try {
        const res = await api.getProducts();
        this.products = res.data;
        this.loaded = true;
      } catch (error) {
        console.error('获取数据失败', error);
      }
    },

    getMockData() {
      return [
        { id: 1, name: '商品1' },
        { id: 2, name: '商品2' }
      ];
    }
  },

  // 设置页面Meta(用于SEO)
  metaInfo() {
    return {
      title: this.title,
      meta: [
        {
          name: 'description',
          content: '2024春节活动,参与赢大奖'
        },
        {
          property: 'og:title',
          content: this.title
        },
        {
          property: 'og:image',
          content: 'https://example.com/activity-cover.jpg'
        }
      ]
    };
  }
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

# 3. React应用适配

// App.jsx
import React, { useEffect } from 'react';
import { BrowserRouter } from 'react-router-dom';
import Routes from './routes';

function App() {
  useEffect(() => {
    // 预渲染环境检测
    if (window.__PRERENDER_INJECTED__) {
      // 通知预渲染完成
      setTimeout(() => {
        window.__PRERENDER_READY__ = true;
        document.dispatchEvent(new Event('prerender-ready'));
      }, 100);
    }
  }, []);

  return (
    <BrowserRouter>
      <Routes />
    </BrowserRouter>
  );
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// pages/Activity.jsx
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet';

function Activity() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchData();
  }, []);

  const fetchData = async () => {
    // 预渲染时使用mock数据
    if (window.__PRERENDER_INJECTED__) {
      setProducts(getMockData());
      setLoading(false);
      return;
    }

    try {
      const res = await api.getProducts();
      setProducts(res.data);
    } catch (error) {
      console.error(error);
    } finally {
      setLoading(false);
    }
  };

  const getMockData = () => [
    { id: 1, name: '商品1' },
    { id: 2, name: '商品2' }
  ];

  return (
    <div className="activity">
      <Helmet>
        <title>2024春节活动</title>
        <meta name="description" content="参与活动赢大奖" />
      </Helmet>

      <h1>春节活动</h1>

      {loading ? (
        <Loading />
      ) : (
        <div className="products">
          {products.map(item => (
            <div key={item.id}>{item.name}</div>
          ))}
        </div>
      )}
    </div>
  );
}

export default Activity;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 4. 构建流程集成

// package.json
{
  "scripts": {
    "build": "npm run build:app && npm run prerender",
    "build:app": "vue-cli-service build",
    "prerender": "node scripts/prerender.js",
    "serve": "vue-cli-service serve"
  },
  "devDependencies": {
    "puppeteer": "^19.0.0",
    "express": "^4.18.0",
    "fs-extra": "^11.0.0",
    "chalk": "^4.1.2"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 5. 高级功能 - 动态路由预渲染

// scripts/prerender.js(增强版)

/**
 * 从API获取动态路由列表
 */
async function getDynamicRoutes() {
  const axios = require('axios');

  try {
    // 获取所有商品ID
    const { data } = await axios.get('https://api.example.com/products');

    // 生成商品详情路由
    const productRoutes = data.map(item => `/product/${item.id}`);

    return productRoutes;
  } catch (error) {
    console.error('获取动态路由失败:', error);
    return [];
  }
}

/**
 * 主流程(支持动态路由)
 */
async function main() {
  // 1. 获取动态路由
  console.log(chalk.blue('获取动态路由...'));
  const dynamicRoutes = await getDynamicRoutes();
  config.routes = [...config.routes, ...dynamicRoutes];

  console.log(chalk.cyan(`共需渲染 ${config.routes.length} 个路由`));

  // 2. 启动服务器
  const server = await startDevServer();

  try {
    // 3. 执行预渲染
    await prerender();
  } finally {
    server.close();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 6. 缓存优化

// scripts/prerender.js(缓存版本)

const crypto = require('crypto');

/**
 * 计算路由内容hash
 */
async function getRouteHash(route) {
  try {
    const filePath = getHtmlPath(route);
    const content = await fs.readFile(filePath, 'utf-8');
    return crypto.createHash('md5').update(content).digest('hex');
  } catch {
    return null;
  }
}

/**
 * 预渲染单个路由(带缓存)
 */
async function prerenderRouteWithCache(browser, route) {
  // 检查缓存
  const cacheFile = path.join(config.distDir, '.prerender-cache.json');
  let cache = {};

  try {
    cache = await fs.readJSON(cacheFile);
  } catch {
    // 缓存文件不存在
  }

  // 计算当前hash
  const currentHash = await getRouteHash(route);

  // 如果hash相同,跳过渲染
  if (cache[route] === currentHash) {
    console.log(chalk.gray(`⊙ ${route} 未变化,跳过`));
    return;
  }

  // 执行渲染
  await prerenderRoute(browser, route);

  // 更新缓存
  const newHash = await getRouteHash(route);
  cache[route] = newHash;
  await fs.writeJSON(cacheFile, cache, { spaces: 2 });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 7. CI/CD集成

# .github/workflows/deploy.yml
name: Build and Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Prerender
        run: npm run prerender
        env:
          PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: false

      - name: Deploy to CDN
        run: |
          # 上传dist目录到CDN
          npm run deploy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 8. 错误处理和重试

// scripts/prerender.js(错误处理)

/**
 * 带重试的预渲染
 */
async function prerenderWithRetry(browser, route, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      await prerenderRoute(browser, route);
      return; // 成功,退出
    } catch (error) {
      console.error(
        chalk.yellow(`${route} 渲染失败 (${i + 1}/${retries}):`, error.message)
      );

      if (i === retries - 1) {
        // 最后一次重试也失败
        throw error;
      }

      // 等待后重试
      await new Promise(resolve => setTimeout(resolve, 2000));
    }
  }
}

/**
 * 并发渲染(提升速度)
 */
async function prerenderConcurrent() {
  const browser = await puppeteer.launch({
    headless: true,
    args: ['--no-sandbox']
  });

  const concurrency = 5; // 同时渲染5个页面
  const chunks = [];

  // 将路由分组
  for (let i = 0; i < config.routes.length; i += concurrency) {
    chunks.push(config.routes.slice(i, i + concurrency));
  }

  try {
    for (const chunk of chunks) {
      await Promise.all(
        chunk.map(route => prerenderWithRetry(browser, route))
      );
    }
  } finally {
    await browser.close();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 9. 性能监控

// scripts/prerender.js(性能监控)

const startTime = Date.now();
const stats = {
  total: 0,
  success: 0,
  failed: 0,
  skipped: 0,
  duration: {}
};

async function prerenderRoute(browser, route) {
  const routeStartTime = Date.now();

  try {
    // ... 预渲染逻辑

    stats.success++;
    stats.duration[route] = Date.now() - routeStartTime;
  } catch (error) {
    stats.failed++;
    throw error;
  } finally {
    stats.total++;
  }
}

function printStats() {
  const totalTime = Date.now() - startTime;

  console.log(chalk.blue('\n=== 预渲染统计 ==='));
  console.log(`总计: ${stats.total} 个路由`);
  console.log(chalk.green(`成功: ${stats.success}`));
  console.log(chalk.red(`失败: ${stats.failed}`));
  console.log(chalk.gray(`跳过: ${stats.skipped}`));
  console.log(`总耗时: ${(totalTime / 1000).toFixed(2)}s`);

  // 最慢的路由
  const slowest = Object.entries(stats.duration)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 5);

  console.log('\n最慢的5个路由:');
  slowest.forEach(([route, time]) => {
    console.log(`  ${route}: ${time}ms`);
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 效果与总结

# 1. 性能提升数据

首屏加载时间对比

优化前(CSR):
  HTML下载: 150ms
  JS下载: 800ms
  JS执行: 400ms
  API请求: 600ms
  首次渲染: 200ms
  总计: 2150ms

优化后(Prerender):
  HTML下载: 200ms(包含完整内容)
  JS下载: 800ms(并行)
  Hydrate: 100ms
  总计: 300ms

首屏时间降低: 86%
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

真实测试数据(30个页面)

构建耗时: +45s(可接受)
页面大小: 30KB -> 85KB(+55KB,含渲染内容)
首屏时间:
  - 4G网络: 2100ms -> 450ms(↓78%)
  - 3G网络: 4200ms -> 800ms(↓81%)
  - 弱网: 8000ms -> 1500ms(↓81%)

Lighthouse评分:
  Performance: 65 -> 92(+27)
  SEO: 70 -> 98(+28)
1
2
3
4
5
6
7
8
9
10

SEO效果

Google索引速度: 提升3倍
搜索引擎可见性: +60%
社交分享点击率: +45%
1
2
3

# 2. 最佳实践总结

1. 适用场景

✅ 适合预渲染:
- 营销活动页(内容相对固定)
- 产品介绍页
- 官网首页
- 博客文章列表
- 落地页

❌ 不适合预渲染:
- 用户中心(需要登录)
- 实时数据页面(股票、比分)
- 个性化推荐
- 后台管理系统
1
2
3
4
5
6
7
8
9
10
11
12

2. 数据处理策略

// 方案1:Mock数据(推荐)
if (window.__PRERENDER_INJECTED__) {
  return getMockData(); // 预渲染用假数据
}
return await api.fetch(); // 真实环境用真数据

// 方案2:请求真实API
// 但要注意:
// - API需要支持CORS
// - 不要依赖Cookie/Token
// - 考虑API限流

// 方案3:内联数据
// 构建时注入数据到HTML
window.__INITIAL_DATA__ = { ... };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

3. 路由配置

// 必须使用history模式
const router = new VueRouter({
  mode: 'history', // ✅
  // mode: 'hash', // ❌ 不支持
  routes: [...]
});

// Nginx配置(history模式需要)
location / {
  try_files $uri $uri/ /index.html;
}
1
2
3
4
5
6
7
8
9
10
11

4. 异步组件处理

// Vue 异步组件
const AsyncComponent = () => import('./Component.vue');

// 预渲染时会等待异步组件加载
// 确保在mounted中触发prerender-ready事件
1
2
3
4
5

5. 第三方脚本

// 避免在预渲染时加载第三方脚本
if (!window.__PRERENDER_INJECTED__) {
  // 百度统计
  loadBaiduAnalytics();

  // 用户行为追踪
  loadUserTracker();
}
1
2
3
4
5
6
7
8

# 3. 常见问题和解决方案

问题1:页面内容闪烁

// 原因:预渲染的内容被重新渲染覆盖
// 解决:Hydrate而不是重新渲染

// Vue
app.$mount('#app', true); // 第二个参数true表示hydrate

// React
ReactDOM.hydrate(<App />, document.getElementById('root'));
1
2
3
4
5
6
7
8

问题2:样式丢失

// 原因:CSS-in-JS样式在预渲染时未生成
// 解决:使用服务端渲染样式

// styled-components
import { ServerStyleSheet } from 'styled-components';

const sheet = new ServerStyleSheet();
const html = renderToString(sheet.collectStyles(<App />));
const styleTags = sheet.getStyleTags();
1
2
3
4
5
6
7
8
9

问题3:构建时间过长

// 优化方案:
// 1. 并发渲染
const concurrency = 5;

// 2. 使用缓存
if (hashNotChanged) skip();

// 3. 只渲染变化的页面
const changedRoutes = getChangedRoutes();

// 4. 按需预渲染(不是所有页面都需要)
const importantRoutes = ['/', '/activity'];
1
2
3
4
5
6
7
8
9
10
11
12

问题4:动态内容无法预渲染

// 方案:骨架屏 + 客户端渲染
<template>
  <div>
    <!-- 预渲染:骨架屏 -->
    <Skeleton v-if="isPrerender" />

    <!-- 客户端:真实内容 -->
    <RealContent v-else :data="data" />
  </div>
</template>

<script>
computed: {
  isPrerender() {
    return window.__PRERENDER_INJECTED__ && !this.dataLoaded;
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 4. 与其他优化方案对比

// 方案组合使用效果最佳
const optimizations = {
  // 1. 预渲染(首屏)
  prerender: {
    effect: '首屏时间 -80%',
    cost: '构建时间 +30s'
  },

  // 2. 代码分割(按需加载)
  codeSplitting: {
    effect: '首次加载 -40%',
    cost: '需要配置路由懒加载'
  },

  // 3. 图片优化
  imageOptimization: {
    effect: '页面大小 -50%',
    cost: 'WebP兼容性处理'
  },

  // 4. CDN加速
  cdn: {
    effect: '加载速度 +100%',
    cost: 'CDN费用'
  },

  // 5. Service Worker缓存
  serviceWorker: {
    effect: '二次访问 -90%',
    cost: '需要处理更新策略'
  }
};

// 综合效果
首屏时间: 2100ms -> 250ms(↓88%)
Lighthouse Performance: 65 -> 95
用户留存率: +35%
转化率: +25%
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 5. 生产环境检查清单

// 部署前检查
const checkList = {
  '✓ 所有路由都正确渲染': true,
  '✓ SEO Meta信息正确': true,
  '✓ 社交分享卡片正常': true,
  '✓ 图片都正常显示': true,
  '✓ 样式无闪烁': true,
  '✓ 交互功能正常': true,
  '✓ 移动端适配OK': true,
  '✓ 构建产物已压缩': true,
  '✓ CDN配置正确': true,
  '✓ 404页面正常': true
};
1
2
3
4
5
6
7
8
9
10
11
12
13

# 6. 未来优化方向

1. 增量预渲染

// 只渲染变化的页面
const changedFiles = getChangedFiles(); // Git diff
const affectedRoutes = getAffectedRoutes(changedFiles);
// 只预渲染受影响的路由
1
2
3
4

2. 分布式预渲染

// 使用多台机器并行渲染
// 路由1-100 -> 机器A
// 路由101-200 -> 机器B
// 大幅缩短构建时间
1
2
3
4

3. 智能缓存

// 基于内容hash缓存
// 内容未变化的页面直接使用缓存
// 大幅减少重复渲染
1
2
3

4. 混合渲染

// 重要页面:预渲染
// 次要页面:SSR on demand
// 个人页面:CSR
// 实现最优性能和成本平衡
1
2
3
4

通过Puppeteer预渲染方案,我们成功将H5活动页面的首屏时间从2秒降低到300ms,Performance评分从65提升到92,同时显著改善了SEO效果,为业务带来了实实在在的价值提升。

Last Updated: 9/25/2026, 2:08:32 PM