渲染性能优化
组件级缓存
// server/cache/component-cache.js
import LRU from 'lru-cache'
class ComponentCache {
constructor(options = {}) {
this.cache = new LRU({
max: options.max || 1000,
ttl: options.ttl || 1000 * 60 * 15, // 15分钟
updateAgeOnGet: true
})
}
get(key) {
return this.cache.get(key)
}
set(key, value, ttl) {
return this.cache.set(key, value, ttl)
}
has(key) {
return this.cache.has(key)
}
delete(key) {
return this.cache.delete(key)
}
clear() {
return this.cache.clear()
}
// 生成缓存键
generateKey(componentName, props = {}, context = {}) {
const propsKey = JSON.stringify(props)
const contextKey = JSON.stringify({
userId: context.userId,
locale: context.locale
})
return `${componentName}:${propsKey}:${contextKey}`
}
}
export const componentCache = new ComponentCache({
max: 1000,
ttl: 1000 * 60 * 15 // 15分钟
})
可缓存组件实现
<!-- src/components/CacheableComponent.vue -->
<template>
<div class="cacheable-component">
<h3>{{ title }}</h3>
<div class="content">
<slot />
</div>
<div class="meta">
<span>缓存时间: {{ cacheTime }}</span>
<span>渲染时间: {{ renderTime }}ms</span>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
name: 'CacheableComponent',
props: {
title: String,
cacheKey: String,
cacheTTL: {
type: Number,
default: 900000 // 15分钟
}
},
// 服务端缓存配置
serverCacheKey: (props, context) => {
return `cacheable-${props.cacheKey}-${context.userId || 'anonymous'}`
},
serverCacheTTL: (props) => {
return props.cacheTTL
},
setup(props) {
const cacheTime = ref(new Date().toLocaleString())
const renderTime = ref(0)
onMounted(() => {
const startTime = performance.now()
// 模拟渲染时间
setTimeout(() => {
renderTime.value = Math.round(performance.now() - startTime)
}, 0)
})
return {
cacheTime,
renderTime
}
}
}
</script>
页面级缓存
// server/cache/page-cache.js
import { componentCache } from './component-cache.js'
import crypto from 'crypto'
class PageCache {
constructor() {
this.cache = new Map()
this.dependencies = new Map() // 依赖追踪
}
// 生成页面缓存键
generatePageKey(url, context = {}) {
const normalizedUrl = this.normalizeUrl(url)
const contextHash = this.hashContext(context)
return `page:${normalizedUrl}:${contextHash}`
}
// 标准化URL
normalizeUrl(url) {
const urlObj = new URL(url, 'http://localhost')
// 移除不影响内容的查询参数
urlObj.searchParams.delete('utm_source')
urlObj.searchParams.delete('utm_medium')
urlObj.searchParams.delete('utm_campaign')
return urlObj.pathname + urlObj.search
}
// 生成上下文哈希
hashContext(context) {
const relevantContext = {
userId: context.userId,
userRole: context.userRole,
locale: context.locale,
theme: context.theme
}
return crypto
.createHash('md5')
.update(JSON.stringify(relevantContext))
.digest('hex')
.substring(0, 8)
}
// 获取缓存
get(url, context) {
const key = this.generatePageKey(url, context)
const cached = this.cache.get(key)
if (cached && cached.expires > Date.now()) {
return cached.html
}
// 清理过期缓存
if (cached) {
this.cache.delete(key)
}
return null
}
// 设置缓存
set(url, context, html, ttl = 300000) { // 默认5分钟
const key = this.generatePageKey(url, context)
this.cache.set(key, {
html,
expires: Date.now() + ttl,
createdAt: Date.now()
})
// 设置依赖
this.setDependencies(key, context)
}
// 设置缓存依赖
setDependencies(cacheKey, context) {
const deps = [
`user:${context.userId}`,
`locale:${context.locale}`
]
deps.forEach(dep => {
if (!this.dependencies.has(dep)) {
this.dependencies.set(dep, new Set())
}
this.dependencies.get(dep).add(cacheKey)
})
}
// 根据依赖清理缓存
invalidateByDependency(dependency) {
const cacheKeys = this.dependencies.get(dependency)
if (cacheKeys) {
cacheKeys.forEach(key => {
this.cache.delete(key)
})
this.dependencies.delete(dependency)
}
}
// 清理所有缓存
clear() {
this.cache.clear()
this.dependencies.clear()
}
// 获取缓存统计
getStats() {
return {
size: this.cache.size,
dependencies: this.dependencies.size,
memory: this.getMemoryUsage()
}
}
getMemoryUsage() {
let totalSize = 0
this.cache.forEach(value => {
totalSize += Buffer.byteLength(value.html, 'utf8')
})
return totalSize
}
}
export const pageCache = new PageCache()
数据缓存策略
Redis缓存实现
// server/cache/redis-cache.js
import Redis from 'ioredis'
class RedisCache {
constructor(options = {}) {
this.redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD,
db: process.env.REDIS_DB || 0,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
...options
})
this.redis.on('error', (err) => {
console.error('Redis连接错误:', err)
})
this.redis.on('connect', () => {
console.log('Redis连接成功')
})
}
// 获取缓存
async get(key) {
try {
const value = await this.redis.get(key)
return value ? JSON.parse(value) : null
} catch (error) {
console.error('Redis获取缓存失败:', error)
return null
}
}
// 设置缓存
async set(key, value, ttl = 3600) {
try {
const serialized = JSON.stringify(value)
if (ttl > 0) {
await this.redis.setex(key, ttl, serialized)
} else {
await this.redis.set(key, serialized)
}
return true
} catch (error) {
console.error('Redis设置缓存失败:', error)
return false
}
}
// 删除缓存
async del(key) {
try {
await this.redis.del(key)
return true
} catch (error) {
console.error('Redis删除缓存失败:', error)
return false
}
}
// 批量删除
async delPattern(pattern) {
try {
const keys = await this.redis.keys(pattern)
if (keys.length > 0) {
await this.redis.del(...keys)
}
return keys.length
} catch (error) {
console.error('Redis批量删除失败:', error)
return 0
}
}
// 检查键是否存在
async exists(key) {
try {
return await this.redis.exists(key) === 1
} catch (error) {
console.error('Redis检查键存在失败:', error)
return false
}
}
// 设置过期时间
async expire(key, ttl) {
try {
await this.redis.expire(key, ttl)
return true
} catch (error) {
console.error('Redis设置过期时间失败:', error)
return false
}
}
// 获取剩余过期时间
async ttl(key) {
try {
return await this.redis.ttl(key)
} catch (error) {
console.error('Redis获取TTL失败:', error)
return -1
}
}
// 关闭连接
async close() {
await this.redis.quit()
}
}
export const redisCache = new RedisCache()
多级缓存策略
// server/cache/multi-level-cache.js
import { componentCache } from './component-cache.js'
import { redisCache } from './redis-cache.js'
class MultiLevelCache {
constructor() {
this.l1Cache = componentCache // 内存缓存(L1)
this.l2Cache = redisCache // Redis缓存(L2)
}
// 获取缓存(先查L1,再查L2)
async get(key) {
// 先查内存缓存
let value = this.l1Cache.get(key)
if (value !== undefined) {
return value
}
// 再查Redis缓存
value = await this.l2Cache.get(key)
if (value !== null) {
// 回填到内存缓存
this.l1Cache.set(key, value, 300000) // 5分钟
return value
}
return null
}
// 设置缓存(同时设置L1和L2)
async set(key, value, ttl = 3600) {
// 设置内存缓存
this.l1Cache.set(key, value, Math.min(ttl * 1000, 300000))
// 设置Redis缓存
await this.l2Cache.set(key, value, ttl)
}
// 删除缓存(同时删除L1和L2)
async del(key) {
this.l1Cache.delete(key)
await this.l2Cache.del(key)
}
// 清空所有缓存
async clear() {
this.l1Cache.clear()
await this.l2Cache.delPattern('*')
}
// 预热缓存
async warmup(keys) {
const promises = keys.map(async (key) => {
const value = await this.l2Cache.get(key)
if (value !== null) {
this.l1Cache.set(key, value, 300000)
}
})
await Promise.all(promises)
}
}
export const multiLevelCache = new MultiLevelCache()
代码分割优化
路由级代码分割
// src/router/index.js
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
// 首页同步加载,提高首屏速度
import Home from '@/pages/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import(
/* webpackChunkName: "about" */
/* webpackPreload: true */
'@/pages/About.vue'
)
},
{
path: '/blog',
name: 'Blog',
component: () => import(
/* webpackChunkName: "blog" */
'@/pages/Blog.vue'
),
children: [
{
path: ':id',
name: 'BlogPost',
component: () => import(
/* webpackChunkName: "blog-post" */
'@/pages/BlogPost.vue'
)
}
]
},
{
path: '/admin',
name: 'Admin',
component: () => import(
/* webpackChunkName: "admin" */
'@/pages/Admin.vue'
),
meta: { requiresAuth: true }
}
]
export function createRouter() {
return createRouter({
history: import.meta.env.SSR
? createMemoryHistory()
: createWebHistory(),
routes
})
}
组件级代码分割
<!-- src/pages/Dashboard.vue -->
<template>
<div class="dashboard">
<h1>仪表板</h1>
<!-- 关键组件同步加载 -->
<UserInfo :user="user" />
<!-- 非关键组件异步加载 -->
<Suspense>
<template #default>
<AsyncChart :data="chartData" />
</template>
<template #fallback>
<ChartSkeleton />
</template>
</Suspense>
<!-- 条件加载的组件 -->
<Suspense v-if="showAdvanced">
<template #default>
<AdvancedAnalytics />
</template>
<template #fallback>
<div>加载高级分析...</div>
</template>
</Suspense>
</div>
</template>
<script>
import { ref, defineAsyncComponent } from 'vue'
import UserInfo from '@/components/UserInfo.vue'
import ChartSkeleton from '@/components/ChartSkeleton.vue'
export default {
name: 'Dashboard',
components: {
UserInfo,
ChartSkeleton,
// 异步组件
AsyncChart: defineAsyncComponent({
loader: () => import('@/components/Chart.vue'),
delay: 200,
timeout: 3000,
errorComponent: () => import('@/components/ChartError.vue'),
loadingComponent: ChartSkeleton
}),
AdvancedAnalytics: defineAsyncComponent(() =>
import(
/* webpackChunkName: "advanced-analytics" */
'@/components/AdvancedAnalytics.vue'
)
)
},
setup() {
const user = ref({})
const chartData = ref([])
const showAdvanced = ref(false)
return {
user,
chartData,
showAdvanced
}
}
}
</script>
资源优化
图片懒加载组件
<!-- src/components/LazyImage.vue -->
<template>
<div class="lazy-image" :class="{ loaded: isLoaded }">
<img
v-if="shouldLoad"
:src="src"
:alt="alt"
@load="onLoad"
@error="onError"
:loading="loading"
/>
<div v-else class="placeholder">
<slot name="placeholder">
<div class="skeleton"></div>
</slot>
</div>
<div v-if="error" class="error">
<slot name="error">
<span>图片加载失败</span>
</slot>
</div>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue'
export default {
name: 'LazyImage',
props: {
src: {
type: String,
required: true
},
alt: {
type: String,
default: ''
},
loading: {
type: String,
default: 'lazy'
},
threshold: {
type: Number,
default: 0.1
}
},
setup(props, { emit }) {
const shouldLoad = ref(false)
const isLoaded = ref(false)
const error = ref(false)
const imageRef = ref(null)
let observer = null
const onLoad = () => {
isLoaded.value = true
emit('load')
}
const onError = () => {
error.value = true
emit('error')
}
onMounted(() => {
if ('IntersectionObserver' in window) {
observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
shouldLoad.value = true
observer.unobserve(entry.target)
}
})
},
{
threshold: props.threshold
}
)
if (imageRef.value) {
observer.observe(imageRef.value)
}
} else {
// 不支持IntersectionObserver时直接加载
shouldLoad.value = true
}
})
onUnmounted(() => {
if (observer) {
observer.disconnect()
}
})
return {
shouldLoad,
isLoaded,
error,
imageRef,
onLoad,
onError
}
}
}
</script>
<style scoped>
.lazy-image {
position: relative;
overflow: hidden;
}
.placeholder {
width: 100%;
height: 200px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
}
.skeleton {
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #ff6b6b;
}
img {
width: 100%;
height: auto;
transition: opacity 0.3s ease;
}
.loaded img {
opacity: 1;
}
</style>
资源预加载策略
// src/utils/preloader.js
class ResourcePreloader {
constructor() {
this.preloadedResources = new Set()
this.preloadQueue = []
this.isPreloading = false
}
// 预加载图片
preloadImage(src) {
return new Promise((resolve, reject) => {
if (this.preloadedResources.has(src)) {
resolve(src)
return
}
const img = new Image()
img.onload = () => {
this.preloadedResources.add(src)
resolve(src)
}
img.onerror = reject
img.src = src
})
}
// 预加载多个图片
async preloadImages(srcs) {
const promises = srcs.map(src => this.preloadImage(src))
return Promise.allSettled(promises)
}
// 预加载JavaScript模块
preloadModule(moduleFactory) {
return new Promise((resolve, reject) => {
if (typeof moduleFactory === 'function') {
moduleFactory()
.then(module => {
resolve(module)
})
.catch(reject)
} else {
reject(new Error('Invalid module factory'))
}
})
}
// 智能预加载(基于用户行为)
smartPreload(resources, priority = 'low') {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
this.batchPreload(resources)
})
} else {
setTimeout(() => {
this.batchPreload(resources)
}, priority === 'high' ? 0 : 1000)
}
}
// 批量预加载
async batchPreload(resources) {
if (this.isPreloading) return
this.isPreloading = true
try {
for (const resource of resources) {
if (resource.type === 'image') {
await this.preloadImage(resource.src)
} else if (resource.type === 'module') {
await this.preloadModule(resource.factory)
}
// 避免阻塞主线程
await new Promise(resolve => setTimeout(resolve, 10))
}
} finally {
this.isPreloading = false
}
}
// 清理预加载缓存
clearCache() {
this.preloadedResources.clear()
}
}
export const preloader = new ResourcePreloader()
// 使用示例
export function usePreloader() {
const preloadRouteResources = (routeName) => {
const routeResources = {
'Blog': [
{ type: 'image', src: '/images/blog-hero.jpg' },
{ type: 'module', factory: () => import('@/components/BlogList.vue') }
],
'Admin': [
{ type: 'module', factory: () => import('@/components/AdminDashboard.vue') },
{ type: 'module', factory: () => import('@/components/UserManagement.vue') }
]
}
const resources = routeResources[routeName]
if (resources) {
preloader.smartPreload(resources)
}
}
return {
preloadRouteResources,
preloader
}
}
监控和分析
性能监控
// src/utils/performance-monitor.js
class PerformanceMonitor {
constructor() {
this.metrics = new Map()
this.observers = []
this.init()
}
init() {
// 监控页面加载性能
if ('PerformanceObserver' in window) {
this.observeNavigation()
this.observePaint()
this.observeLCP()
this.observeFID()
this.observeCLS()
}
}
// 监控导航时间
observeNavigation() {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
this.metrics.set('navigation', {
domContentLoaded: entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart,
loadComplete: entry.loadEventEnd - entry.loadEventStart,
firstByte: entry.responseStart - entry.requestStart,
domInteractive: entry.domInteractive - entry.navigationStart
})
})
})
observer.observe({ entryTypes: ['navigation'] })
this.observers.push(observer)
}
// 监控绘制时间
observePaint() {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
this.metrics.set(entry.name, entry.startTime)
})
})
observer.observe({ entryTypes: ['paint'] })
this.observers.push(observer)
}
// 监控最大内容绘制(LCP)
observeLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
this.metrics.set('lcp', lastEntry.startTime)
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
this.observers.push(observer)
}
// 监控首次输入延迟(FID)
observeFID() {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
this.metrics.set('fid', entry.processingStart - entry.startTime)
})
})
observer.observe({ entryTypes: ['first-input'] })
this.observers.push(observer)
}
// 监控累积布局偏移(CLS)
observeCLS() {
let clsValue = 0
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) {
clsValue += entry.value
this.metrics.set('cls', clsValue)
}
})
})
observer.observe({ entryTypes: ['layout-shift'] })
this.observers.push(observer)
}
// 获取所有指标
getMetrics() {
return Object.fromEntries(this.metrics)
}
// 发送指标到分析服务
async sendMetrics() {
const metrics = this.getMetrics()
try {
await fetch('/api/analytics/performance', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
metrics
})
})
} catch (error) {
console.error('发送性能指标失败:', error)
}
}
// 清理观察器
disconnect() {
this.observers.forEach(observer => observer.disconnect())
this.observers = []
}
}
export const performanceMonitor = new PerformanceMonitor()
// 页面卸载时发送指标
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => {
performanceMonitor.sendMetrics()
})
}
下一步
在下一章节中,我们将学习如何部署Vue SSR应用到生产环境,包括Docker容器化、CI/CD流程等。