前端性能直接影响用户体验和业务指标。研究表明,页面加载时间每增加 1 秒,转化率下降 7%,跳出率增加 30%。性能优化不是玄学,而是有明确方法论的工程实践。本文总结了 15 条最实用的前端性能优化技巧,从资源加载到运行时,覆盖了绝大多数场景。
一、资源加载优化
1. 压缩与合并资源
这是最基础也是性价比最高的优化。HTML、CSS、JavaScript 都应该在构建时进行压缩,移除空格、注释和冗余字符。
// webpack 生产模式自动压缩
// mode: 'production'
// Vite 开箱即用,无需配置
// 构建时自动压缩 JS 和 CSS
// 额外配置:移除 console 和 debugger
// vite.config.js
export default {
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
}
2. 图片优化
图片通常占页面总大小的 60% 以上,优化图片往往能带来最大的性能收益。
- 使用现代格式:WebP 比 JPEG 小 25-35%,AVIF 更小(但兼容性稍差)
- 响应式图片:用 srcset 和 sizes 提供不同尺寸的图片
- 懒加载:首屏以外的图片延迟加载
- 渐进式加载:先显示低质量占位图,再逐步清晰
<!-- 响应式图片 + 现代格式 + 懒加载 -->
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg"
srcset="image-300.jpg 300w, image-600.jpg 600w, image-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, 50vw"
loading="lazy"
decoding="async"
alt="描述"
width="800"
height="600">
</picture>
别忘了设置 width 和 height 属性,浏览器可以在图片加载前就预留空间,避免布局偏移(CLS)。
3. 代码分割与按需加载
不要把所有代码打包成一个巨大的 JS 文件。利用 ES Module 的动态导入,按路由或功能拆分代码块,用户只加载当前页面需要的代码。
// React 路由级懒加载
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
);
}
// 交互触发时加载
function handleClick() {
import('./heavy-module').then(mod => {
mod.doSomething();
});
}
4. 善用浏览器缓存
合理的缓存策略可以让二次访问几乎秒开。核心原则:静态资源(JS/CSS/图片)设置长期缓存,HTML 不缓存或短缓存。
# Nginx 配置示例
# 静态资源:一年缓存,内容更新通过文件名 hash 实现
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML:不缓存,每次都获取最新版本
location ~* \.html$ {
add_header Cache-Control "no-cache";
}
现代构建工具(Vite、Webpack)会自动给文件名加 hash,内容变了文件名就变,缓存自然失效。这就是所谓的"指纹缓存策略"。
5. 使用 CDN
CDN(内容分发网络)把资源缓存到全球各地的边缘节点,用户从最近的节点获取资源,延迟可以降低几十到几百毫秒。静态资源、图片、视频都应该走 CDN。常用的 CDN 服务有 Cloudflare、AWS CloudFront、阿里云 CDN 等。
二、渲染性能优化
6. 关键 CSS 内联
CSS 是阻塞渲染的资源——浏览器必须等 CSS 加载解析完才能绘制页面。把首屏需要的关键 CSS 内联在 HTML 中,其余 CSS 异步加载,可以显著提升首屏渲染速度。
<head>
<style>
/* 首屏关键样式内联 */
body { margin: 0; font-family: sans-serif; }
.header { background: #fff; padding: 16px; }
.hero { text-align: center; padding: 40px 0; }
</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles.css"></noscript>
</head>
7. 减少重排重绘
DOM 操作会触发重排(reflow)和重绘(repaint),这是前端性能的常见瓶颈。
// ❌ 不好:每次循环都触发重排
for (let i = 0; i < items.length; i++) {
element.innerHTML += '<div>' + items[i] + '</div>';
}
// ✅ 好:批量更新,只触发一次重排
let html = '';
for (let i = 0; i < items.length; i++) {
html += '<div>' + items[i] + '</div>';
}
element.innerHTML = html;
// 或者用 DocumentFragment
const fragment = document.createDocumentFragment();
items.forEach(item => {
fragment.appendChild(document.createElement('div'));
});
element.appendChild(fragment);
8. 虚拟列表(长列表优化)
当列表有几千甚至几万条数据时,渲染所有 DOM 节点会导致页面卡顿。虚拟列表只渲染可视区域内的元素,大大减少 DOM 节点数量。
// 虚拟列表核心思路
function VirtualList({ items, itemHeight, containerHeight }) {
const [scrollTop, setScrollTop] = useState(0);
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 1,
items.length
);
const visibleItems = items.slice(startIndex, endIndex);
const offsetY = startIndex * itemHeight;
return (
<div style={{ height: containerHeight, overflow: 'auto' }}
onScroll={e => setScrollTop(e.target.scrollTop)}>
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map(item => (
<div style={{ height: itemHeight }}>{item.content}</div>
))}
</div>
</div>
</div>
);
}
实际项目中推荐使用成熟的虚拟列表库:react-virtualized、vue-virtual-scroller 等。
三、JavaScript 运行时优化
9. 防抖和节流
频繁触发的事件(scroll、resize、input)如果每次都执行复杂操作,会导致页面卡顿。防抖(debounce)和节流(throttle)是两大法宝。
// 防抖:事件触发后等待 n 秒再执行,期间再次触发则重新计时
function debounce(fn, delay = 300) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 节流:n 秒内只执行一次
function throttle(fn, interval = 300) {
let last = 0;
return function(...args) {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn.apply(this, args);
}
};
}
// 应用
const handleSearch = debounce(query => fetchData(query), 500);
const handleScroll = throttle(() => updatePosition(), 100);
10. Web Worker:把计算移出主线程
JavaScript 是单线程的,大量计算会阻塞 UI 渲染。Web Worker 可以在后台线程运行脚本,适合处理大数据、复杂计算等任务。
// 主线程
const worker = new Worker('/worker.js');
worker.postMessage(largeData);
worker.onmessage = e => {
console.log('计算完成:', e.data);
};
// worker.js
self.onmessage = e => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
11. 合理使用 requestAnimationFrame
对于动画和 DOM 更新,使用 requestAnimationFrame 而不是 setTimeout,可以确保动画与浏览器刷新率同步,避免掉帧。
// 平滑滚动到顶部
function smoothScrollToTop() {
const scrollTop = document.documentElement.scrollTop;
if (scrollTop > 0) {
window.requestAnimationFrame(smoothScrollToTop);
window.scrollTo(0, scrollTop - scrollTop / 8);
}
}
四、网络与协议优化
12. 使用 HTTP/2 或 HTTP/3
HTTP/2 支持多路复用、头部压缩、服务器推送等特性,相比 HTTP/1.1 有显著的性能提升。HTTP/3 基于 QUIC 协议,性能更好。确保你的服务器启用了 HTTP/2(现在大部分 CDN 和托管平台默认支持)。
13. 预加载关键资源
<!-- 预加载关键字体 -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<!-- 预加载首屏图片 -->
<link rel="preload" href="/hero-image.webp" as="image">
<!-- 预连接第三方域名(提前建立 TCP 连接) -->
<link rel="preconnect" href="https://api.example.com">
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com">
14. 字体优化
字体加载会导致文字闪烁(FOIT)或布局偏移(FOUT)。使用 font-display: swap 先用系统字体显示,自定义字体加载完成后再替换。
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap; /* 关键 */
}
15. 性能监控与度量
无法度量就无法优化。关注 Web Vitals 三大核心指标:
- LCP(Largest Contentful Paint):最大内容绘制,应 < 2.5 秒
- FID(First Input Delay):首次输入延迟,应 < 100 毫秒(将被 INP 替代)
- CLS(Cumulative Layout Shift):累积布局偏移,应 < 0.1
// 简单的性能监控
if ('performance' in window) {
window.addEventListener('load', () => {
const timing = performance.timing;
const loadTime = timing.loadEventEnd - timing.navigationStart;
const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
console.log(`页面加载: ${loadTime}ms, DOM就绪: ${domReady}ms`);
});
}
推荐工具:Lighthouse(Chrome DevTools 内置)做全面性能审计;WebPageTest 测试不同地区和网络环境下的表现;DevToolHub 的 图片压缩工具可以快速优化图片大小。
五、构建与工程优化
16. Tree Shaking:移除无用代码
Tree shaking 是 ES Module 带来的福利——打包时自动移除没有被使用的代码,减少包体积。
// ✅ 好:按需导入,tree shaking 会移除没用到的方法
import { debounce, throttle } from 'lodash-es';
// ❌ 不好:全量导入,所有代码都会被打包
import _ from 'lodash';
// 确认配置
// vite.config.js / rollup 默认支持 tree shaking
// webpack 需要设置 mode: 'production' 且使用 ES Module
// package.json 中标记副作用
// {
// "sideEffects": false // 没有副作用
// "sideEffects": ["*.css"] // 只有 CSS 文件有副作用
// }
注意:只有 ES Module(import/export)才能 tree shaking,CommonJS(require/module.exports)不行。所以尽量使用 ES Module 版本的库,比如 lodash-es 而不是 lodash。
17. 合理选择第三方库
第三方库是包体积膨胀的重灾区。引入库之前先考虑:
- 这个功能自己实现需要多少代码?有时 20 行就能搞定的事,别引入一个 50KB 的库
- 有没有更轻量的替代品?比如 dayjs(2KB)替代 moment(200KB+)
- 能按需加载吗?比如 ECharts 可以只引入需要的图表类型
- 是否可以动态导入?比如只有用户点了编辑按钮才加载富文本编辑器
使用 bundlephobia.com 可以查询任意 npm 包的体积,决定是否值得引入。BundleAnalyzer 则可以可视化地分析你项目的包组成,找出哪些库占比最大。
18. 预取与预加载策略
<!-- prefetch:预判用户接下来可能需要的资源,闲时加载 -->
<link rel="prefetch" href="/next-page.js" as="script">
<!-- preload:当前页面马上需要的关键资源,高优先级加载 -->
<link rel="preload" href="/critical-font.woff2" as="font" crossorigin>
<!-- prerender:预判用户下一个页面,提前渲染整个页面 -->
<link rel="prerender" href="/next-page.html">
策略原则:当前页面关键资源用 preload,下一个页面可能需要的资源用 prefetch。不要滥用 prefetch,否则会浪费带宽。可以根据用户行为来触发——比如用户鼠标悬停在链接上时,才开始 prefetch 目标页面的资源。
六、感知性能优化
19. 骨架屏与加载状态
感知性能是用户主观感受到的速度。即使真实加载时间一样,有骨架屏的页面看起来加载更快,因为用户知道内容正在加载,而不是卡在那里。
/* 骨架屏闪烁动画 */
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
20. 乐观更新
用户点击按钮后,先在界面上显示成功状态,同时在后台发请求。如果请求失败了再回退。这种"乐观更新"策略可以让用户感觉操作很流畅。
async function handleLike(postId) {
// 立即更新 UI
setLiked(true);
setCount(c => c + 1);
try {
await api.likePost(postId);
} catch (e) {
// 失败了回退
setLiked(false);
setCount(c => c - 1);
showToast('操作失败,请重试');
}
}
乐观更新适合那些成功率高、失败代价小的操作(点赞、收藏、关注)。对于删除、支付等不可逆操作,还是应该老实等服务端返回结果。
七、自动化性能监控
性能优化不是一次性的工作,而是持续的过程。建立性能监控机制,才能防止性能回退。
- CI/CD 集成:每次构建后自动跑 Lighthouse,分数低于阈值阻止合并
- 真实用户监控(RUM):收集真实用户的性能数据,用统计方法分析
- 合成监控:定期从各地节点跑性能测试,主动发现问题
- 告警机制:关键指标恶化时自动告警
常用工具:Web Vitals(Google 官方)、Sentry Performance、Datadog RUM、New Relic。自建方案也不难——用 performance API + PerformanceObserver 采集数据,上报到自己的服务端即可。
最后记住:性能优化是一场没有终点的马拉松。今天优化到了 2 秒,明年功能越堆越多可能又变成 5 秒。建立性能文化,把性能当做日常开发的考量因素,而不是上线前的临时抱佛脚。