页面卡? 白屏久? 还是滑动掉帧? 性能优化从来不是玄学,先把脉,再对症下药总是对的。
先分析:别在黑暗里改代码
没有数据的优化,多半是感动自己。那怎么分析呢?
- Chrome DevTools:Network 看体积与瀑布流,Performance 看主线程 长任务;
- Lighthouse:一键给 FCP、LCP、INP 等打分,数据总是最客观的;
- 打包分析:
webpack-bundle-analyzer等,看谁把 bundle 撑胖了。
本地快速跑一轮 Lighthouse(需已装 Chrome 与 @lhci/cli):
npm i @lhci/cli
npx lighthouse https://你的站点 --only-categories=performance --output html --output-path ./report.html
你会得到report.html(以百度为例,报告没截全):
核心指标心里要有数:首屏要快(少下载、少阻塞)、交互要顺(少长任务、少布局抖动)。
简单来说,前端性能优化 === 少下载、少解析、少渲染、少阻塞。
整体优化思路
从网络 → 渲染 → JS 运行 → 工程化构建,我将在下文逐步展开
一、网络层:疗效最显著
1. 减小资源体积
- 压缩 JS/CSS/HTML(Terser、Gzip)
- 图片压缩
- 去除无用代码:Tree Shaking
2. 减少请求数量
- 资源合并(如 JS Bundle 合并)
- 雪碧图(小图标)
- 图片懒加载(Intersection Observer)
<img src="hero.jpg" alt="首屏" fetchpriority="high" />
<img src="placeholder.png" data-src="below-fold.jpg" alt="非首屏" loading="lazy" />
配合 Intersection Observer:
IntersectionObserver 是浏览器提供的 API,用于异步监听元素与视口的交叉状态,性能高效,常用于图片懒加载、无限滚动、曝光统计等场景
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (!e.isIntersecting) return;
const img = e.target;
img.src = img.dataset.src;
io.unobserve(img);
});
});
document.querySelectorAll('img[data-src]').forEach((img) => io.observe(img));
3. 加强缓存策略
- 强缓存:Cache-Control: max-age
- 协商缓存:Last-Modified / ETag
- 哈希文件名实现增量更新
4. 使用 CDN
- 静态资源(JS/CSS/ 图片 / 字体)放 CDN
- 多地域加速,下载距离更短
5. 预连接 / 预加载
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin />
二、渲染层:首屏体验的「面子工程」也是真工程
1. 减少 DOM/CSS 体积
- 减少 DOM 层级(最好 ≤ 6 层)
- 减少无用 CSS:清除冗余样式(PurgeCSS)
- 关键 CSS 内联(首屏直接可用)
<head>
<style>
/* 只放首屏必备:布局、字体、主色 */
body { margin: 0; font-family: system-ui, sans-serif; }
.hero { min-height: 40vh; }
</style>
<link rel="stylesheet" href="/rest.css" />
</head>
2. 避免渲染阻塞
- CSS 放在
- JS 放在 body 底部,或使用 async/defer
- 避免大量同步 JS 阻碍解析
3. 减少重绘重排(Layout/Paint)
- 避免频繁操作样式
- 批量更新 DOM
const frag = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = `项 ${i}`;
frag.appendChild(li);
}
listEl.appendChild(frag); // 一次插入,减少多次重排
- 使用 requestAnimationFrame
requestAnimationFrame(() => {
box.style.transform = `translateX(${x}px)`; // 优先 transform / opacity,利于合成层
});
- 动画优先使用 transform/opacity(可直接 GPU 加速)
4. 首屏加速
- 骨架屏、Skeleton
- 服务端渲染 SSR / 静态站点 SSG
- 先渲染核心内容,再加载非首屏资源
三、JS 运行层:主线程是单行道,别堵死
1. 避免主线程阻塞
- 复杂计算 → Web Worker
// main.js
const w = new Worker('/worker.js');
w.postMessage({ n: 1e8 });
w.onmessage = (e) => console.log('结果', e.data);
// worker.js
self.onmessage = ({ data }) => {
let s = 0;
for (let i = 0; i < data.n; i++) s += i % 7;
self.postMessage(s);
};
- 避免长任务(≤ 50ms)
// 长任务让出主线程(简单切片)
async function runChunks(items, chunkSize, processItem) {
for (let i = 0; i < items.length; i += chunkSize) {
const slice = items.slice(i, i + chunkSize);
slice.forEach(processItem);
await new Promise((r) => setTimeout(r, 0)); // 让浏览器喘口气,避免长时间卡死
}
}
- 异步化:使用 Promise/async
2. 优化交互逻辑
- 防抖节流(scroll/resize/input)
// 防抖
function debounce(fn, wait = 300) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, args), wait);
};
}
input.addEventListener('input', debounce((e) => console.log(e.target.value), 300));
// 节流
function throttle(fn, wait = 200) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last < wait) return;
last = now;
fn.apply(this, args);
};
}
window.addEventListener('scroll', throttle(() => console.log(window.scrollY), 200));
- 事件委托减少事件绑定
document.getElementById('list').addEventListener('click', (e) => {
const btn = e.target.closest('[data-action="del"]');
if (!btn) return;
const id = btn.dataset.id;
console.log('删除', id);
});
- 虚拟列表(Virtual List)渲染大数据
3. 减少内存泄漏
- 及时清除事件监听
import { useEffect } from 'react';
// 定时器与监听在卸载时清掉
useEffect(() => {
const t = setInterval(() => {}, 1000);
const onVis = () => {};
document.addEventListener('visibilitychange', onVis);
return () => {
clearInterval(t);
document.removeEventListener('visibilitychange', onVis);
};
}, []);
- 避免闭包意外引用
- 组件卸载清理 state / 定时器
四、工程化:长期收益最高的一刀
1. 代码分割(Code Splitting)
- 按模块分割,避免超大 bundle(splitChunks)
2. 第三方库优化
- 按需引入(如 lodash-es)
- 外部化 CDN 加载大型库(React/Three.js)
3. 构建产物优化
- 移除 console
- 压缩混淆(Terser)
- 模块规范使用 ES Module(支持 Tree Shaking)
4. 分析与监控
- 使用 Webpack-Bundle-Analyzer / speed-measure-webpack-plugin等先分析
- Lighthouse 自动化检测
- 监控 Core Web Vitals(FCP/LCP/INP)
- FCP —— First Contentful Paint 首次内容绘制
- 含义:浏览器第一次渲染出内容(文本、图片、非空白的 canvas 等)的时间点。
- 代表:页面开始有内容显示,不再是白屏。
- 越快越好。
- LCP —— Largest Contentful Paint 最大内容绘制(最重要的加载指标)
- 含义:页面最大的那块内容(通常是标题图、首屏标题)渲染完成的时间。
- 代表:用户感知到页面加载完成的速度。
- 优秀标准:≤ 2.5s
- 影响因素:图片大小、服务器响应速度、JS 阻塞
- INP —— Interaction to Next Paint 交互到下一次绘制(Google 新核心体验指标)
- 含义:从用户点击 / 输入 → 到页面真正响应并更新画面的延迟。
- 代表:页面交互流畅度、是否卡顿
- 优秀标准:≤ 200ms
- INP 高 = 点了没反应、页面卡顿、输入延迟
更进一步可参考
# 手把手系列之——Webpack 优化技巧
收尾
整体思路可以从 网络 => 渲染 => JS执行 =>工程化四部分进行,多上手试试,形成自己的优化体系就会轻松很多啦。
切记,先分析再动手,时刻关注数据(即量化指标),拒绝负优化。