第56天:Element Plus SSR 性能优化与缓存策略
学习目标
- 掌握 SSR 性能优化的核心技术和策略
- 学会实现多层次的缓存机制
- 了解 Element Plus 在 SSR 环境下的性能优化方法
- 掌握性能监控和分析技术
知识点概览
1. SSR 性能优化基础
1.1 性能指标体系
typescript
// types/performance.ts
export interface SSRPerformanceMetrics {
// 服务端指标
server: {
renderTime: number // 渲染时间 (ms)
memoryUsage: number // 内存使用 (MB)
cpuUsage: number // CPU 使用率 (%)
concurrentRequests: number // 并发请求数
cacheHitRate: number // 缓存命中率 (%)
}
// 客户端指标
client: {
ttfb: number // Time to First Byte (ms)
fcp: number // First Contentful Paint (ms)
lcp: number // Largest Contentful Paint (ms)
fid: number // First Input Delay (ms)
cls: number // Cumulative Layout Shift
hydrationTime: number // 水合时间 (ms)
}
// 网络指标
network: {
htmlSize: number // HTML 大小 (KB)
jsSize: number // JavaScript 大小 (KB)
cssSize: number // CSS 大小 (KB)
totalSize: number // 总大小 (KB)
requestCount: number // 请求数量
}
}
// 性能预算配置
export interface PerformanceBudget {
server: {
maxRenderTime: 200 // 最大渲染时间
maxMemoryUsage: 512 // 最大内存使用
maxCpuUsage: 80 // 最大 CPU 使用率
}
client: {
maxTTFB: 600 // 最大 TTFB
maxFCP: 1500 // 最大 FCP
maxLCP: 2500 // 最大 LCP
maxFID: 100 // 最大 FID
maxCLS: 0.1 // 最大 CLS
}
network: {
maxHtmlSize: 50 // 最大 HTML 大小
maxJsSize: 200 // 最大 JS 大小
maxCssSize: 50 // 最大 CSS 大小
maxRequestCount: 10 // 最大请求数
}
}
1.2 性能监控系统
typescript
// utils/performance-monitor.ts
export class SSRPerformanceMonitor {
private metrics: SSRPerformanceMetrics[] = []
private budget: PerformanceBudget
private observers: Map<string, PerformanceObserver> = new Map()
constructor(budget: PerformanceBudget) {
this.budget = budget
this.setupObservers()
}
// 设置性能观察器
private setupObservers() {
if (typeof window === 'undefined') return
// 观察导航时间
const navObserver = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
if (entry.entryType === 'navigation') {
this.recordNavigationMetrics(entry as PerformanceNavigationTiming)
}
})
})
navObserver.observe({ entryTypes: ['navigation'] })
this.observers.set('navigation', navObserver)
// 观察绘制时间
const paintObserver = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
this.recordPaintMetrics(entry as PerformancePaintTiming)
})
})
paintObserver.observe({ entryTypes: ['paint'] })
this.observers.set('paint', paintObserver)
// 观察布局偏移
const clsObserver = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
this.recordLayoutShift(entry as PerformanceEntry)
})
})
clsObserver.observe({ entryTypes: ['layout-shift'] })
this.observers.set('layout-shift', clsObserver)
// 观察长任务
const longTaskObserver = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
this.recordLongTask(entry as PerformanceEntry)
})
})
longTaskObserver.observe({ entryTypes: ['longtask'] })
this.observers.set('longtask', longTaskObserver)
}
// 记录导航指标
private recordNavigationMetrics(entry: PerformanceNavigationTiming) {
const metrics: Partial<SSRPerformanceMetrics> = {
client: {
ttfb: entry.responseStart - entry.requestStart,
fcp: 0, // 将在 paint 观察器中设置
lcp: 0, // 将在 LCP 观察器中设置
fid: 0, // 将在 FID 观察器中设置
cls: 0, // 将在 CLS 观察器中设置
hydrationTime: 0 // 将在水合完成时设置
},
network: {
htmlSize: entry.transferSize / 1024,
jsSize: 0,
cssSize: 0,
totalSize: entry.transferSize / 1024,
requestCount: 1
}
}
this.updateMetrics(metrics)
}
// 记录绘制指标
private recordPaintMetrics(entry: PerformancePaintTiming) {
const metrics: Partial<SSRPerformanceMetrics> = {
client: {
fcp: entry.name === 'first-contentful-paint' ? entry.startTime : 0
}
}
this.updateMetrics(metrics)
}
// 记录布局偏移
private recordLayoutShift(entry: any) {
if (!entry.hadRecentInput) {
const metrics: Partial<SSRPerformanceMetrics> = {
client: {
cls: (this.getCurrentMetrics()?.client?.cls || 0) + entry.value
}
}
this.updateMetrics(metrics)
}
}
// 记录长任务
private recordLongTask(entry: PerformanceEntry) {
console.warn(`Long task detected: ${entry.duration}ms`, entry)
}
// 更新指标
private updateMetrics(newMetrics: Partial<SSRPerformanceMetrics>) {
const current = this.getCurrentMetrics() || this.createEmptyMetrics()
const updated = this.mergeMetrics(current, newMetrics)
this.metrics.push(updated)
this.checkBudget(updated)
}
// 创建空指标
private createEmptyMetrics(): SSRPerformanceMetrics {
return {
server: {
renderTime: 0,
memoryUsage: 0,
cpuUsage: 0,
concurrentRequests: 0,
cacheHitRate: 0
},
client: {
ttfb: 0,
fcp: 0,
lcp: 0,
fid: 0,
cls: 0,
hydrationTime: 0
},
network: {
htmlSize: 0,
jsSize: 0,
cssSize: 0,
totalSize: 0,
requestCount: 0
}
}
}
// 合并指标
private mergeMetrics(
current: SSRPerformanceMetrics,
updates: Partial<SSRPerformanceMetrics>
): SSRPerformanceMetrics {
return {
server: { ...current.server, ...updates.server },
client: { ...current.client, ...updates.client },
network: { ...current.network, ...updates.network }
}
}
// 获取当前指标
private getCurrentMetrics(): SSRPerformanceMetrics | null {
return this.metrics[this.metrics.length - 1] || null
}
// 检查性能预算
private checkBudget(metrics: SSRPerformanceMetrics) {
const violations: string[] = []
// 检查服务端预算
if (metrics.server.renderTime > this.budget.server.maxRenderTime) {
violations.push(`Render time exceeded: ${metrics.server.renderTime}ms > ${this.budget.server.maxRenderTime}ms`)
}
// 检查客户端预算
if (metrics.client.ttfb > this.budget.client.maxTTFB) {
violations.push(`TTFB exceeded: ${metrics.client.ttfb}ms > ${this.budget.client.maxTTFB}ms`)
}
if (metrics.client.fcp > this.budget.client.maxFCP) {
violations.push(`FCP exceeded: ${metrics.client.fcp}ms > ${this.budget.client.maxFCP}ms`)
}
if (metrics.client.lcp > this.budget.client.maxLCP) {
violations.push(`LCP exceeded: ${metrics.client.lcp}ms > ${this.budget.client.maxLCP}ms`)
}
// 检查网络预算
if (metrics.network.htmlSize > this.budget.network.maxHtmlSize) {
violations.push(`HTML size exceeded: ${metrics.network.htmlSize}KB > ${this.budget.network.maxHtmlSize}KB`)
}
if (violations.length > 0) {
console.warn('Performance budget violations:', violations)
this.reportViolations(violations)
}
}
// 报告违规
private reportViolations(violations: string[]) {
// 发送到监控服务
if (typeof window !== 'undefined' && window.gtag) {
violations.forEach(violation => {
window.gtag('event', 'performance_budget_violation', {
violation_type: violation.split(':')[0],
violation_details: violation
})
})
}
}
// 记录服务端指标
recordServerMetrics(metrics: Partial<SSRPerformanceMetrics['server']>) {
const current = this.getCurrentMetrics() || this.createEmptyMetrics()
const updated = this.mergeMetrics(current, { server: metrics })
this.metrics.push(updated)
}
// 记录水合时间
recordHydrationTime(startTime: number) {
const hydrationTime = performance.now() - startTime
const metrics: Partial<SSRPerformanceMetrics> = {
client: { hydrationTime }
}
this.updateMetrics(metrics)
}
// 获取性能报告
getPerformanceReport() {
const latest = this.getCurrentMetrics()
if (!latest) return null
return {
metrics: latest,
budget: this.budget,
score: this.calculatePerformanceScore(latest),
recommendations: this.generateRecommendations(latest)
}
}
// 计算性能分数
private calculatePerformanceScore(metrics: SSRPerformanceMetrics): number {
const scores = {
ttfb: Math.max(0, 100 - (metrics.client.ttfb / this.budget.client.maxTTFB) * 100),
fcp: Math.max(0, 100 - (metrics.client.fcp / this.budget.client.maxFCP) * 100),
lcp: Math.max(0, 100 - (metrics.client.lcp / this.budget.client.maxLCP) * 100),
cls: Math.max(0, 100 - (metrics.client.cls / this.budget.client.maxCLS) * 100)
}
return Object.values(scores).reduce((sum, score) => sum + score, 0) / Object.keys(scores).length
}
// 生成优化建议
private generateRecommendations(metrics: SSRPerformanceMetrics): string[] {
const recommendations: string[] = []
if (metrics.client.ttfb > this.budget.client.maxTTFB) {
recommendations.push('优化服务器响应时间,考虑使用 CDN 或缓存')
}
if (metrics.client.fcp > this.budget.client.maxFCP) {
recommendations.push('优化关键渲染路径,减少阻塞资源')
}
if (metrics.client.lcp > this.budget.client.maxLCP) {
recommendations.push('优化最大内容绘制,压缩图片和字体')
}
if (metrics.client.cls > this.budget.client.maxCLS) {
recommendations.push('减少布局偏移,为图片和广告预留空间')
}
if (metrics.network.totalSize > 500) {
recommendations.push('减少资源大小,启用压缩和代码分割')
}
return recommendations
}
// 清理观察器
cleanup() {
this.observers.forEach(observer => observer.disconnect())
this.observers.clear()
}
}
2. 多层次缓存策略
2.1 页面级缓存
typescript
// server/cache/page-cache.ts
import LRU from 'lru-cache'
import { createHash } from 'crypto'
export class PageCache {
private cache: LRU<string, CachedPage>
private config: PageCacheConfig
constructor(config: PageCacheConfig) {
this.config = config
this.cache = new LRU({
max: config.maxPages,
ttl: config.defaultTTL,
updateAgeOnGet: true,
allowStale: false
})
}
// 生成缓存键
private generateCacheKey(url: string, context: CacheContext): string {
const keyData = {
url: url.split('?')[0], // 移除查询参数
locale: context.locale,
theme: context.theme,
userAgent: this.normalizeUserAgent(context.userAgent),
viewport: context.viewport
}
const keyString = JSON.stringify(keyData)
return createHash('md5').update(keyString).digest('hex')
}
// 标准化用户代理
private normalizeUserAgent(userAgent: string): string {
// 简化用户代理字符串,只保留主要信息
if (userAgent.includes('Mobile')) return 'mobile'
if (userAgent.includes('Tablet')) return 'tablet'
return 'desktop'
}
// 检查是否可缓存
private isCacheable(url: string, context: CacheContext): boolean {
// 不缓存的条件
const noCacheConditions = [
url.includes('/api/'),
url.includes('/admin/'),
context.isAuthenticated,
context.hasPersonalizedContent,
url.includes('preview=true')
]
return !noCacheConditions.some(condition => condition)
}
// 获取缓存
async get(url: string, context: CacheContext): Promise<CachedPage | null> {
if (!this.isCacheable(url, context)) {
return null
}
const key = this.generateCacheKey(url, context)
const cached = this.cache.get(key)
if (cached) {
// 检查是否需要重新验证
if (this.needsRevalidation(cached)) {
return null
}
// 更新命中统计
this.updateHitStats(key)
return cached
}
return null
}
// 设置缓存
async set(
url: string,
context: CacheContext,
html: string,
metadata: PageMetadata
): Promise<void> {
if (!this.isCacheable(url, context)) {
return
}
const key = this.generateCacheKey(url, context)
const ttl = this.calculateTTL(url, metadata)
const cachedPage: CachedPage = {
html,
metadata,
createdAt: Date.now(),
lastAccessed: Date.now(),
accessCount: 0,
etag: this.generateETag(html),
size: Buffer.byteLength(html, 'utf8')
}
this.cache.set(key, cachedPage, { ttl })
}
// 计算 TTL
private calculateTTL(url: string, metadata: PageMetadata): number {
// 根据页面类型调整 TTL
if (url === '/' || url === '/home') {
return this.config.homepageTTL
}
if (url.startsWith('/blog/')) {
return this.config.blogTTL
}
if (url.startsWith('/product/')) {
return this.config.productTTL
}
if (metadata.isStatic) {
return this.config.staticTTL
}
return this.config.defaultTTL
}
// 检查是否需要重新验证
private needsRevalidation(cached: CachedPage): boolean {
const now = Date.now()
const age = now - cached.createdAt
// 如果超过最大年龄,需要重新验证
if (age > this.config.maxAge) {
return true
}
// 如果很少访问且年龄较大,需要重新验证
if (cached.accessCount < 5 && age > this.config.defaultTTL / 2) {
return true
}
return false
}
// 生成 ETag
private generateETag(html: string): string {
return createHash('md5').update(html).digest('hex').substring(0, 16)
}
// 更新命中统计
private updateHitStats(key: string) {
const cached = this.cache.get(key)
if (cached) {
cached.lastAccessed = Date.now()
cached.accessCount++
}
}
// 清除缓存
clear(pattern?: string) {
if (pattern) {
// 清除匹配模式的缓存
const keys = Array.from(this.cache.keys())
keys.forEach(key => {
if (key.includes(pattern)) {
this.cache.delete(key)
}
})
} else {
this.cache.clear()
}
}
// 获取缓存统计
getStats() {
return {
size: this.cache.size,
maxSize: this.cache.max,
hitRate: this.calculateHitRate(),
memoryUsage: this.calculateMemoryUsage()
}
}
// 计算命中率
private calculateHitRate(): number {
// 这里需要实现命中率计算逻辑
return 0.85 // 示例值
}
// 计算内存使用
private calculateMemoryUsage(): number {
let totalSize = 0
this.cache.forEach(page => {
totalSize += page.size
})
return totalSize / 1024 / 1024 // 转换为 MB
}
}
// 类型定义
interface PageCacheConfig {
maxPages: number
defaultTTL: number
maxAge: number
homepageTTL: number
blogTTL: number
productTTL: number
staticTTL: number
}
interface CacheContext {
locale: string
theme: string
userAgent: string
viewport: string
isAuthenticated: boolean
hasPersonalizedContent: boolean
}
interface PageMetadata {
title: string
description: string
isStatic: boolean
lastModified: number
dependencies: string[]
}
interface CachedPage {
html: string
metadata: PageMetadata
createdAt: number
lastAccessed: number
accessCount: number
etag: string
size: number
}
2.2 组件级缓存
typescript
// server/cache/component-cache.ts
export class ComponentCache {
private cache: Map<string, CachedComponent> = new Map()
private config: ComponentCacheConfig
constructor(config: ComponentCacheConfig) {
this.config = config
this.setupCleanupInterval()
}
// 设置清理间隔
private setupCleanupInterval() {
setInterval(() => {
this.cleanup()
}, this.config.cleanupInterval)
}
// 生成组件缓存键
private generateComponentKey(
componentName: string,
props: Record<string, any>,
context: ComponentCacheContext
): string {
const keyData = {
component: componentName,
props: this.normalizeProps(props),
locale: context.locale,
theme: context.theme
}
const keyString = JSON.stringify(keyData)
return createHash('md5').update(keyString).digest('hex')
}
// 标准化属性
private normalizeProps(props: Record<string, any>): Record<string, any> {
const normalized: Record<string, any> = {}
Object.keys(props).sort().forEach(key => {
const value = props[key]
// 忽略函数和复杂对象
if (typeof value === 'function') {
return
}
if (typeof value === 'object' && value !== null) {
// 只保留简单对象的序列化
try {
normalized[key] = JSON.parse(JSON.stringify(value))
} catch {
// 忽略无法序列化的对象
}
} else {
normalized[key] = value
}
})
return normalized
}
// 检查组件是否可缓存
private isComponentCacheable(
componentName: string,
props: Record<string, any>
): boolean {
// 不可缓存的组件
const noCacheComponents = [
'UserProfile',
'ShoppingCart',
'LiveChat',
'RandomContent'
]
if (noCacheComponents.includes(componentName)) {
return false
}
// 包含动态内容的属性
const dynamicProps = ['timestamp', 'random', 'userId', 'sessionId']
const hasDynamicProps = Object.keys(props).some(key =>
dynamicProps.some(dynamicProp => key.includes(dynamicProp))
)
return !hasDynamicProps
}
// 获取组件缓存
async getComponent(
componentName: string,
props: Record<string, any>,
context: ComponentCacheContext
): Promise<string | null> {
if (!this.isComponentCacheable(componentName, props)) {
return null
}
const key = this.generateComponentKey(componentName, props, context)
const cached = this.cache.get(key)
if (cached && !this.isExpired(cached)) {
cached.lastAccessed = Date.now()
cached.accessCount++
return cached.html
}
return null
}
// 设置组件缓存
async setComponent(
componentName: string,
props: Record<string, any>,
context: ComponentCacheContext,
html: string
): Promise<void> {
if (!this.isComponentCacheable(componentName, props)) {
return
}
const key = this.generateComponentKey(componentName, props, context)
const ttl = this.getComponentTTL(componentName)
const cached: CachedComponent = {
html,
componentName,
createdAt: Date.now(),
lastAccessed: Date.now(),
accessCount: 1,
ttl,
size: Buffer.byteLength(html, 'utf8')
}
this.cache.set(key, cached)
// 检查缓存大小限制
this.enforceMemoryLimit()
}
// 获取组件 TTL
private getComponentTTL(componentName: string): number {
const ttlMap: Record<string, number> = {
'Header': 3600000, // 1 小时
'Footer': 3600000, // 1 小时
'Navigation': 1800000, // 30 分钟
'ProductCard': 600000, // 10 分钟
'BlogPost': 1800000, // 30 分钟
'StaticContent': 7200000 // 2 小时
}
return ttlMap[componentName] || this.config.defaultTTL
}
// 检查是否过期
private isExpired(cached: CachedComponent): boolean {
return Date.now() - cached.createdAt > cached.ttl
}
// 强制内存限制
private enforceMemoryLimit() {
let totalSize = 0
this.cache.forEach(cached => {
totalSize += cached.size
})
if (totalSize > this.config.maxMemoryUsage) {
// 按最后访问时间排序,删除最旧的
const entries = Array.from(this.cache.entries())
entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed)
while (totalSize > this.config.maxMemoryUsage * 0.8 && entries.length > 0) {
const [key, cached] = entries.shift()!
this.cache.delete(key)
totalSize -= cached.size
}
}
}
// 清理过期缓存
private cleanup() {
const now = Date.now()
const toDelete: string[] = []
this.cache.forEach((cached, key) => {
if (this.isExpired(cached)) {
toDelete.push(key)
}
})
toDelete.forEach(key => {
this.cache.delete(key)
})
console.log(`Cleaned up ${toDelete.length} expired component cache entries`)
}
// 清除特定组件的缓存
clearComponent(componentName: string) {
const toDelete: string[] = []
this.cache.forEach((cached, key) => {
if (cached.componentName === componentName) {
toDelete.push(key)
}
})
toDelete.forEach(key => {
this.cache.delete(key)
})
}
// 获取缓存统计
getStats() {
let totalSize = 0
let totalAccess = 0
const componentStats: Record<string, number> = {}
this.cache.forEach(cached => {
totalSize += cached.size
totalAccess += cached.accessCount
componentStats[cached.componentName] =
(componentStats[cached.componentName] || 0) + 1
})
return {
totalEntries: this.cache.size,
totalSize: totalSize / 1024 / 1024, // MB
averageAccess: totalAccess / this.cache.size,
componentStats
}
}
}
// 类型定义
interface ComponentCacheConfig {
defaultTTL: number
maxMemoryUsage: number
cleanupInterval: number
}
interface ComponentCacheContext {
locale: string
theme: string
}
interface CachedComponent {
html: string
componentName: string
createdAt: number
lastAccessed: number
accessCount: number
ttl: number
size: number
}
2.3 数据缓存层
typescript
// server/cache/data-cache.ts
import Redis from 'ioredis'
export class DataCache {
private redis: Redis
private localCache: Map<string, CachedData> = new Map()
private config: DataCacheConfig
constructor(config: DataCacheConfig) {
this.config = config
this.redis = new Redis(config.redis)
this.setupLocalCacheCleanup()
}
// 设置本地缓存清理
private setupLocalCacheCleanup() {
setInterval(() => {
this.cleanupLocalCache()
}, this.config.localCleanupInterval)
}
// 获取数据
async get<T>(key: string): Promise<T | null> {
// 首先检查本地缓存
const localData = this.getFromLocalCache<T>(key)
if (localData !== null) {
return localData
}
// 然后检查 Redis 缓存
try {
const redisData = await this.getFromRedis<T>(key)
if (redisData !== null) {
// 将数据存储到本地缓存
this.setToLocalCache(key, redisData, this.config.localTTL)
return redisData
}
} catch (error) {
console.error('Redis get error:', error)
}
return null
}
// 设置数据
async set<T>(
key: string,
data: T,
ttl: number = this.config.defaultTTL
): Promise<void> {
// 存储到本地缓存
this.setToLocalCache(key, data, Math.min(ttl, this.config.localTTL))
// 存储到 Redis
try {
await this.setToRedis(key, data, ttl)
} catch (error) {
console.error('Redis set error:', error)
}
}
// 从本地缓存获取
private getFromLocalCache<T>(key: string): T | null {
const cached = this.localCache.get(key)
if (!cached) {
return null
}
if (Date.now() > cached.expiresAt) {
this.localCache.delete(key)
return null
}
cached.lastAccessed = Date.now()
return cached.data as T
}
// 设置到本地缓存
private setToLocalCache<T>(key: string, data: T, ttl: number) {
const cached: CachedData = {
data,
createdAt: Date.now(),
lastAccessed: Date.now(),
expiresAt: Date.now() + ttl
}
this.localCache.set(key, cached)
// 检查本地缓存大小
if (this.localCache.size > this.config.maxLocalEntries) {
this.evictLocalCache()
}
}
// 从 Redis 获取
private async getFromRedis<T>(key: string): Promise<T | null> {
const data = await this.redis.get(key)
if (!data) {
return null
}
try {
return JSON.parse(data) as T
} catch (error) {
console.error('JSON parse error:', error)
return null
}
}
// 设置到 Redis
private async setToRedis<T>(key: string, data: T, ttl: number) {
const serialized = JSON.stringify(data)
await this.redis.setex(key, Math.floor(ttl / 1000), serialized)
}
// 驱逐本地缓存
private evictLocalCache() {
const entries = Array.from(this.localCache.entries())
entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed)
// 删除最旧的 25% 条目
const toDelete = Math.floor(entries.length * 0.25)
for (let i = 0; i < toDelete; i++) {
this.localCache.delete(entries[i][0])
}
}
// 清理本地缓存
private cleanupLocalCache() {
const now = Date.now()
const toDelete: string[] = []
this.localCache.forEach((cached, key) => {
if (now > cached.expiresAt) {
toDelete.push(key)
}
})
toDelete.forEach(key => {
this.localCache.delete(key)
})
}
// 删除缓存
async delete(key: string): Promise<void> {
this.localCache.delete(key)
try {
await this.redis.del(key)
} catch (error) {
console.error('Redis delete error:', error)
}
}
// 批量删除
async deletePattern(pattern: string): Promise<void> {
// 清理本地缓存
const localKeys = Array.from(this.localCache.keys())
localKeys.forEach(key => {
if (key.includes(pattern)) {
this.localCache.delete(key)
}
})
// 清理 Redis 缓存
try {
const keys = await this.redis.keys(`*${pattern}*`)
if (keys.length > 0) {
await this.redis.del(...keys)
}
} catch (error) {
console.error('Redis delete pattern error:', error)
}
}
// 获取缓存统计
async getStats() {
const localStats = {
entries: this.localCache.size,
maxEntries: this.config.maxLocalEntries
}
let redisStats = {
entries: 0,
memory: 0
}
try {
const info = await this.redis.info('memory')
const dbsize = await this.redis.dbsize()
redisStats = {
entries: dbsize,
memory: this.parseRedisMemory(info)
}
} catch (error) {
console.error('Redis stats error:', error)
}
return {
local: localStats,
redis: redisStats
}
}
// 解析 Redis 内存信息
private parseRedisMemory(info: string): number {
const match = info.match(/used_memory:(\d+)/)
return match ? parseInt(match[1]) / 1024 / 1024 : 0 // MB
}
}
// 类型定义
interface DataCacheConfig {
redis: {
host: string
port: number
password?: string
db?: number
}
defaultTTL: number
localTTL: number
maxLocalEntries: number
localCleanupInterval: number
}
interface CachedData {
data: any
createdAt: number
lastAccessed: number
expiresAt: number
}
3. Element Plus 特定优化
3.1 组件按需加载优化
typescript
// utils/element-plus-optimizer.ts
export class ElementPlusOptimizer {
private loadedComponents: Set<string> = new Set()
private componentRegistry: Map<string, () => Promise<any>> = new Map()
constructor() {
this.setupComponentRegistry()
}
// 设置组件注册表
private setupComponentRegistry() {
// 按需注册 Element Plus 组件
this.componentRegistry.set('ElButton', () => import('element-plus/es/components/button'))
this.componentRegistry.set('ElInput', () => import('element-plus/es/components/input'))
this.componentRegistry.set('ElForm', () => import('element-plus/es/components/form'))
this.componentRegistry.set('ElTable', () => import('element-plus/es/components/table'))
this.componentRegistry.set('ElDialog', () => import('element-plus/es/components/dialog'))
this.componentRegistry.set('ElSelect', () => import('element-plus/es/components/select'))
this.componentRegistry.set('ElDatePicker', () => import('element-plus/es/components/date-picker'))
this.componentRegistry.set('ElPagination', () => import('element-plus/es/components/pagination'))
// ... 更多组件
}
// 预加载关键组件
async preloadCriticalComponents(): Promise<void> {
const criticalComponents = [
'ElButton',
'ElInput',
'ElForm',
'ElMessage'
]
const loadPromises = criticalComponents.map(component =>
this.loadComponent(component)
)
await Promise.all(loadPromises)
}
// 加载组件
async loadComponent(componentName: string): Promise<any> {
if (this.loadedComponents.has(componentName)) {
return
}
const loader = this.componentRegistry.get(componentName)
if (!loader) {
console.warn(`Component ${componentName} not found in registry`)
return
}
try {
const component = await loader()
this.loadedComponents.add(componentName)
return component
} catch (error) {
console.error(`Failed to load component ${componentName}:`, error)
}
}
// 分析页面组件使用情况
analyzePageComponents(html: string): string[] {
const componentPattern = /<el-([a-z-]+)/gi
const matches = html.match(componentPattern) || []
const components = matches.map(match => {
const componentName = match.replace('<el-', '').replace(/[^a-z-]/gi, '')
return this.kebabToPascal(`el-${componentName}`)
})
return [...new Set(components)]
}
// 转换命名格式
private kebabToPascal(kebab: string): string {
return kebab
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('')
}
// 生成组件加载脚本
generateLoadScript(components: string[]): string {
const loadStatements = components
.filter(component => this.componentRegistry.has(component))
.map(component => `import('element-plus/es/components/${this.pascalToKebab(component.replace('El', ''))}')`)
return `
Promise.all([
${loadStatements.join(',\n ')}
]).then(() => {
console.log('Element Plus components loaded');
});
`
}
// 转换命名格式
private pascalToKebab(pascal: string): string {
return pascal.replace(/([A-Z])/g, '-$1').toLowerCase().slice(1)
}
// 获取加载统计
getLoadStats() {
return {
totalRegistered: this.componentRegistry.size,
totalLoaded: this.loadedComponents.size,
loadedComponents: Array.from(this.loadedComponents),
loadRate: this.loadedComponents.size / this.componentRegistry.size
}
}
}
3.2 样式优化
typescript
// utils/style-optimizer.ts
export class StyleOptimizer {
private criticalCSS: string = ''
private nonCriticalCSS: string = ''
// 提取关键 CSS
async extractCriticalCSS(html: string, fullCSS: string): Promise<string> {
// 分析 HTML 中使用的 Element Plus 组件
const usedComponents = this.analyzeUsedComponents(html)
// 提取关键样式
const criticalRules = this.extractComponentStyles(fullCSS, usedComponents)
// 添加基础样式
const baseStyles = this.extractBaseStyles(fullCSS)
return baseStyles + criticalRules
}
// 分析使用的组件
private analyzeUsedComponents(html: string): string[] {
const componentClasses = new Set<string>()
// Element Plus 组件类名模式
const classPatterns = [
/el-button[\w-]*/g,
/el-input[\w-]*/g,
/el-form[\w-]*/g,
/el-table[\w-]*/g,
/el-dialog[\w-]*/g,
/el-select[\w-]*/g,
/el-date-picker[\w-]*/g,
/el-pagination[\w-]*/g
]
classPatterns.forEach(pattern => {
const matches = html.match(pattern) || []
matches.forEach(match => componentClasses.add(match))
})
return Array.from(componentClasses)
}
// 提取组件样式
private extractComponentStyles(css: string, components: string[]): string {
const rules: string[] = []
components.forEach(component => {
// 提取组件相关的 CSS 规则
const componentRules = this.extractRulesForComponent(css, component)
rules.push(...componentRules)
})
return rules.join('\n')
}
// 提取特定组件的规则
private extractRulesForComponent(css: string, component: string): string[] {
const rules: string[] = []
const rulePattern = new RegExp(`\\.[^{]*${component}[^{]*\\{[^}]*\\}`, 'g')
const matches = css.match(rulePattern) || []
rules.push(...matches)
return rules
}
// 提取基础样式
private extractBaseStyles(css: string): string {
const baseSelectors = [
':root',
'html',
'body',
'*',
'*::before',
'*::after'
]
const rules: string[] = []
baseSelectors.forEach(selector => {
const pattern = new RegExp(`${selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{[^}]*\\}`, 'g')
const matches = css.match(pattern) || []
rules.push(...matches)
})
return rules.join('\n')
}
// 生成内联关键 CSS
generateInlineCriticalCSS(criticalCSS: string): string {
return `<style>${this.minifyCSS(criticalCSS)}</style>`
}
// 生成非关键 CSS 加载脚本
generateNonCriticalCSSLoader(cssUrl: string): string {
return `
<script>
(function() {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '${cssUrl}';
link.media = 'print';
link.onload = function() {
this.media = 'all';
};
document.head.appendChild(link);
})();
</script>
`
}
// 压缩 CSS
private minifyCSS(css: string): string {
return css
.replace(/\/\*[\s\S]*?\*\//g, '') // 移除注释
.replace(/\s+/g, ' ') // 压缩空白
.replace(/;\s*}/g, '}') // 移除最后的分号
.replace(/\s*{\s*/g, '{') // 压缩大括号
.replace(/;\s*/g, ';') // 压缩分号
.trim()
}
// 分析 CSS 使用情况
analyzeCSSUsage(html: string, css: string) {
const allRules = this.extractAllRules(css)
const usedRules = allRules.filter(rule =>
this.isRuleUsed(rule, html)
)
return {
totalRules: allRules.length,
usedRules: usedRules.length,
unusedRules: allRules.length - usedRules.length,
usageRate: usedRules.length / allRules.length,
potentialSavings: this.calculateSavings(allRules, usedRules)
}
}
// 提取所有规则
private extractAllRules(css: string): string[] {
const rulePattern = /[^{}]+\{[^}]*\}/g
return css.match(rulePattern) || []
}
// 检查规则是否被使用
private isRuleUsed(rule: string, html: string): boolean {
const selectorMatch = rule.match(/^([^{]+)/)
if (!selectorMatch) return false
const selector = selectorMatch[1].trim()
// 简化的选择器检查
if (selector.startsWith('.')) {
const className = selector.substring(1).split(/[\s:>+~]/)[0]
return html.includes(className)
}
if (selector.startsWith('#')) {
const id = selector.substring(1).split(/[\s:>+~]/)[0]
return html.includes(`id="${id}"`)
}
// 元素选择器
const element = selector.split(/[\s:>+~]/)[0]
return html.includes(`<${element}`)
}
// 计算潜在节省
private calculateSavings(allRules: string[], usedRules: string[]): number {
const totalSize = allRules.join('').length
const usedSize = usedRules.join('').length
return totalSize - usedSize
}
}
4. 实践练习
性能监控实践:
- 集成性能监控系统
- 设置性能预算和告警
- 分析性能瓶颈
缓存策略实践:
- 实现多层次缓存
- 测试缓存效果
- 优化缓存策略
Element Plus 优化实践:
- 实现按需加载
- 优化样式加载
- 测试优化效果
5. 学习资源
- Web Performance Optimization
- Vue.js Performance Guide
- Element Plus Performance Tips
- Redis Caching Strategies
6. 作业
- 实现完整的 SSR 性能监控系统
- 设计和实现多层次缓存架构
- 优化 Element Plus 组件的加载性能
- 创建性能优化最佳实践文档
总结
通过第56天的学习,我们深入掌握了:
- 性能监控:建立了完整的 SSR 性能指标体系和监控系统
- 缓存策略:实现了页面级、组件级和数据级的多层次缓存
- Element Plus 优化:掌握了组件按需加载和样式优化技术
- 性能分析:学会了性能瓶颈分析和优化方法
这些技能将帮助我们构建高性能、可扩展的 SSR 应用。