前端性能优化
大约 10 分钟约 2954 字
前端性能优化
简介
前端性能直接影响用户体验和业务指标。优化涵盖网络加载、资源体积、渲染性能和运行时优化。掌握性能分析工具和优化策略,是全栈开发者的加分项。
特点
性能指标
Core Web Vitals
| 指标 | 含义 | 目标值 |
|---|---|---|
| LCP | 最大内容绘制 | < 2.5s |
| FID / INP | 首次输入延迟 | < 100ms |
| CLS | 累积布局偏移 | < 0.1 |
| FCP | 首次内容绘制 | < 1.8s |
| TTFB | 首字节时间 | < 800ms |
网络优化
资源加载
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//api.example.com">
<!-- 预连接 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<!-- 预加载关键资源 -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
<link rel="preload" href="/js/app.js" as="script">
<!-- 懒加载图片 -->
<img src="image.jpg" loading="lazy" alt="图片">
<!-- 异步加载 JS -->
<script src="/js/analytics.js" async></script>
<script src="/js/non-critical.js" defer></script>Nginx 配置优化
# 开启 gzip 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1024;
gzip_comp_level 6;
# 缓存策略
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache";
}资源体积优化
构建优化
// Vite 配置优化
// vite.config.ts
export default defineConfig({
build: {
// 代码分割
rollupOptions: {
output: {
manualChunks: {
'vendor': ['vue', 'vue-router', 'pinia'],
'ui': ['element-plus'],
'charts': ['echarts']
}
}
},
// 压缩
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // 移除 console
drop_debugger: true
}
},
// CSS 代码分割
cssCodeSplit: true
}
})图片优化
<!-- 使用 WebP 格式 -->
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.jpg" type="image/jpeg">
<img src="image.jpg" alt="图片" loading="lazy">
</picture>
<!-- 响应式图片 -->
<img srcset="small.jpg 480w, medium.jpg 800w, large.jpg 1200w"
sizes="(max-width: 600px) 480px, (max-width: 1000px) 800w, 1200px"
src="medium.jpg" alt="响应式图片">Vue 性能优化
组件优化
<!-- 懒加载组件 -->
<script setup>
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent(() =>
import('@/components/HeavyChart.vue')
)
</script>
<template>
<HeavyChart v-if="showChart" />
</template>列表优化
<script setup>
import { ref, computed } from 'vue'
// 虚拟列表 — 只渲染可见区域
// 推荐使用 vue-virtual-scroller
// 分页加载
const items = ref([])
const page = ref(1)
async function loadMore() {
const newItems = await fetch(`/api/items?page=${page.value}`)
items.value.push(...newItems)
page.value++
}
</script>状态管理优化
// 避免存储大量数据
// 差
const store = defineStore('main', () => {
const allProducts = ref<Product[]>([]) // 不要一次加载全部
})
// 好:分页加载,只存当前页
const store = defineStore('products', () => {
const currentPage = ref<Product[]>([])
const totalCount = ref(0)
async function loadPage(page: number, size: number) {
const data = await api.getProducts(page, size)
currentPage.value = data.items
totalCount.value = data.total
}
})运行时优化
requestAnimationFrame 优化
// 使用 rAF 节流高频事件
function useScrollPosition() {
const scrollY = ref(0)
let ticking = false
function onScroll() {
if (!ticking) {
requestAnimationFrame(() => {
scrollY.value = window.scrollY
ticking = false
})
ticking = true
}
}
onMounted(() => window.addEventListener('scroll', onScroll, { passive: true }))
onUnmounted(() => window.removeEventListener('scroll', onScroll))
return scrollY
}CSS 性能优化
/* 1. 使用 transform 代替 top/left 做动画(避免触发重排) */
.animated {
/* 差 — 触发重排 */
/* transition: top 0.3s, left 0.3s; */
/* 好 — 只触发合成 */
transition: transform 0.3s, opacity 0.3s;
will-change: transform;
}
/* 2. 避免层叠上下文爆炸 */
.card {
/* 谨慎使用 — 每个都会创建新的合成层 */
/* will-change: transform; */
}
/* 3. contain 属性 — 告诉浏览器元素的渲染范围 */
.sidebar {
contain: layout style paint; /* 浏览器可以独立渲染此区域 */
}
.list-item {
contain: content; /* 相当于 layout style paint */
}
/* 4. 减少选择器复杂度 */
/* 差 — 浏览器从右向左匹配 */
.nav ul li a span.title { color: #333; }
/* 好 — 选择器简单直接 */
.nav-title { color: #333; }
/* 5. 避免昂贵的属性 */
/* 避免 box-shadow、filter: blur() 在大面积元素上使用 */
/* 避免 * 通配符选择器 */内存泄漏检测与预防
// 常见内存泄漏场景:
// 1. 事件监听未清理
const handler = () => { /* ... */ }
window.addEventListener('resize', handler)
// 必须在 onUnmounted 中移除
onUnmounted(() => window.removeEventListener('resize', handler))
// 2. 定时器未清理
const timer = setInterval(() => { /* ... */ }, 1000)
onUnmounted(() => clearInterval(timer))
// 3. 闭包引用
function createLeakyComponent() {
const hugeData = new Array(1000000).fill('data')
return {
getData: () => hugeData.slice(0, 10),
// hugeData 永远不会被回收
}
}
// 4. DOM 引用
const elements = ref<HTMLElement[]>([])
// 如果不再使用,需要手动清空
onUnmounted(() => { elements.value = [] })
// Chrome DevTools 检测内存泄漏:
// 1. 打开 Memory 面板
// 2. 执行操作(切换页面等)
// 3. 点击 "Take Heap Snapshot"
// 4. 重复几次操作
// 5. 对比快照,查看 "Detached DOM" 和增长的对象Service Worker 缓存策略
// sw.js — 不同资源的缓存策略
const CACHE_NAME = 'v1'
self.addEventListener('fetch', (event) => {
const { request } = event
const url = new URL(request.url)
// 1. 静态资源 — Cache First
if (url.pathname.match(/\.(js|css|png|jpg|webp|woff2)$/)) {
event.respondWith(
caches.match(request).then(cached => {
return cached || fetch(request).then(response => {
const clone = response.clone()
caches.open(CACHE_NAME).then(cache => cache.put(request, clone))
return response
})
})
)
return
}
// 2. API 请求 — Network First
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(request)
.then(response => {
const clone = response.clone()
caches.open(CACHE_NAME).then(cache => cache.put(request, clone))
return response
})
.catch(() => caches.match(request))
)
return
}
// 3. HTML 页面 — Network First,回退缓存
event.respondWith(
fetch(request)
.catch(() => caches.match(request))
)
})React 性能优化技巧
import { memo, useMemo, useCallback } from 'react'
// 1. memo — 避免不必要的重渲染
const UserCard = memo(function UserCard({ user }: { user: User }) {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)
})
// 2. useMemo — 缓存计算结果
function UserList({ users, filter }: Props) {
const filteredUsers = useMemo(() => {
return users.filter(u => u.name.includes(filter))
}, [users, filter])
return (
<ul>
{filteredUsers.map(user => (
<UserCard key={user.id} user={user} />
))}
</ul>
)
}
// 3. useCallback — 缓存函数引用
function SearchBar({ onSearch }: { onSearch: (keyword: string) => void }) {
const [keyword, setKeyword] = useState('')
const handleSearch = useCallback(() => {
onSearch(keyword)
}, [keyword, onSearch])
return (
<div>
<input value={keyword} onChange={e => setKeyword(e.target.value)} />
<button onClick={handleSearch}>搜索</button>
</div>
)
}
// 4. React.memo 使用自定义比较函数
const ExpensiveList = memo(function ExpensiveList({ items, sortBy }: {
items: Item[]
sortBy: string
}) {
return items.sort(/* ... */).map(/* ... */)
}, (prevProps, nextProps) => {
// 自定义比较 — 只在 items 或 sortBy 变化时重渲染
return prevProps.sortBy === nextProps.sortBy &&
prevProps.items.length === nextProps.items.length
})防抖和节流
// 防抖 — 搜索输入
function useDebounce<T extends (...args: any[]) => any>(fn: T, delay: number) {
let timeout: ReturnType<typeof setTimeout>
return ((...args: Parameters<T>) => {
clearTimeout(timeout)
timeout = setTimeout(() => fn(...args), delay)
}) as T
}
// 节流 — 滚动事件
function useThrottle<T extends (...args: any[]) => any>(fn: T, delay: number) {
let last = 0
return ((...args: Parameters<T>) => {
const now = Date.now()
if (now - last >= delay) {
last = now
fn(...args)
}
}) as T
}大数据计算优化
// computed 缓存
const filteredList = computed(() => {
// 只在依赖变化时重新计算
return list.value.filter(item =>
item.name.includes(keyword.value)
)
})
// Web Worker — 复杂计算不阻塞 UI
// worker.ts
self.onmessage = (e) => {
const result = heavyComputation(e.data)
self.postMessage(result)
}
// 主线程
const worker = new Worker('/worker.ts', { type: 'module' })
worker.postMessage(data)
worker.onmessage = (e) => {
result.value = e.data
}API 调用优化
请求合并与缓存
// 请求取消 — 避免竞态
let controller: AbortController | null = null
async function search(keyword: string) {
controller?.abort()
controller = new AbortController()
const response = await fetch(`/api/search?q=${keyword}`, {
signal: controller.signal
})
return response.json()
}
// 数据预取 — 鼠标悬停时预加载
function prefetchUser(id: number) {
fetch(`/api/users/${id}`).then(r => r.json())
}性能分析工具
| 工具 | 用途 |
|---|---|
| Lighthouse | 综合性能评分 |
| Chrome DevTools Performance | 运行时性能分析 |
| Network 面板 | 网络请求分析 |
| webpack-bundle-analyzer | 包体积分析 |
| Vue DevTools | Vue 组件性能 |
使用 Chrome DevTools Performance 分析
Performance 面板使用步骤:
1. 打开 Chrome DevTools > Performance
2. 点击 Record(录制)
3. 执行需要分析的操作(滚动、点击等)
4. 停止录制
5. 分析结果:
- Main 线程:查看长任务(红色标记)
- FPS:帧率是否稳定在 60fps
- Layout/Paint:重排重绘的频率
- Network:资源加载时序
关注指标:
- Long Tasks (> 50ms) — 阻塞主线程的任务
- Layout Shift — 布局偏移
- CPU Usage — CPU 使用率Lighthouse CI 自动化
// package.json — Lighthouse CI 集成
{
"scripts": {
"lighthouse": "lighthouse http://localhost:3000 --output html --output-path ./report.html",
"lighthouse:ci": "lhci autorun"
},
"devDependencies": {
"@lhci/cli": "^0.13.0"
}
}
// lighthouserc.js — Lighthouse CI 配置
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000/', 'http://localhost:3000/dashboard'],
numberOfRuns: 3,
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.85 }],
'categories:accessibility': ['error', { minScore: 0.9 }],
'first-contentful-paint': ['error', { maxNumericValue: 1800 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
}
},
upload: {
target: 'filesystem',
outputDir: './lhci-reports',
}
}
}前端性能监控 SDK
// utils/performance.ts — 轻量级性能监控
class PerformanceMonitor {
private metrics: Record<string, number> = {}
// 测量页面加载指标
measurePageLoad() {
if (typeof window === 'undefined') return
window.addEventListener('load', () => {
setTimeout(() => {
const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
this.metrics = {
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
ttfb: timing.responseStart - timing.requestStart,
download: timing.responseEnd - timing.responseStart,
domParse: timing.domInteractive - timing.responseEnd,
domReady: timing.domContentLoadedEventEnd - timing.fetchStart,
fullLoad: timing.loadEventEnd - timing.fetchStart,
}
this.report()
}, 0)
})
}
// 测量自定义任务耗时
measure(name: string, fn: () => void | Promise<void>) {
const start = performance.now()
const result = fn()
const end = () => {
const duration = performance.now() - start
this.metrics[name] = duration
// 上报慢操作
if (duration > 1000) {
console.warn(`[Performance] ${name} 耗时 ${duration.toFixed(2)}ms`)
}
}
if (result instanceof Promise) {
return result.then(end)
}
end()
return result
}
// 测量资源加载
measureResource(url: string) {
const entry = performance.getEntriesByName(url)[0] as PerformanceResourceTiming
if (entry) {
return {
dns: entry.domainLookupEnd - entry.domainLookupStart,
tcp: entry.connectEnd - entry.connectStart,
ttfb: entry.responseStart - entry.requestStart,
duration: entry.duration,
size: entry.transferSize,
}
}
return null
}
// 上报指标
private report() {
console.log('[Performance Metrics]', this.metrics)
// 上报到监控平台
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/metrics/performance', JSON.stringify(this.metrics))
}
}
}
export const perfMonitor = new PerformanceMonitor()优化检查清单
| 项目 | 措施 | 影响 |
|---|---|---|
| 压缩 | Gzip/Brotli | 体积减少 60-80% |
| 缓存 | 静态资源长期缓存 | 减少重复请求 |
| 懒加载 | 图片和组件懒加载 | 加快首屏 |
| 代码分割 | 按路由分割 | 减小初始包 |
| CDN | 静态资源上 CDN | 降低延迟 |
| 图片优化 | WebP + 响应式 | 体积减少 30-50% |
优点
缺点
总结
前端性能优化的核心:减少网络请求、压缩资源体积、优化渲染路径。用 Lighthouse 定位问题,用 DevTools 分析瓶颈。静态资源上 CDN + 缓存,图片用 WebP,代码分割按需加载,长列表用虚拟滚动。性能优化不是一次性的,需要持续监控和改进。
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
适用场景
- 当你准备把《前端性能优化》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《前端性能优化》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《前端性能优化》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《前端性能优化》最大的收益和代价分别是什么?
