6.1 缓存策略
6.1.1 组件缓存
// server.js - 组件缓存配置
const LRU = require('lru-cache')
const { createBundleRenderer } = require('vue-server-renderer')
// 创建组件缓存
const microCache = new LRU({
max: 100,
maxAge: 1000 * 60 * 15 // 15分钟
})
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template,
clientManifest,
// 组件缓存
cache: microCache
})
// 页面级缓存
const pageCache = new LRU({
max: 1000,
maxAge: 1000 * 60 * 15
})
app.get('*', (req, res) => {
const hit = pageCache.get(req.url)
if (hit) {
return res.end(hit)
}
renderer.renderToString(context, (err, html) => {
if (err) {
return handleError(err)
}
pageCache.set(req.url, html)
res.end(html)
})
})
6.1.2 智能缓存策略
// utils/cache.js
class SmartCache {
constructor() {
this.componentCache = new LRU({ max: 100, maxAge: 1000 * 60 * 15 })
this.pageCache = new LRU({ max: 1000, maxAge: 1000 * 60 * 5 })
this.dataCache = new LRU({ max: 500, maxAge: 1000 * 60 * 10 })
}
// 组件缓存键生成
getComponentCacheKey(componentName, props) {
return `${componentName}:${JSON.stringify(props)}`
}
// 页面缓存键生成
getPageCacheKey(url, userAgent, locale) {
return `${url}:${userAgent}:${locale}`
}
// 数据缓存
cacheData(key, data, ttl = 600000) {
this.dataCache.set(key, data, ttl)
}
// 缓存失效
invalidateCache(pattern) {
const keys = this.pageCache.keys()
keys.forEach(key => {
if (key.includes(pattern)) {
this.pageCache.del(key)
}
})
}
}
module.exports = new SmartCache()
6.1.3 CDN缓存配置
// middleware/cache-headers.js
function setCacheHeaders(req, res, next) {
// 静态资源缓存
if (req.url.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)) {
res.setHeader('Cache-Control', 'public, max-age=31536000') // 1年
res.setHeader('Expires', new Date(Date.now() + 31536000000).toUTCString())
}
// HTML页面缓存
else if (req.url.match(/\.(html)$/) || req.url === '/') {
res.setHeader('Cache-Control', 'public, max-age=300') // 5分钟
}
// API缓存
else if (req.url.startsWith('/api/')) {
res.setHeader('Cache-Control', 'public, max-age=60') // 1分钟
}
next()
}
module.exports = setCacheHeaders
6.2 代码分割与懒加载
6.2.1 路由级代码分割
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
},
{
path: '/products',
name: 'Products',
component: () => import('../views/Products.vue'),
children: [
{
path: ':id',
name: 'ProductDetail',
component: () => import('../views/ProductDetail.vue')
}
]
},
{
path: '/admin',
name: 'Admin',
component: () => import('../views/admin/Layout.vue'),
children: [
{
path: 'dashboard',
component: () => import('../views/admin/Dashboard.vue')
},
{
path: 'users',
component: () => import('../views/admin/Users.vue')
}
]
}
]
export function createAppRouter() {
return createRouter({
history: createWebHistory(),
routes
})
}
6.2.2 组件级懒加载
<!-- components/LazyComponent.vue -->
<template>
<div>
<Suspense>
<template #default>
<AsyncComponent v-if="shouldLoad" />
</template>
<template #fallback>
<div class="loading">加载中...</div>
</template>
</Suspense>
</div>
</template>
<script>
import { ref, onMounted, defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: () => import('./LoadingSpinner.vue'),
errorComponent: () => import('./ErrorComponent.vue'),
delay: 200,
timeout: 3000
})
export default {
name: 'LazyComponent',
components: {
AsyncComponent
},
setup() {
const shouldLoad = ref(false)
onMounted(() => {
// 延迟加载或基于用户交互加载
setTimeout(() => {
shouldLoad.value = true
}, 1000)
})
return {
shouldLoad
}
}
}
</script>
6.2.3 智能预加载
// utils/preloader.js
class IntelligentPreloader {
constructor() {
this.preloadedRoutes = new Set()
this.observer = null
this.init()
}
init() {
// 创建Intersection Observer
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const route = entry.target.dataset.preload
if (route && !this.preloadedRoutes.has(route)) {
this.preloadRoute(route)
}
}
})
},
{ threshold: 0.1 }
)
}
// 预加载路由
async preloadRoute(routeName) {
try {
this.preloadedRoutes.add(routeName)
// 动态导入路由组件
const routeMap = {
'products': () => import('../views/Products.vue'),
'about': () => import('../views/About.vue'),
'contact': () => import('../views/Contact.vue')
}
if (routeMap[routeName]) {
await routeMap[routeName]()
console.log(`预加载完成: ${routeName}`)
}
} catch (error) {
console.error(`预加载失败: ${routeName}`, error)
}
}
// 观察链接元素
observeLinks() {
const links = document.querySelectorAll('[data-preload]')
links.forEach(link => this.observer.observe(link))
}
// 基于用户行为预测
predictivePreload() {
// 鼠标悬停预加载
document.addEventListener('mouseover', (e) => {
const link = e.target.closest('a[data-preload]')
if (link) {
const route = link.dataset.preload
setTimeout(() => this.preloadRoute(route), 100)
}
})
}
}
export default new IntelligentPreloader()
6.3 资源优化
6.3.1 图片优化
// utils/image-optimizer.js
class ImageOptimizer {
constructor() {
this.supportedFormats = this.detectSupportedFormats()
}
// 检测浏览器支持的图片格式
detectSupportedFormats() {
const canvas = document.createElement('canvas')
canvas.width = 1
canvas.height = 1
return {
webp: canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0,
avif: canvas.toDataURL('image/avif').indexOf('data:image/avif') === 0
}
}
// 获取最优图片URL
getOptimizedImageUrl(baseUrl, options = {}) {
const {
width = 800,
height = 600,
quality = 80,
format = 'auto'
} = options
let optimalFormat = format
if (format === 'auto') {
if (this.supportedFormats.avif) {
optimalFormat = 'avif'
} else if (this.supportedFormats.webp) {
optimalFormat = 'webp'
} else {
optimalFormat = 'jpg'
}
}
return `${baseUrl}?w=${width}&h=${height}&q=${quality}&f=${optimalFormat}`
}
// 响应式图片
generateResponsiveImageSet(baseUrl, sizes = [400, 800, 1200, 1600]) {
return sizes.map(size => ({
url: this.getOptimizedImageUrl(baseUrl, { width: size }),
width: size
}))
}
}
export default new ImageOptimizer()
6.3.2 字体优化
/* styles/fonts.css */
/* 字体预加载 */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-font.woff2') format('woff2'),
url('/fonts/custom-font.woff') format('woff');
font-display: swap; /* 字体交换策略 */
font-weight: 400;
font-style: normal;
}
/* 字体子集化 */
@font-face {
font-family: 'CustomFont-Chinese';
src: url('/fonts/custom-font-chinese.woff2') format('woff2');
font-display: swap;
unicode-range: U+4E00-9FFF; /* 中文字符范围 */
}
/* 字体加载优化 */
.font-loading {
font-family: system-ui, -apple-system, sans-serif;
}
.font-loaded {
font-family: 'CustomFont', system-ui, -apple-system, sans-serif;
}
// utils/font-loader.js
class FontLoader {
constructor() {
this.loadedFonts = new Set()
}
// 预加载字体
async preloadFont(fontFamily, fontUrl) {
if (this.loadedFonts.has(fontFamily)) {
return Promise.resolve()
}
return new Promise((resolve, reject) => {
const font = new FontFace(fontFamily, `url(${fontUrl})`)
font.load().then(() => {
document.fonts.add(font)
this.loadedFonts.add(fontFamily)
document.body.classList.add('font-loaded')
resolve()
}).catch(reject)
})
}
// 字体加载状态检测
checkFontLoadStatus() {
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => {
console.log('所有字体加载完成')
document.body.classList.add('fonts-ready')
})
}
}
}
export default new FontLoader()
6.3.3 资源压缩与合并
// build/webpack.prod.js
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [
// JS压缩
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log']
},
mangle: {
safari10: true
}
},
parallel: true
}),
// CSS压缩
new CssMinimizerPlugin({
minimizerOptions: {
preset: [
'default',
{
discardComments: { removeAll: true },
normalizeWhitespace: true
}
]
}
})
],
// 代码分割
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5
}
}
}
},
plugins: [
// Gzip压缩
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8
}),
// Brotli压缩
new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
compressionOptions: {
level: 11
},
threshold: 8192,
minRatio: 0.8
})
]
}
6.4 服务端优化
6.4.1 渲染性能优化
// server/renderer.js
const { createBundleRenderer } = require('vue-server-renderer')
const cluster = require('cluster')
const numCPUs = require('os').cpus().length
class OptimizedRenderer {
constructor() {
this.renderers = new Map()
this.renderQueue = []
this.isProcessing = false
}
// 创建渲染器池
createRendererPool(serverBundle, options) {
const poolSize = Math.min(numCPUs, 4)
for (let i = 0; i < poolSize; i++) {
const renderer = createBundleRenderer(serverBundle, {
...options,
runInNewContext: false, // 性能优化
shouldPrefetch: (file, type) => {
// 智能预取策略
if (type === 'script') {
return !file.includes('chunk')
}
return true
}
})
this.renderers.set(i, {
renderer,
busy: false,
lastUsed: Date.now()
})
}
}
// 获取可用渲染器
getAvailableRenderer() {
for (const [id, rendererInfo] of this.renderers) {
if (!rendererInfo.busy) {
rendererInfo.busy = true
rendererInfo.lastUsed = Date.now()
return { id, renderer: rendererInfo.renderer }
}
}
return null
}
// 释放渲染器
releaseRenderer(id) {
const rendererInfo = this.renderers.get(id)
if (rendererInfo) {
rendererInfo.busy = false
}
}
// 渲染页面
async renderPage(context) {
return new Promise((resolve, reject) => {
const availableRenderer = this.getAvailableRenderer()
if (!availableRenderer) {
// 加入队列等待
this.renderQueue.push({ context, resolve, reject })
this.processQueue()
return
}
const { id, renderer } = availableRenderer
renderer.renderToString(context, (err, html) => {
this.releaseRenderer(id)
if (err) {
reject(err)
} else {
resolve(html)
}
// 处理队列中的下一个请求
this.processQueue()
})
})
}
// 处理渲染队列
processQueue() {
if (this.isProcessing || this.renderQueue.length === 0) {
return
}
this.isProcessing = true
const availableRenderer = this.getAvailableRenderer()
if (availableRenderer) {
const { context, resolve, reject } = this.renderQueue.shift()
const { id, renderer } = availableRenderer
renderer.renderToString(context, (err, html) => {
this.releaseRenderer(id)
if (err) {
reject(err)
} else {
resolve(html)
}
this.isProcessing = false
this.processQueue()
})
} else {
this.isProcessing = false
}
}
}
module.exports = OptimizedRenderer
6.4.2 内存管理
// server/memory-manager.js
class MemoryManager {
constructor() {
this.memoryThreshold = 0.8 // 80%内存使用率阈值
this.gcInterval = 30000 // 30秒GC间隔
this.startMonitoring()
}
// 开始内存监控
startMonitoring() {
setInterval(() => {
this.checkMemoryUsage()
}, this.gcInterval)
}
// 检查内存使用情况
checkMemoryUsage() {
const memUsage = process.memoryUsage()
const totalMemory = require('os').totalmem()
const usedMemory = memUsage.heapUsed
const memoryUsageRatio = usedMemory / totalMemory
console.log('内存使用情况:', {
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024) + 'MB',
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024) + 'MB',
external: Math.round(memUsage.external / 1024 / 1024) + 'MB',
rss: Math.round(memUsage.rss / 1024 / 1024) + 'MB',
usageRatio: Math.round(memoryUsageRatio * 100) + '%'
})
// 内存使用率过高时触发GC
if (memoryUsageRatio > this.memoryThreshold) {
console.log('内存使用率过高,触发垃圾回收')
this.forceGC()
}
}
// 强制垃圾回收
forceGC() {
if (global.gc) {
global.gc()
console.log('垃圾回收完成')
} else {
console.warn('垃圾回收不可用,请使用 --expose-gc 启动参数')
}
}
// 内存泄漏检测
detectMemoryLeaks() {
const heapSnapshot = require('v8').writeHeapSnapshot()
console.log('堆快照已保存:', heapSnapshot)
}
}
module.exports = new MemoryManager()
6.4.3 集群部署
// server/cluster.js
const cluster = require('cluster')
const numCPUs = require('os').cpus().length
const path = require('path')
if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`)
// 创建工作进程
const workers = []
for (let i = 0; i < numCPUs; i++) {
const worker = cluster.fork()
workers.push(worker)
worker.on('message', (msg) => {
if (msg.type === 'memory-warning') {
console.log(`工作进程 ${worker.process.pid} 内存警告:`, msg.data)
}
})
}
// 工作进程重启策略
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`)
if (code !== 0 && !worker.exitedAfterDisconnect) {
console.log('启动新的工作进程...')
const newWorker = cluster.fork()
workers.push(newWorker)
}
})
// 优雅关闭
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号,开始优雅关闭...')
workers.forEach(worker => {
worker.disconnect()
setTimeout(() => {
if (!worker.isDead()) {
worker.kill('SIGKILL')
}
}, 10000) // 10秒超时
})
})
} else {
// 工作进程
const app = require('./app')
const port = process.env.PORT || 3000
const server = app.listen(port, () => {
console.log(`工作进程 ${process.pid} 在端口 ${port} 上运行`)
})
// 优雅关闭工作进程
process.on('SIGTERM', () => {
console.log(`工作进程 ${process.pid} 收到SIGTERM信号`)
server.close(() => {
console.log(`工作进程 ${process.pid} 已关闭`)
process.exit(0)
})
})
// 内存监控
setInterval(() => {
const memUsage = process.memoryUsage()
const memoryUsageRatio = memUsage.heapUsed / memUsage.heapTotal
if (memoryUsageRatio > 0.9) {
process.send({
type: 'memory-warning',
data: {
pid: process.pid,
memUsage,
ratio: memoryUsageRatio
}
})
}
}, 30000)
}
6.5 监控与分析
6.5.1 性能监控
// utils/performance-monitor.js
class PerformanceMonitor {
constructor() {
this.metrics = {
renderTime: [],
memoryUsage: [],
requestCount: 0,
errorCount: 0
}
this.startTime = Date.now()
}
// 记录渲染时间
recordRenderTime(startTime, endTime) {
const renderTime = endTime - startTime
this.metrics.renderTime.push(renderTime)
// 保持最近1000条记录
if (this.metrics.renderTime.length > 1000) {
this.metrics.renderTime.shift()
}
}
// 记录内存使用
recordMemoryUsage() {
const memUsage = process.memoryUsage()
this.metrics.memoryUsage.push({
timestamp: Date.now(),
heapUsed: memUsage.heapUsed,
heapTotal: memUsage.heapTotal,
external: memUsage.external,
rss: memUsage.rss
})
// 保持最近100条记录
if (this.metrics.memoryUsage.length > 100) {
this.metrics.memoryUsage.shift()
}
}
// 增加请求计数
incrementRequestCount() {
this.metrics.requestCount++
}
// 增加错误计数
incrementErrorCount() {
this.metrics.errorCount++
}
// 获取性能统计
getStats() {
const renderTimes = this.metrics.renderTime
const avgRenderTime = renderTimes.length > 0
? renderTimes.reduce((a, b) => a + b, 0) / renderTimes.length
: 0
const maxRenderTime = renderTimes.length > 0
? Math.max(...renderTimes)
: 0
const uptime = Date.now() - this.startTime
const requestsPerSecond = this.metrics.requestCount / (uptime / 1000)
return {
uptime,
requestCount: this.metrics.requestCount,
errorCount: this.metrics.errorCount,
errorRate: this.metrics.requestCount > 0
? this.metrics.errorCount / this.metrics.requestCount
: 0,
avgRenderTime,
maxRenderTime,
requestsPerSecond,
memoryUsage: this.metrics.memoryUsage.slice(-1)[0] || null
}
}
// 导出性能报告
exportReport() {
const stats = this.getStats()
const report = {
timestamp: new Date().toISOString(),
stats,
recommendations: this.generateRecommendations(stats)
}
return report
}
// 生成优化建议
generateRecommendations(stats) {
const recommendations = []
if (stats.avgRenderTime > 100) {
recommendations.push('平均渲染时间过长,建议启用组件缓存')
}
if (stats.errorRate > 0.01) {
recommendations.push('错误率过高,请检查错误日志')
}
if (stats.memoryUsage && stats.memoryUsage.heapUsed > 500 * 1024 * 1024) {
recommendations.push('内存使用过高,建议检查内存泄漏')
}
return recommendations
}
}
module.exports = new PerformanceMonitor()
6.5.2 错误追踪
// utils/error-tracker.js
class ErrorTracker {
constructor() {
this.errors = []
this.errorCounts = new Map()
}
// 记录错误
trackError(error, context = {}) {
const errorInfo = {
timestamp: new Date().toISOString(),
message: error.message,
stack: error.stack,
context,
userAgent: context.userAgent || '',
url: context.url || '',
userId: context.userId || null
}
this.errors.push(errorInfo)
// 错误计数
const errorKey = `${error.message}:${context.url}`
const count = this.errorCounts.get(errorKey) || 0
this.errorCounts.set(errorKey, count + 1)
// 保持最近1000条错误记录
if (this.errors.length > 1000) {
this.errors.shift()
}
// 发送到外部监控服务
this.sendToMonitoringService(errorInfo)
}
// 发送到监控服务
async sendToMonitoringService(errorInfo) {
try {
// 这里可以集成Sentry、Bugsnag等服务
if (process.env.SENTRY_DSN) {
// await Sentry.captureException(errorInfo)
}
// 或者发送到自定义API
if (process.env.ERROR_TRACKING_API) {
await fetch(process.env.ERROR_TRACKING_API, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(errorInfo)
})
}
} catch (err) {
console.error('发送错误信息失败:', err)
}
}
// 获取错误统计
getErrorStats() {
const now = Date.now()
const oneHourAgo = now - 60 * 60 * 1000
const oneDayAgo = now - 24 * 60 * 60 * 1000
const recentErrors = this.errors.filter(error =>
new Date(error.timestamp).getTime() > oneHourAgo
)
const dailyErrors = this.errors.filter(error =>
new Date(error.timestamp).getTime() > oneDayAgo
)
return {
totalErrors: this.errors.length,
recentErrors: recentErrors.length,
dailyErrors: dailyErrors.length,
topErrors: Array.from(this.errorCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([error, count]) => ({ error, count }))
}
}
}
module.exports = new ErrorTracker()
本章小结
在本章中,我们深入学习了Vue SSR的性能优化技巧:
核心要点
缓存策略
- 组件级缓存提升渲染性能
- 页面级缓存减少重复渲染
- CDN缓存优化资源加载
代码分割与懒加载
- 路由级代码分割减少初始包大小
- 组件级懒加载按需加载资源
- 智能预加载提升用户体验
资源优化
- 图片格式优化和响应式加载
- 字体加载优化和子集化
- 资源压缩和合并策略
服务端优化
- 渲染器池提升并发处理能力
- 内存管理防止内存泄漏
- 集群部署提升系统可用性
监控与分析
- 性能指标监控和分析
- 错误追踪和报告
- 优化建议生成
最佳实践
- 合理使用缓存,避免过度缓存
- 按需加载,避免一次性加载过多资源
- 监控关键性能指标,及时发现问题
- 定期进行性能测试和优化
下一章预告
下一章我们将学习测试与调试,包括单元测试、集成测试、端到端测试以及调试技巧,确保Vue SSR应用的质量和稳定性。
练习作业
- 实现一个智能缓存系统,支持多级缓存和自动失效
- 优化一个现有的Vue SSR应用,提升首屏加载速度
- 搭建性能监控系统,实时监控应用性能指标
- 实现资源预加载策略,提升用户体验
- 配置集群部署,提升应用的并发处理能力