性能优化
发表于:2026-07-29
字数统计:6000 字
预计阅读21分钟
涵盖加载性能(LCP/FID/CLS)、IntersectionObserver 懒加载、虚拟滚动、Web Vitals、图片优化、CDN、缓存策略、防抖节流、代码分割。
一、核心性能指标(Web Vitals)
1. Core Web Vitals
| 指标 | 含义 | 良好阈值 |
|---|---|---|
| LCP(Largest Contentful Paint) | 最大内容渲染时间 | < 2.5s |
| FID / INP(First Input Delay / Interaction to Next Paint) | 首次输入响应 / 交互到下次渲染 | < 100ms |
| CLS(Cumulative Layout Shift) | 累积布局偏移 | < 0.1 |
2. 其他指标
Plain
FP (First Paint) 首次绘制(任何像素)
FCP (First Contentful Paint) 首次内容渲染
LCP (Largest Contentful Paint) 最大内容渲染(FID 升级为 INP)
FID (First Input Delay) 首次输入延迟(旧)
INP (Interaction to Next Paint) 交互到下次渲染(新)
TTI (Time to Interactive) 可交互时间
TBT (Total Blocking Time) 总阻塞时间
CLS (Cumulative Layout Shift) 累积布局偏移
SI (Speed Index) 速度指数3. 测量工具
Plain
- Chrome DevTools → Performance 面板
- Lighthouse(Performance、Accessibility 等审计)
- Web Vitals Chrome 插件
- PerformanceObserver API(生产监控)
- PageSpeed Insights(在线)
- WebPageTest(在线)二、加载性能优化
1. 资源加载策略
HTML
<!-- preload:关键资源,必须立即加载 -->
<link rel="preload" href="/critical.css" as="style" />
<link rel="preload" href="/main.js" as="script" />
<link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin />
<!-- prefetch:可预测的下一个资源,空闲时加载 -->
<link rel="prefetch" href="/next-page.js" />
<!-- preconnect:提前建立连接 -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin />
<!-- dns-prefetch:提前解析 DNS -->
<link rel="dns-prefetch" href="//cdn.example.com" />2. script 加载策略
HTML
<!-- 默认:阻塞解析,按顺序执行 -->
<script src="a.js"></script>
<!-- async:异步下载,下载完成立即执行(执行时仍阻塞) -->
<script src="analytics.js" async></script>
<!-- defer:异步下载,等 DOMContentLoaded 前按顺序执行 -->
<script src="app.js" defer></script>
<!-- type=module:自带 defer 语义 + 严格模式 + CORS -->
<script type="module" src="app.js"></script>详见 DOCTYPE、meta、iframe、async-defer 与 Service Worker。
3. 资源压缩
HTTP
# 启用 Gzip / Brotli(节省 60-80%)
Content-Encoding: gzip
Content-Encoding: br # Brotli(更高效)
# 配置(Nginx)
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_min_length 1000;
# Brotli(Nginx 需 ngx_brotli 模块)
brotli on;
brotli_types text/plain text/css application/json application/javascript;4. 图片优化
HTML
<!-- 1. 现代格式:WebP / AVIF -->
<picture>
<source srcset="img.avif" type="image/avif" />
<source srcset="img.webp" type="image/webp" />
<img src="img.jpg" alt="..." />
</picture>
<!-- 2. 响应式图片 -->
<img srcset="small.jpg 480w, medium.jpg 800w, large.jpg 1200w"
sizes="(max-width: 600px) 480px, (max-width: 1024px) 800px, 1200px"
src="large.jpg"
alt="..." />
<!-- 3. 懒加载 -->
<img src="img.jpg" loading="lazy" alt="..." />
<iframe src="..." loading="lazy"></iframe>
<!-- 4. 图片固定宽高(防 CLS) -->
<img src="img.jpg" width="600" height="400" alt="..." />5. CDN 加速
Plain
CDN 原理:
- 把资源分发到全国 / 全球节点
- 用户从最近的节点获取资源
- 减少回源、降低延迟
常见 CDN:
- 阿里云 / 腾讯云 / 华为云 CDN
- Cloudflare(国外)
- 七牛 / 又拍
- Vercel / Netlify(前端项目部署)
配置:
- 静态资源(JS / CSS / 图片)走 CDN
- API 接口不缓存(除非 GET 公开数据)
- 缓存策略:Cache-Control / ETag / Last-Modified6. 浏览器缓存策略
HTTP
# 强缓存(浏览器不发送请求)
Cache-Control: max-age=31536000, immutable # 1 年,资源 hash 命名
Cache-Control: no-cache # 缓存但每次验证
Cache-Control: no-store # 不缓存
# 协商缓存(浏览器发请求,服务端验证)
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMTJavaScript
// 前端 JS:文件名带 hash,永久缓存
main.abc123.js → Cache-Control: max-age=31536000, immutable三、运行时性能优化
1. IntersectionObserver 懒加载
JavaScript
// 图片懒加载
const lazyImages = document.querySelectorAll('img[data-src]')
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
obs.unobserve(img)
}
})
}, {
root: null, // 默认为视口
rootMargin: '100px', // 提前 100px 触发
threshold: 0.1 // 10% 可见时触发
})
lazyImages.forEach(img => observer.observe(img))JavaScript
// 无限滚动
let page = 1
const sentinel = document.getElementById('sentinel')
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting) {
page++
const data = await fetch(`/api/list?page=${page}`)
renderList(data)
}
})
observer.observe(sentinel)JavaScript
// 替代 scroll 事件(性能更好)
// scroll 事件触发频率高,可能引起重排
// IntersectionObserver 是浏览器原生,性能优化
window.addEventListener('scroll', () => {
// ❌ 高频触发,需要节流
})
const obs = new IntersectionObserver(() => {
// ✅ 元素进入/离开视口时触发
})2. 虚拟滚动
Plain
问题:长列表渲染 10000 行,DOM 节点过多,性能差
方案:只渲染视口内可见的列表项(虚拟列表)
常见实现:
- vue-virtual-scroller
- react-window
- react-virtualized
原理:
- 可视区域固定高度
- 监听滚动,根据 scrollTop 计算应该渲染哪些项
- 用绝对定位 + transform 让列表看起来连续JavaScript
// 简化版虚拟滚动
class VirtualList {
constructor(container, itemHeight = 50, total = 10000) {
this.container = container
this.itemHeight = itemHeight
this.total = total
this.viewHeight = container.clientHeight
// 占位元素(撑起滚动条)
const placeholder = document.createElement('div')
placeholder.style.height = `${total * itemHeight}px`
container.appendChild(placeholder)
container.addEventListener('scroll', () => this.render())
this.render()
}
render() {
const startIdx = Math.floor(this.container.scrollTop / this.itemHeight)
const endIdx = Math.min(this.total, startIdx + Math.ceil(this.viewHeight / this.itemHeight))
// 渲染 startIdx 到 endIdx
// ...
}
}3. 防抖与节流
JavaScript
// 防抖:连续触发只执行最后一次
function debounce(fn, delay) {
let timer = null
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
// 节流:固定时间内只执行一次
function throttle(fn, delay) {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= delay) {
lastTime = now
fn.apply(this, args)
}
}
}
// 适用场景
window.addEventListener('scroll', throttle(() => {
// 滚动处理(节流)
}, 100))
input.addEventListener('input', debounce(() => {
// 搜索(防抖)
}, 300))4. 代码分割(Code Splitting)
JavaScript
// 路由级代码分割
const Home = () => import('./views/Home.vue')
const About = () => import('./views/About.vue')
const router = createRouter({
routes: [
{ path: '/', component: Home }, // 按需加载
{ path: '/about', component: About }
]
})
// 组件级代码分割
const HeavyComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
// 库按需引入
import { Button, Input } from 'element-plus' // 按需(自动 tree-shaking)5. 函数节流/防抖在 Vue 中的应用
JavaScript
// 不要在模板中使用 debounce 结果的函数(每次重渲染都会重新创建)
// ❌
export default {
methods: {
handleScroll: debounce(() => { /* ... */ }, 100)
}
}
// ✅ 用 lodash 的 debounce 或自定义工具
import { debounce } from 'lodash-es'
export default {
created() {
this.handleScroll = debounce(this.handleScroll, 100)
},
beforeUnmount() {
this.handleScroll.cancel()
},
methods: {
handleScroll() { /* ... */ }
}
}四、渲染性能优化
1. 减少重排与重绘
JavaScript
// ❌ 多次 DOM 操作触发多次重排
const el = document.getElementById('box')
el.style.width = '100px'
el.style.height = '100px'
el.style.margin = '10px'
// ✅ 一次性修改
el.style.cssText = 'width: 100px; height: 100px; margin: 10px;'
// 或
el.className = 'new-style'
// ✅ 读写分离(避免布局抖动)
// 浏览器优化:同一帧内的写操作会延迟,读操作会强制 reflow
// 好的做法:先写后读
el.style.width = '100px' // 写
const w = el.clientWidth // 读(强制 layout)
el.style.height = '200px' // 写2. requestAnimationFrame
JavaScript
// 把耗时操作分摊到多个帧
function longTask(tasks) {
let i = 0
function processChunk() {
const chunkSize = 10
while (i < tasks.length && i < chunkSize) {
processTask(tasks[i++])
}
if (i < tasks.length) {
requestAnimationFrame(processChunk)
}
}
processChunk()
}
// 滚动节流(替代 setTimeout)
let ticking = false
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
// 渲染逻辑
ticking = false
})
ticking = true
}
})3. Web Worker 耗时任务
JavaScript
// 主线程
const worker = new Worker('worker.js')
worker.postMessage({ data: largeData })
worker.onmessage = (e) => {
console.log('处理完成:', e.data.result)
}
// worker.js
self.onmessage = (e) => {
const result = heavyComputation(e.data.data)
self.postMessage({ result })
}五、Vue 性能优化
1. 列表渲染
HTML
<!-- ✅ 稳定的 key -->
<div v-for="item in list" :key="item.id">
{{ item.name }}
</div>
<!-- ❌ index 作为 key(在增删改时性能差) -->
<div v-for="(item, index) in list" :key="index">
{{ item.name }}
</div>详见 Vue Diff 算法。
2. v-if vs v-show
HTML
<!-- 频繁切换:v-show -->
<div v-show="isVisible">频繁切换</div>
<!-- 不频繁切换:v-if(销毁重建) -->
<div v-if="isVisible">不频繁切换</div>3. 组件优化
JavaScript
// functional component(无状态)
export default {
functional: true,
render(h, ctx) {
return h('div', ctx.data, ctx.children)
}
}
// keep-alive 缓存
<keep-alive>
<component :is="currentTab" />
</keep-alive>
// 异步组件
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'))
// v-once 只渲染一次
<span v-once>{{ staticContent }}</span>4. computed vs watch vs methods
JavaScript
// computed:有缓存,依赖不变不重新计算(优先)
computed: {
fullName() { return this.firstName + this.lastName }
}
// watch:执行异步或昂贵操作时用
watch: {
searchKeyword(newVal) {
this.fetchResults(newVal)
}
}
// methods:每次调用都执行(无缓存)
methods: {
getFullName() { return this.firstName + this.lastName }
}六、网络协议优化
1. HTTP/2
Plain
HTTP/2 优势(自动启用):
- 多路复用:单连接并发多个请求-响应
- 头部压缩(HPACK)
- 服务器推送(Server Push)
- 流量优先级
HTTP/2 配合:
- 资源分域:尽量用单域名(多路复用才能发挥优势)
- 不要合并文件(HTTP/1.1 才需要的优化)2. HTTP/3
Plain
HTTP/3 基于 QUIC(UDP):
- 解决 TCP 队头阻塞
- 0-RTT 握手
- 连接迁移(切换网络不断连)
启用:现代 CDN 已支持 HTTP/3七、面试高频问答
Q1: Core Web Vitals 有哪些?
答:
- LCP(Largest Contentful Paint):最大内容渲染时间,衡量加载速度。阈值:< 2.5s
- INP(Interaction to Next Paint):交互到下次渲染,衡量交互响应。阈值:< 200ms(2024 年新指标替代 FID)
- CLS(Cumulative Layout Shift):累积布局偏移,衡量视觉稳定性。阈值:< 0.1
优化 LCP:减少资源大小、CDN、SSR、preload 优化 INP:减少主线程阻塞、Web Worker、time slicing 优化 CLS:图片固定宽高、字体 fallback、动态内容预占空间
Q2: 什么是 IntersectionObserver?用来干什么?
答:IntersectionObserver 是浏览器提供的 API,异步观察元素与视口(或父元素)的交叉状态。
优势:
- 浏览器原生,性能比 scroll 事件好
- 异步,不阻塞主线程
- 自动节流,无需自己 throttle
- 可以监听多层(包括 intersection ratio)
常见用途:
- 图片懒加载
- 无限滚动(监听 sentinel 元素)
- 元素曝光埋点
- 自动播放(元素进入视口时播放)
Q3: 列表渲染为什么用 key?用 index 有什么问题?
答:key 是 Vue / React 用作 Diff 算法的标识。
不用 key 或用 index 的问题:
- 列表项增删时,框架无法准确识别"哪个是新加的、哪个是删除的、哪个是移动的"
- 导致不必要的 DOM 操作(重新渲染而非移动)
- 性能差,状态可能错乱
正确做法:用稳定唯一 ID(如 item.id),让框架精准识别。
Q4: 什么是代码分割?怎么做?
答:代码分割是把代码包拆成多个小块,按需加载,减少首屏加载大小。
做法:
- 路由级分割:每个路由一个 chunk(dynamic import)
- 组件级分割:大组件用
defineAsyncComponent异步加载 - 库按需引入:避免 import 全量
- 工具:Webpack
splitChunks、RollupmanualChunks
详见 构建工具。