Appearance
前端性能优化实战指南
前端性能优化是提升用户体验的关键因素。本文将从多个维度深入探讨前端性能优化的策略和实践,帮助你构建更快、更流畅的 Web 应用。
1. 性能指标与测量
1.1 Core Web Vitals
javascript
// 测量 Core Web Vitals
function measureWebVitals() {
// Largest Contentful Paint (LCP)
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
const lastEntry = entries[entries.length - 1]
console.log('LCP:', lastEntry.startTime)
}).observe({ entryTypes: ['largest-contentful-paint'] })
// First Input Delay (FID)
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
entries.forEach((entry) => {
if (entry.name === 'first-input') {
const fid = entry.processingStart - entry.startTime
console.log('FID:', fid)
}
})
}).observe({ entryTypes: ['first-input'] })
// Cumulative Layout Shift (CLS)
let clsValue = 0
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
entries.forEach((entry) => {
if (!entry.hadRecentInput) {
clsValue += entry.value
console.log('CLS:', clsValue)
}
})
}).observe({ entryTypes: ['layout-shift'] })
}
// 页面加载完成后测量
window.addEventListener('load', measureWebVitals)
1.2 自定义性能指标
javascript
// 自定义性能测量
class PerformanceTracker {
constructor() {
this.marks = new Map()
this.measures = new Map()
}
mark(name) {
const timestamp = performance.now()
this.marks.set(name, timestamp)
performance.mark(name)
}
measure(name, startMark, endMark) {
const startTime = this.marks.get(startMark)
const endTime = this.marks.get(endMark)
if (startTime && endTime) {
const duration = endTime - startTime
this.measures.set(name, duration)
performance.measure(name, startMark, endMark)
return duration
}
}
getMetrics() {
return {
marks: Object.fromEntries(this.marks),
measures: Object.fromEntries(this.measures)
}
}
}
const tracker = new PerformanceTracker()
// 使用示例
tracker.mark('api-start')
fetch('/api/data')
.then(response => {
tracker.mark('api-end')
tracker.measure('api-duration', 'api-start', 'api-end')
return response.json()
})
2. 加载性能优化
2.1 资源优化
html
<!-- 预加载关键资源 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/css/critical.css" as="style">
<link rel="preload" href="/js/main.js" as="script">
<!-- 预连接外部域名 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://api.example.com">
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//cdn.example.com">
<!-- 预获取下一页资源 -->
<link rel="prefetch" href="/page2.html">
<link rel="prefetch" href="/api/next-data">
2.2 图片优化
html
<!-- 响应式图片 -->
<picture>
<source media="(min-width: 800px)" srcset="large.webp" type="image/webp">
<source media="(min-width: 800px)" srcset="large.jpg">
<source media="(min-width: 400px)" srcset="medium.webp" type="image/webp">
<source media="(min-width: 400px)" srcset="medium.jpg">
<img src="small.jpg" alt="描述" loading="lazy">
</picture>
<!-- 现代图片格式 -->
<img src="image.jpg"
srcset="image-320w.webp 320w, image-640w.webp 640w, image-1280w.webp 1280w"
sizes="(max-width: 320px) 280px, (max-width: 640px) 600px, 1200px"
alt="描述"
loading="lazy">
javascript
// 图片懒加载实现
class LazyImageLoader {
constructor() {
this.imageObserver = new IntersectionObserver(
this.handleIntersection.bind(this),
{
rootMargin: '50px 0px',
threshold: 0.01
}
)
this.init()
}
init() {
const lazyImages = document.querySelectorAll('img[data-src]')
lazyImages.forEach(img => this.imageObserver.observe(img))
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
this.loadImage(img)
this.imageObserver.unobserve(img)
}
})
}
loadImage(img) {
const src = img.dataset.src
const srcset = img.dataset.srcset
if (src) {
img.src = src
}
if (srcset) {
img.srcset = srcset
}
img.classList.add('loaded')
// 移除 data 属性
delete img.dataset.src
delete img.dataset.srcset
}
}
new LazyImageLoader()
2.3 代码分割
javascript
// 动态导入
const loadModule = async (moduleName) => {
try {
const module = await import(`./modules/${moduleName}.js`)
return module.default
} catch (error) {
console.error(`Failed to load module: ${moduleName}`, error)
throw error
}
}
// 路由级别的代码分割
const routes = [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
},
{
path: '/admin',
component: () => import('./views/Admin.vue')
}
]
// 组件级别的代码分割
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})
3. 运行时性能优化
3.1 JavaScript 优化
javascript
// 防抖和节流
function debounce(func, wait, immediate) {
let timeout
return function executedFunction(...args) {
const later = () => {
timeout = null
if (!immediate) func(...args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func(...args)
}
}
function throttle(func, limit) {
let inThrottle
return function(...args) {
if (!inThrottle) {
func.apply(this, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
// 使用示例
const debouncedSearch = debounce((query) => {
searchAPI(query)
}, 300)
const throttledScroll = throttle(() => {
updateScrollPosition()
}, 16) // 60fps
// 事件监听
input.addEventListener('input', debouncedSearch)
window.addEventListener('scroll', throttledScroll)
3.2 DOM 操作优化
javascript
// 批量 DOM 操作
function batchDOMUpdates(updates) {
const fragment = document.createDocumentFragment()
updates.forEach(update => {
const element = document.createElement(update.tag)
element.textContent = update.text
element.className = update.className
fragment.appendChild(element)
})
// 一次性插入到 DOM
document.getElementById('container').appendChild(fragment)
}
// 使用 requestAnimationFrame 优化动画
function smoothScroll(element, to, duration) {
const start = element.scrollTop
const change = to - start
const startTime = performance.now()
function animateScroll(currentTime) {
const timeElapsed = currentTime - startTime
const progress = Math.min(timeElapsed / duration, 1)
element.scrollTop = start + change * easeInOutQuad(progress)
if (progress < 1) {
requestAnimationFrame(animateScroll)
}
}
requestAnimationFrame(animateScroll)
}
function easeInOutQuad(t) {
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
}
3.3 内存管理
javascript
// 内存泄漏预防
class ComponentManager {
constructor() {
this.eventListeners = new Map()
this.timers = new Set()
this.observers = new Set()
}
addEventListener(element, event, handler) {
element.addEventListener(event, handler)
if (!this.eventListeners.has(element)) {
this.eventListeners.set(element, [])
}
this.eventListeners.get(element).push({ event, handler })
}
setTimeout(callback, delay) {
const timerId = setTimeout(callback, delay)
this.timers.add(timerId)
return timerId
}
setInterval(callback, interval) {
const timerId = setInterval(callback, interval)
this.timers.add(timerId)
return timerId
}
observe(observer) {
this.observers.add(observer)
return observer
}
cleanup() {
// 清理事件监听器
this.eventListeners.forEach((listeners, element) => {
listeners.forEach(({ event, handler }) => {
element.removeEventListener(event, handler)
})
})
this.eventListeners.clear()
// 清理定时器
this.timers.forEach(timerId => {
clearTimeout(timerId)
clearInterval(timerId)
})
this.timers.clear()
// 清理观察者
this.observers.forEach(observer => {
if (observer.disconnect) {
observer.disconnect()
}
})
this.observers.clear()
}
}
4. 缓存策略
4.1 HTTP 缓存
javascript
// Service Worker 缓存策略
self.addEventListener('fetch', event => {
const { request } = event
const url = new URL(request.url)
// 静态资源缓存优先
if (request.destination === 'script' ||
request.destination === 'style' ||
request.destination === 'image') {
event.respondWith(
caches.match(request).then(response => {
return response || fetch(request).then(fetchResponse => {
const responseClone = fetchResponse.clone()
caches.open('static-v1').then(cache => {
cache.put(request, responseClone)
})
return fetchResponse
})
})
)
}
// API 请求网络优先
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(request).then(response => {
const responseClone = response.clone()
caches.open('api-v1').then(cache => {
cache.put(request, responseClone)
})
return response
}).catch(() => {
return caches.match(request)
})
)
}
})
4.2 应用级缓存
javascript
// LRU 缓存实现
class LRUCache {
constructor(capacity) {
this.capacity = capacity
this.cache = new Map()
}
get(key) {
if (this.cache.has(key)) {
const value = this.cache.get(key)
// 移动到最新位置
this.cache.delete(key)
this.cache.set(key, value)
return value
}
return null
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key)
} else if (this.cache.size >= this.capacity) {
// 删除最久未使用的项
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, value)
}
clear() {
this.cache.clear()
}
}
// 使用示例
const apiCache = new LRUCache(100)
async function fetchWithCache(url) {
const cached = apiCache.get(url)
if (cached) {
return cached
}
const response = await fetch(url)
const data = await response.json()
apiCache.set(url, data)
return data
}
5. 网络优化
5.1 HTTP/2 优化
javascript
// 服务器推送
app.get('/', (req, res) => {
// 推送关键资源
res.push('/css/critical.css')
res.push('/js/main.js')
res.push('/fonts/main.woff2')
res.render('index')
})
// 多路复用优化
const fetchMultiple = async (urls) => {
const promises = urls.map(url => fetch(url))
const responses = await Promise.all(promises)
return Promise.all(responses.map(res => res.json()))
}
5.2 请求优化
javascript
// 请求合并
class RequestBatcher {
constructor(batchSize = 10, delay = 100) {
this.batchSize = batchSize
this.delay = delay
this.queue = []
this.timer = null
}
add(request) {
return new Promise((resolve, reject) => {
this.queue.push({ request, resolve, reject })
if (this.queue.length >= this.batchSize) {
this.flush()
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.delay)
}
})
}
async flush() {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
const batch = this.queue.splice(0, this.batchSize)
if (batch.length === 0) return
try {
const requests = batch.map(item => item.request)
const response = await fetch('/api/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ requests })
})
const results = await response.json()
batch.forEach((item, index) => {
item.resolve(results[index])
})
} catch (error) {
batch.forEach(item => item.reject(error))
}
}
}
const batcher = new RequestBatcher()
// 使用示例
const getUserData = (userId) => {
return batcher.add({ type: 'getUser', userId })
}
6. 构建优化
6.1 Webpack 优化
javascript
// webpack.config.js
const path = require('path')
const TerserPlugin = require('terser-webpack-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
enforce: true
}
}
},
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
})
]
},
plugins: [
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8
})
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
}
6.2 Vite 优化
javascript
// vite.config.js
import { defineConfig } from 'vite'
import { resolve } from 'path'
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'],
ui: ['element-plus'],
utils: ['lodash', 'dayjs']
}
}
},
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
optimizeDeps: {
include: ['vue', 'vue-router'],
exclude: ['@vueuse/core']
}
})
7. 监控与分析
7.1 性能监控
javascript
// 性能监控系统
class PerformanceMonitor {
constructor() {
this.metrics = {}
this.observers = []
this.init()
}
init() {
this.observeNavigation()
this.observeResources()
this.observeLongTasks()
this.observeMemory()
}
observeNavigation() {
window.addEventListener('load', () => {
const navigation = performance.getEntriesByType('navigation')[0]
this.metrics.navigation = {
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
tcp: navigation.connectEnd - navigation.connectStart,
request: navigation.responseStart - navigation.requestStart,
response: navigation.responseEnd - navigation.responseStart,
dom: navigation.domContentLoadedEventEnd - navigation.navigationStart,
load: navigation.loadEventEnd - navigation.navigationStart
}
})
}
observeResources() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
if (entry.duration > 1000) { // 超过1秒的资源
console.warn('Slow resource:', entry.name, entry.duration)
}
})
})
observer.observe({ entryTypes: ['resource'] })
this.observers.push(observer)
}
observeLongTasks() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
console.warn('Long task detected:', entry.duration)
})
})
observer.observe({ entryTypes: ['longtask'] })
this.observers.push(observer)
}
observeMemory() {
if ('memory' in performance) {
setInterval(() => {
const memory = performance.memory
this.metrics.memory = {
used: memory.usedJSHeapSize,
total: memory.totalJSHeapSize,
limit: memory.jsHeapSizeLimit
}
// 内存使用率超过80%时警告
if (memory.usedJSHeapSize / memory.jsHeapSizeLimit > 0.8) {
console.warn('High memory usage detected')
}
}, 10000) // 每10秒检查一次
}
}
getMetrics() {
return this.metrics
}
sendMetrics() {
fetch('/api/metrics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.metrics)
})
}
}
const monitor = new PerformanceMonitor()
8. 最佳实践总结
8.1 加载优化清单
- [ ] 启用 HTTP/2
- [ ] 使用 CDN
- [ ] 压缩资源(Gzip/Brotli)
- [ ] 优化图片格式和大小
- [ ] 实现懒加载
- [ ] 代码分割
- [ ] 预加载关键资源
- [ ] 移除未使用的代码
8.2 运行时优化清单
- [ ] 防抖和节流
- [ ] 虚拟滚动
- [ ] 内存泄漏预防
- [ ] 避免强制同步布局
- [ ] 优化动画性能
- [ ] 合理使用缓存
- [ ] 减少 DOM 操作
- [ ] 优化事件处理
8.3 监控清单
- [ ] Core Web Vitals 监控
- [ ] 资源加载监控
- [ ] 错误监控
- [ ] 用户行为分析
- [ ] 性能预算设置
- [ ] 持续性能测试
总结
前端性能优化是一个持续的过程,需要从多个维度进行考虑:
- 测量为先:建立完善的性能监控体系
- 加载优化:减少资源大小,优化加载策略
- 运行时优化:提升代码执行效率
- 缓存策略:合理利用各级缓存
- 网络优化:减少网络请求,提升传输效率
- 构建优化:优化打包和部署流程
- 持续监控:建立性能监控和告警机制
通过系统性的优化策略,可以显著提升 Web 应用的性能和用户体验。