前端性能优化分两条线:让首屏更快加载(加载性能),让页面运行更流畅(运行时性能)。前者解决"白屏等待",后者解决"操作卡顿"。本文从资源压缩、懒加载到虚拟列表、防抖节流,配合 Web Vitals 指标,给你一套可落地的优化手册。
一、性能优化的衡量标准:Web Vitals
优化前先会测量。Google 提出的 Core Web Vitals 是当前主流指标:
- LCP(Largest Contentful Paint):最大内容渲染时间,<2.5s 为佳,衡量首屏加载体验。
- FID/INP(Interaction to Next Paint):交互响应延迟,<200ms 为佳,衡量交互流畅度。
- CLS(Cumulative Layout Shift):累计布局偏移,<0.1 为佳,衡量视觉稳定性。
// 用 web-vitals 库采集真实用户指标
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
// 或用 PerformanceObserver 监听 LCP
new PerformanceObserver((list) => {
const entries = list.getEntries();
const last = entries[entries.length - 1];
console.log('LCP:', last.startTime, last.element);
}).observe({ type: 'largest-contentful-paint', buffered: true });
二、资源压缩:减小体积是第一步
传输体积直接决定加载时间。压缩策略贯穿构建全流程:
// vite.config.js / webpack.config.js 关键项
export default {
build: {
minify: 'terser', // JS 压缩
cssCodeSplit: true, // CSS 按需拆分
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'], // 第三方拆包
}
}
}
}
}
- JS/CSS 压缩:terser、cssnano 去空格、删注释、缩短变量。
- 图片压缩:WebP/AVIF 比 JPEG 小 30%-50%,体积显著下降。
- Gzip/Brotli:服务器开启压缩,文本资源再减 70%。
- Tree Shaking:ES Module 静态分析,剔除未用代码。
三、懒加载:按需加载,延后开销
首屏只加载首屏需要的资源,其余延后。路由懒加载与图片懒加载是两大杀器。
// 路由懒加载:按需拆分 chunk
const routes = [
{ path: '/', component: () => import('./views/Home.vue') },
{ path: '/about', component: () => import('./views/About.vue') },
];
// 图片懒加载:原生 loading 属性
<img src="hero.jpg" loading="lazy" alt="首屏图"> <!-- 首屏用 eager -->
<img src="news-1.jpg" loading="lazy" alt="资讯图"> <!-- 视口外延迟加载 -->
// IntersectionObserver 自定义懒加载
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.src = e.target.dataset.src;
io.unobserve(e.target);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));
四、虚拟列表:长列表渲染神器
渲染上万条数据时,DOM 节点过多是卡顿元凶。虚拟列表只渲染可视区域的几十条,滚动时动态替换,DOM 数量恒定。
// 虚拟列表核心思路
function VirtualList({ items, itemHeight, viewportH }) {
const [scrollTop, setScrollTop] = useState(0);
const startIdx = Math.floor(scrollTop / itemHeight);
const visibleCount = Math.ceil(viewportH / itemHeight) + 2; // 缓冲
const endIdx = startIdx + visibleCount;
const visible = items.slice(startIdx, endIdx);
return (
<div style={{ height: viewportH, overflow: 'auto' }}
onScroll={e => setScrollTop(e.target.scrollTop)}>
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
{visible.map((it, i) => (
<div key={startIdx + i}
style={{ position: 'absolute', top: (startIdx + i) * itemHeight, height: itemHeight }}>
{it.text}
</div>
))}
</div>
</div>
);
}
五、防抖与节流:控制高频触发
搜索框输入、窗口 resize、滚动事件会高频触发回调,造成性能浪费。防抖节流是经典解法。
// 防抖:连续触发只执行最后一次(适合搜索联想)
function debounce(fn, delay = 300) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
input.addEventListener('input', debounce(fetchSuggest, 300));
// 节流:固定间隔最多执行一次(适合滚动、resize)
function throttle(fn, interval = 200) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn.apply(this, args);
}
};
}
window.addEventListener('scroll', throttle(onScroll, 200));
区别:防抖"等停下来再执行",节流"按固定频率执行"。搜索用防抖,滚动用节流。
六、运行时优化:减少重排重绘
- 批量 DOM 操作:用 DocumentFragment 或一次性 className 切换,避免多次触发布局。
- 避免强制同步布局:不要在修改样式后立即读取 offsetTop/clientWidth,会触发强制重排。
- requestAnimationFrame:动画用它调度,对齐刷新率,避免丢帧。
- transform/opacity 做动画:它们走合成层,不触发重排。
// 差:逐个添加节点,触发多次重排
data.forEach(item => list.appendChild(createLi(item)));
// 好:DocumentFragment 一次性插入
const frag = document.createDocumentFragment();
data.forEach(item => frag.appendChild(createLi(item)));
list.appendChild(frag);
// 动画用 transform,硬件加速
.box { transition: transform .3s; }
.box.move { transform: translateX(100px); } /* 而非 left: 100px */
七、缓存策略:让重复访问秒开
合理的缓存能让二次访问近乎瞬开。配合强缓存与协商缓存分层处理。
# 静态资源:带 hash 文件名,强缓存一年
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType text/css "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
</IfModule>
// Service Worker 离线缓存(PWA)
const CACHE = 'mzph-v1';
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(CACHE).then(c => c.addAll(['/','/app.js'])));
});
self.addEventListener('fetch', (e) => {
e.respondWith(caches.match(e.request).then(r => r || fetch(e.request)));
});
性能优化没有银弹,核心是"测量—定位—优化—验证"的闭环。先用 Lighthouse 与 Web Vitals 找到瓶颈,再对症下药:首屏慢从资源体积与加载策略入手,交互卡从渲染与事件频率入手。把这些手段沉淀成团队的性能检查清单,性能才会持续可控。