目录
2.1 DOM 操作优化:从 120ms 到 18ms 的蜕变

class 卑微码农:
def __init__(self):
self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
self.发量 = 100 # 初始发量
self.咖啡因耐受度 = '极限'
def 修Bug(self, bug):
try:
# 试图用玄学解决问题
if bug.严重程度 == '离谱':
print("这一定是环境问题!")
else:
print("让我看看是谁又没写注释...哦,是我自己。")
except Exception as e:
# 如果try块都救不了,那就...
print("重启一下试试?")
self.发量 -= 1 # 每解决一个bug,头发-1
# 实例化一个我
我 = 卑微码农()
一、诊断篇:先找对问题,再谈优化(性能瓶颈定位指南)

1.1 为什么你的页面会卡顿?3 个核心指标说清本质
我踩过最蠢的坑:花一周重构代码,结果页面加载反而慢了 0.8 秒。后来才明白,性能优化不是 "盲目改代码",而是 "精准打靶"—— 先搞懂问题在哪,再动手优化。
现在行业公认的性能衡量标准是 Google 的Core Web Vitals(核心网页指标),它直接对应用户的三大核心体验,也是搜索引擎排名的重要依据:
| 指标名称 | 英文缩写 | 衡量维度 | 达标阈值 | 不达标后果 |
|---|---|---|---|---|
| 最大内容绘制 | LCP | 首屏加载速度 | ≤2.5 秒 | 页面每慢 1 秒,用户流失率上升 7% |
| 交互下一步延迟 | INP | 交互响应速度 | ≤200ms | 响应延迟超 300ms,用户会明显感知卡顿 |
| 累积布局偏移 | CLS | 页面稳定性 | ≤0.1 | CLS>0.25,用户误触率增加 40% |
划重点:2024 年 INP 已正式替代 FID 成为交互指标,它衡量 "所有用户交互中最慢的单次响应延迟",比 FID 更能反映真实交互体验。
1.2 3 分钟定位瓶颈:我常用的 4 个实战工具
工具 1:Chrome DevTools(实时调试神器)
这是我日常开发最常用的工具,尤其是Performance 面板,能精准捕捉到每一个性能卡点:
- 打开页面按 F12 进入 DevTools,切换到 Performance
- 点击 "录制" 按钮(圆形红点),操作页面 2-3 秒后停止
- 查看结果面板:
- 红色长条代表长任务(超过 50ms 的 JS 执行),是 INP 超标的主要原因
- 上方 "Timings" 卡片直接显示 LCP、INP、CLS 的数值和达标状态(绿色达标,红黄警告)
- 底部 "Call Tree" 可定位到具体耗时的函数
工具 2:Lighthouse(生成优化清单)
如果想快速获得完整的优化方向,Lighthouse 是首选。它能模拟真实用户环境,生成包含优化建议的报告:
- DevTools 内置:F12→Lighthouse→勾选 "Performance"→点击 "Generate report"
- 命令行使用(适合批量检测):
# 安装Lighthouse
npm install -g lighthouse
# 生成性能报告(仅检测性能维度)
lighthouse https://your-url.com --only-categories=performance --view
报告中的 "Opportunities" 模块会列出具体优化项及预期收益,比如 "压缩图片可节省 200KB,减少加载时间 0.5 秒",非常直观。
工具 3:Web Vitals 库(采集真实用户数据)
实验室数据再好,不如真实用户的反馈。我会用 Google 官方的web-vitals库采集线上数据:
// 安装依赖
npm install web-vitals
// 采集并上报核心指标
import { getCLS, getINP, getLCP } from 'web-vitals';
function sendToAnalytics(metric) {
// 上报到后端或监控平台
fetch('/api/performance', {
method: 'POST',
body: JSON.stringify({
name: metric.name, // 指标名称(LCP/INP/CLS)
value: metric.value, // 指标数值
rating: metric.rating, // 评级(good/needs-improvement/poor)
url: window.location.href,
timestamp: Date.now()
})
});
}
// 监听指标变化
getCLS(sendToAnalytics);
getINP(sendToAnalytics);
getLCP(sendToAnalytics);
工具 4:Memory 面板(排查内存泄漏)
如果页面用久了越来越卡,大概率是内存泄漏。用 Chrome Memory 面板检测:
- 打开 Memory→选择 "Allocation sampling"
- 点击 "Start",操作页面 1 分钟后点击 "Stop"
- 查看 "Summary" 面板:如果某个对象的 "Retainers" 持续增长且不释放,就是泄漏点
二、实战篇:5 大高频场景,从代码层面解决问题

2.1 DOM 操作优化:从 120ms 到 18ms 的蜕变
DOM 操作是性能优化的重灾区。浏览器的 JS 引擎和渲染引擎是独立线程,频繁操作 DOM 会导致两者频繁通信,触发大量重排(Reflow) 和重绘(Repaint)。
场景:渲染 1000 条列表数据
痛点:直接循环创建 DOM 节点,执行时间 120ms,重排 1000 次,页面明显卡顿。
优化方案 1:用 DocumentFragment 批量操作DocumentFragment 是 "离线 DOM 容器",所有操作在内存中完成,最后一次性插入页面,只触发 1 次重排:
// 低效写法:触发1000次重排
function renderListBad(data) {
const list = document.getElementById('list');
data.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
list.appendChild(li); // 每次都触发重排
});
}
// 优化写法:仅触发1次重排
function renderListGood(data) {
const list = document.getElementById('list');
const fragment = document.createDocumentFragment(); // 离线容器
data.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
fragment.appendChild(li); // 内存中操作,不触发重排
});
list.appendChild(fragment); // 单次插入,仅1次重排
}
// 性能测试
const testData = Array(1000).fill({name: '测试项'});
console.time('bad');
renderListBad(testData);
console.timeEnd('bad'); // 约120ms
console.time('good');
renderListGood(testData);
console.timeEnd('good'); // 约25ms
优化方案 2:虚拟滚动(万级数据无压力)当数据量超过 1 万条时,即使批量操作也会卡顿。这时需要虚拟滚动—— 只渲染可视区域的内容,隐藏区域的内容不创建 DOM 节点。
<!-- 虚拟滚动容器 -->
<div class="virtual-list" style="height: 500px; overflow-y: auto;">
<div class="list-placeholder"></div> <!-- 占位容器,维持滚动高度 -->
<div class="visible-items"></div> <!-- 仅渲染可视区域的项 -->
</div>
class VirtualList {
constructor(container, data, itemHeight = 50) {
this.container = container;
this.data = data;
this.itemHeight = itemHeight;
this.placeholder = container.querySelector('.list-placeholder');
this.visibleContainer = container.querySelector('.visible-items');
this.bufferSize = 5; // 可视区域外额外渲染的缓冲项数
// 初始化占位容器高度(总高度=数据量×单项高度)
this.placeholder.style.height = `${data.length * itemHeight}px`;
// 监听滚动事件(用requestAnimationFrame优化)
this.container.addEventListener('scroll', () => {
requestAnimationFrame(() => this.renderVisibleItems());
});
// 首次渲染
this.renderVisibleItems();
}
// 计算可视区域的起始和结束索引
getVisibleRange() {
const scrollTop = this.container.scrollTop;
const clientHeight = this.container.clientHeight;
// 起始索引 = 滚动距离 ÷ 单项高度
const startIdx = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.bufferSize);
// 结束索引 = 起始索引 + 可视区域能容纳的项数 + 缓冲项数
const endIdx = Math.min(
this.data.length - 1,
startIdx + Math.ceil(clientHeight / this.itemHeight) + this.bufferSize * 2
);
return { startIdx, endIdx };
}
// 渲染可视区域的内容
renderVisibleItems() {
const { startIdx, endIdx } = this.getVisibleRange();
// 截取可视区域的数据
const visibleData = this.data.slice(startIdx, endIdx + 1);
// 计算可视区域的偏移量(让内容对齐滚动位置)
this.visibleContainer.style.transform = `translateY(${startIdx * this.itemHeight}px)`;
// 渲染内容(用模板字符串比createElement更快)
this.visibleContainer.innerHTML = visibleData.map(item => `
<div class="list-item" style="height: ${this.itemHeight}px;">
${item.name}
</div>
`).join('');
}
}
// 初始化10万条数据的虚拟滚动
const bigData = Array(100000).fill({name: '大数据项'});
new VirtualList(document.querySelector('.virtual-list'), bigData);
性能对比(渲染 10,000 项数据):
| 方案 | 初始化时间 | 滚动帧率 | 内存占用 |
|---|---|---|---|
| 全量渲染 | 4200ms | 8fps | 850MB |
| 虚拟滚动 | 380ms | 60fps | 95MB |
避坑技巧:DOM 选择器的性能差异
不同的 DOM 查询 API 性能差距很大,我总结了一张决策表:
| 场景 | 推荐 API | 原因 | 性能对比(万次查询) |
|---|---|---|---|
| 按 ID 查找 | getElementById | 浏览器维护哈希索引,O (1) 复杂度 | 12ms |
| 按类名查找(动态集合) | getElementsByClassName | 返回实时 HTMLCollection,支持动态更新 | 28ms |
| 按类名查找(静态集合) | querySelectorAll | 返回静态 NodeList,适合静态页面 | 45ms |
| 复杂选择器(如 div>.class) | querySelector | 支持 CSS 选择器语法,开发效率高 | 62ms |
实测:getElementById 比 querySelector ('#id') 快约 25%,在循环中差异更明显。
2.2 资源加载优化:首屏加载从 3 秒到 1.2 秒
首屏加载速度直接影响用户留存。我之前优化的后台管理系统,通过资源加载优化,首屏时间从 3.2 秒降到 1.2 秒,用户投诉减少了 60%。
核心策略 1:JS 代码分割与懒加载
传统做法是把所有 JS 打包成一个文件,即使是首页用不到的功能也会加载。代码分割能把代码拆分成多个小文件,按需加载。
1. 路由级懒加载(React 示例)
// 未优化:直接导入组件,打包到主文件
import Dashboard from './Dashboard';
// 优化:懒加载组件,单独打包
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 用React.lazy动态导入,会单独生成dashboard.chunk.js
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<BrowserRouter>
{/* 加载时显示骨架屏 */}
<Suspense fallback={<div className="skeleton">加载中...</div>}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
2. 组件级懒加载(Vue 示例)
<template>
<div>
<button @click="showChart = true">显示图表</button>
<!-- 用v-if控制加载时机,组件会按需加载 -->
<ChartComponent v-if="showChart" />
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue';
// 动态导入图表组件,单独打包
const ChartComponent = defineAsyncComponent(() =>
import('./ChartComponent.vue')
);
const showChart = ref(false);
</script>
3. Webpack 手动代码分割如果不用框架,可直接在 Webpack 中配置:
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all', // 分割所有类型的chunk
cacheGroups: {
// 提取第三方库(如react、vue)到vendor.chunk.js
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
},
// 提取公共组件到common.chunk.js
common: {
minChunks: 2, // 被引用2次以上的模块
name: 'common',
chunks: 'all'
}
}
}
}
};
核心策略 2:图片优化(减少 70% 体积)
图片是首屏资源的 "大头",优化空间最大。我常用这 4 个技巧:
- 使用现代图片格式:WebP 比 JPEG 小 30%,AVIF 比 WebP 再小 20%
<!-- 自动降级:浏览器支持WebP就加载,否则加载JPEG -->
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.avif" type="image/avif">
<img src="image.jpg" alt="示例图片" width="800" height="450">
</picture>
- 懒加载非首屏图片:用原生
loading="lazy"属性,无需 JS
<!-- 首屏图片不懒加载,非首屏图片添加loading="lazy" -->
<img src="hero.jpg" alt="首屏banner" width="1200" height="600">
<img src="product1.jpg" alt="商品1" width="300" height="300" loading="lazy">
<img src="product2.jpg" alt="商品2" width="300" height="300" loading="lazy">
- 固定图片宽高比:避免图片加载时布局跳动(解决 CLS 问题)
/* 用aspect-ratio固定宽高比,适配响应式 */
.img-container {
width: 100%;
aspect-ratio: 16/9; /* 宽高比16:9 */
overflow: hidden;
}
.img-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
- 使用 CDN 和图片服务:阿里云、腾讯云等 CDN 提供自动格式转换和裁剪
<!-- 阿里云CDN:自动裁剪为300x300,格式为WebP -->
<img src="https://cdn.example.com/image.jpg?x-oss-process=image/resize,w_300,h_300/format,webp"
alt="优化后的图片">
核心策略 3:CSS 与字体优化
- 减少 CSS 阻塞渲染:
- 内联关键 CSS(首屏必需的样式)
- 非关键 CSS 用
media="print"标记,加载不阻塞渲染
<!-- 内联首屏关键CSS -->
<style>
.hero { height: 60vh; background: #f5f5f5; }
.header { position: fixed; top: 0; width: 100%; }
</style>
<!-- 非关键CSS延迟加载 -->
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">
- 字体加载优化:用
font-display: swap避免文字闪烁
/* 字体加载时先用系统字体显示,加载完成后替换 */
@font-face {
font-family: 'MyFont';
src: url('myfont.woff2') format('woff2');
font-display: swap; /* 关键属性 */
font-weight: 400;
font-style: normal;
}
2.3 代码执行优化:长任务拆分与效率提升
JS 执行时间过长会阻塞主线程,导致交互卡顿(INP 超标)。我之前处理过一个数据导出功能,点击按钮后 2 秒没反应,就是因为有个 1800ms 的长任务。
场景 1:长任务拆分(解决 INP 超标)
把超过 50ms 的长任务拆分成多个小任务,用requestIdleCallback或setTimeout调度执行。
优化前:
// 长任务:处理10万条数据,耗时1800ms
function processBigData(data) {
const result = [];
data.forEach(item => {
// 复杂计算:解析、转换、过滤
const processed = heavyCalculation(item);
result.push(processed);
});
return result;
}
// 点击按钮后执行,阻塞主线程2秒
document.getElementById('export-btn').addEventListener('click', () => {
const bigData = get100kData();
const result = processBigData(bigData); // 阻塞主线程
downloadFile(result);
});
优化后:
// 拆分任务:每次处理100条数据
function processInChunks(data, chunkSize = 100, callback) {
let index = 0;
const result = [];
// 处理单个chunk
function processChunk() {
const end = Math.min(index + chunkSize, data.length);
for (; index < end; index++) {
const processed = heavyCalculation(data[index]);
result.push(processed);
}
// 还有数据没处理,继续调度
if (index < data.length) {
// 用requestIdleCallback在浏览器空闲时执行
requestIdleCallback(processChunk, { timeout: 100 });
} else {
// 全部处理完成,执行回调
callback(result);
}
}
// 启动第一个chunk处理
requestIdleCallback(processChunk, { timeout: 100 });
}
// 优化后的点击事件:不阻塞主线程
document.getElementById('export-btn').addEventListener('click', () => {
const bigData = get100kData();
// 显示加载状态
showLoading();
// 分块处理数据
processInChunks(bigData, 100, (result) => {
hideLoading();
downloadFile(result);
});
});
场景 2:Web Workers 处理计算密集型任务
对于纯计算任务(如数据处理、加密解密),用 Web Workers 把任务放到后台线程执行,主线程完全不阻塞。
1. 创建 Worker 文件(data-processor.worker.js)
// Worker线程:处理计算任务,不影响主线程
self.onmessage = (e) => {
const { data } = e;
// 复杂计算
const result = data.map(item => heavyCalculation(item));
// 把结果发送回主线程
self.postMessage(result);
// 关闭Worker
self.close();
};
2. 主线程调用 Worker
// 主线程代码
document.getElementById('process-btn').addEventListener('click', () => {
const bigData = get100kData();
showLoading();
// 创建Worker实例
const worker = new Worker('data-processor.worker.js');
// 发送数据给Worker
worker.postMessage(bigData);
// 接收Worker返回的结果
worker.onmessage = (e) => {
const result = e.data;
hideLoading();
renderResult(result);
};
// 处理错误
worker.onerror = (error) => {
console.error(`Worker error: ${error.message}`);
hideLoading();
};
});
注意:Worker 不能访问 DOM、window 对象,只能通过
postMessage与主线程通信。
场景 3:循环与数据结构优化
循环和数据结构的选择对代码执行效率影响很大,我总结了几个实战技巧:
- 缓存数组长度:避免每次循环都计算
arr.length
// 低效写法
for (let i = 0; i < arr.length; i++) {
// 每次循环都要读取arr.length
}
// 优化写法
const len = arr.length;
for (let i = 0; i < len; i++) {
// 仅读取一次长度
}
- 用 Map/Set 替代对象:频繁增删操作时性能更优
// 场景:存储用户ID到用户名的映射,频繁查询和修改
const userIdToName = new Map();
// 添加数据
userIdToName.set(1, '张三');
userIdToName.set(2, '李四');
// 查询数据(O(1)复杂度,比对象快)
userIdToName.get(1); // '张三'
// 遍历数据(保持插入顺序,对象不保证)
for (const [id, name] of userIdToName) {
console.log(id, name);
}
- 避免不必要的函数调用:循环内的函数调用会累积性能开销
// 低效写法:循环内调用函数
const result = [];
arr.forEach(item => {
result.push(transformItem(item)); // 每次循环都调用函数
});
// 优化写法:提前获取函数引用,或内联逻辑
const transform = transformItem; // 缓存函数引用
const result = [];
const len = arr.length;
for (let i = 0; i < len; i++) {
result.push(transform(arr[i]));
}
2.4 内存管理:避免页面越用越卡
内存泄漏是很多开发者容易忽视的问题,表现为页面用得越久,内存占用越高,最终导致卡顿甚至崩溃。
常见内存泄漏场景及解决方案
- 意外的全局变量
// 问题:未声明的变量会挂载到window上,不会被回收
function handleClick() {
// 遗漏var/let/const,变成全局变量
userName = document.getElementById('username').value;
}
// 解决方案1:用let/const声明变量
function handleClick() {
const userName = document.getElementById('username').value;
}
// 解决方案2:启用严格模式(禁止意外全局变量)
'use strict';
function handleClick() {
userName = 'test'; // 直接报错
}
- 未清理的事件监听器
// 问题:组件销毁后,事件监听器未移除,导致DOM无法回收
class MyComponent {
constructor() {
this.button = document.getElementById('btn');
this.button.addEventListener('click', this.handleClick);
}
handleClick() {
console.log('clicked');
}
destroy() {
// 忘记移除事件监听器
// this.button.removeEventListener('click', this.handleClick);
}
}
// 解决方案:组件销毁时移除事件监听器
destroy() {
this.button.removeEventListener('click', this.handleClick);
this.button = null; // 解除引用
}
- 闭包导致的内存泄漏
// 问题:闭包引用外部变量,导致变量无法回收
function createTimer() {
const data = Array(10000).fill('large data'); // 大数据
setInterval(() => {
console.log('timer running');
// 闭包引用了data,即使不需要也无法回收
}, 1000);
}
// 解决方案1:不需要时清除定时器
function createTimer() {
const data = Array(10000).fill('large data');
const timer = setInterval(() => {
console.log('timer running');
}, 1000);
// 提供清除定时器的方法
return () => clearInterval(timer);
}
// 解决方案2:用WeakMap缓存数据(自动回收无引用的数据)
const cache = new WeakMap();
function processData(key, data) {
cache.set(key, data); // 弱引用,key被回收时data也会被回收
}
内存泄漏检测实战
- 打开 Chrome DevTools→Memory
- 选择 "Allocation timeline"→点击 "Start"
- 操作页面(如切换组件、点击按钮)30 秒
- 点击 "Stop",查看内存走势图:
- 如果内存持续上升且不回落,说明有泄漏
- 点击 "Take snapshot",对比不同时间点的快照,找出持续增长的对象
2.5 高频事件优化:防抖与节流
scroll、resize、input 等高频事件,触发频率可达每秒 60 次,如果在事件回调中做复杂操作,会严重卡顿。
防抖(Debounce):合并多次触发为一次
适合输入框搜索、按钮点击防重复提交等场景 —— 事件触发后等待 n 秒再执行,如果 n 秒内再次触发则重新计时。
function debounce(fn, delay = 300) {
let timer = null;
// 返回防抖后的函数
return function(...args) {
// 清除之前的定时器
if (timer) clearTimeout(timer);
// 重新设置定时器
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
// 应用:输入框搜索提示
const searchInput = document.getElementById('search-input');
const fetchSuggestions = debounce(async (value) => {
const suggestions = await fetch(`/api/suggest?key=${value}`);
renderSuggestions(suggestions);
}, 500);
// 绑定防抖后的函数
searchInput.addEventListener('input', (e) => {
fetchSuggestions(e.target.value);
});
节流(Throttle):控制触发频率
适合滚动加载、拖拽等场景 —— 每隔 n 秒只执行一次,即使事件触发多次。
function throttle(fn, interval = 300) {
let lastTime = 0; // 上次执行时间
return function(...args) {
const now = Date.now();
// 距离上次执行时间超过interval才执行
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 应用:滚动加载更多数据
const container = document.getElementById('scroll-container');
const loadMore = throttle(async () => {
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
// 滚动到底部
if (scrollTop + clientHeight >= scrollHeight - 100) {
showLoading();
const moreData = await fetch(`/api/data?page=${nextPage}`);
renderData(moreData);
hideLoading();
nextPage++;
}
}, 500);
container.addEventListener('scroll', loadMore);
三、框架篇:React/Vue 性能优化实战

现代框架虽然已经做了很多优化,但不合理的写法仍会导致性能问题。我结合实际项目经验,总结了 React 和 Vue 的核心优化技巧。
3.1 React 性能优化
React 的核心优化思路是减少不必要的渲染,主要通过控制组件的重渲染时机实现。
技巧 1:用 React.memo 避免组件重复渲染
React.memo是高阶组件,用于缓存组件渲染结果,当 props 没有变化时不重新渲染。
// 子组件:显示用户信息
const UserCard = React.memo(function UserCard({ user, onEdit }) {
console.log('UserCard渲染'); // 仅在user或onEdit变化时执行
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={onEdit}>编辑</button>
</div>
);
});
// 父组件
function UserList({ users }) {
// 问题:每次父组件渲染,都会创建新的onEdit函数
// const onEdit = (id) => { /* 编辑逻辑 */ };
// 解决方案:用useCallback缓存函数引用
const onEdit = useCallback((id) => {
/* 编辑逻辑 */
}, []); // 依赖为空,函数不会重新创建
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} onEdit={onEdit} />
))}
</div>
);
}
技巧 2:用 useMemo 缓存计算结果
对于复杂计算(如大数据排序、过滤),用useMemo缓存计算结果,避免每次渲染都重新计算。
function ProductList({ products, filter }) {
// 复杂计算:过滤并排序商品(耗时操作)
// 未优化:每次渲染都重新计算
// const filteredProducts = products
// .filter(p => p.price <= filter.maxPrice)
// .sort((a, b) => b.sales - a.sales);
// 优化:仅在products或filter变化时重新计算
const filteredProducts = useMemo(() => {
return products
.filter(p => p.price <= filter.maxPrice)
.sort((a, b) => b.sales - a.sales);
}, [products, filter]); // 依赖数组
return (
<div className="product-list">
{filteredProducts.map(product => (
<ProductItem key={product.id} product={product} />
))}
</div>
);
}
技巧 3:用 useTransition 处理非紧急更新
对于不紧急的更新(如列表过滤、搜索结果渲染),用useTransition标记为低优先级,避免阻塞紧急操作(如输入框输入)。
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// 创建过渡状态
const [isPending, startTransition] = useTransition();
// 输入框变化时更新query(紧急操作)
const handleInputChange = (e) => {
setQuery(e.target.value);
// 用startTransition包裹非紧急操作(过滤结果)
startTransition(() => {
// 过滤结果是耗时操作,但不会阻塞输入
const filtered = getProducts().filter(p =>
p.name.includes(e.target.value)
);
setResults(filtered);
});
};
return (
<div>
<input
type="text"
value={query}
onChange={handleInputChange}
placeholder="搜索商品..."
/>
{/* 加载状态 */}
{isPending && <div>加载中...</div>}
<ProductList products={results} />
</div>
);
}
3.2 Vue 3 性能优化
Vue 3 通过Proxy实现了细粒度的响应式追踪,优化起来比 Vue 2 更简单,但仍有一些实战技巧。
技巧 1:用 v-memo 缓存渲染结果
v-memo类似 React.memo,用于缓存模板片段,当依赖项没有变化时不重新渲染。
<template>
<div>
<h2>商品列表</h2>
<!-- 仅在products或sortKey变化时重新渲染列表 -->
<div v-memo="[products, sortKey]">
<product-item
v-for="product in sortedProducts"
:key="product.id"
:product="product"
/>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps(['products', 'sortKey']);
// 排序商品
const sortedProducts = computed(() => {
return [...props.products].sort((a, b) => {
return a[props.sortKey] - b[props.sortKey];
});
});
</script>
技巧 2:用 markRaw 避免不必要的响应式
对于大型对象(如图表数据、第三方库实例),如果不需要响应式,可以用markRaw标记为非响应式,减少 Proxy 代理的开销。
<script setup>
import { markRaw } from 'vue';
import { Chart } from 'chart.js';
// 图表实例不需要响应式,用markRaw避免代理
const chartInstance = markRaw(new Chart(
document.getElementById('chart'),
{ /* 配置 */ }
));
// 后续操作图表实例,不会触发响应式追踪
function updateChart(data) {
chartInstance.data.datasets[0].data = data;
chartInstance.update();
}
</script>
技巧 3:组件拆分与动态导入
把大型组件拆分成小型组件,不仅利于维护,还能减少重渲染范围。配合动态导入,进一步优化加载性能。
<!-- 大型表单组件拆分为多个小型组件 -->
<template>
<div class="large-form">
<form-personal-info :data="formData.personal" />
<form-address :data="formData.address" />
<form-payment :data="formData.payment" />
<!-- 动态导入低频使用的组件 -->
<component :is="formExtraComponent" v-if="showExtra" />
</div>
</template>
<script setup>
import { defineAsyncComponent, ref } from 'vue';
// 直接导入高频使用的组件
import FormPersonalInfo from './FormPersonalInfo.vue';
import FormAddress from './FormAddress.vue';
// 动态导入低频使用的组件(如额外信息表单)
const FormExtra = defineAsyncComponent(() =>
import('./FormExtra.vue')
);
const formExtraComponent = ref(FormExtra);
const showExtra = ref(false);
const formData = ref({ /* 表单数据 */ });
</script>
四、监控篇:性能优化不是一锤子买卖

很多项目优化后初期效果很好,但随着迭代,性能逐渐回退 —— 这是因为缺乏持续监控。我会建立 "性能监控→告警→优化" 的闭环体系。
4.1 线上性能监控方案
方案 1:自建监控系统(适合大型项目)
基于Performance API采集关键指标,上报到后端进行分析:
// 采集页面加载性能指标
function collectLoadPerformance() {
// 等待所有资源加载完成
window.addEventListener('load', () => {
const perfData = performance.getEntriesByType('navigation')[0];
sendToAnalytics({
type: 'load_performance',
dnsTime: perfData.domainLookupEnd - perfData.domainLookupStart, // DNS解析时间
tcpTime: perfData.connectEnd - perfData.connectStart, // TCP连接时间
loadTime: perfData.loadEventStart - perfData.navigationStart, // 页面加载总时间
firstPaint: perfData.firstPaint - perfData.navigationStart, // 首次绘制时间
});
});
}
// 采集长任务
function collectLongTasks() {
const observer = new PerformanceObserver((entries) => {
entries.forEach(entry => {
// 上报超过50ms的长任务
if (entry.duration > 50) {
sendToAnalytics({
type: 'long_task',
duration: entry.duration,
startTime: entry.startTime,
// 获取长任务的调用栈(需要开启Chrome的性能面板设置)
stack: entry.name
});
}
});
});
// 监听长任务
observer.observe({ entryTypes: ['longtask'] });
}
// 初始化监控
collectLoadPerformance();
collectLongTasks();
方案 2:第三方监控工具(适合快速落地)
如果不想自建,可以用成熟的第三方工具:
- Sentry:主打错误监控,同时支持性能监控,能定位到具体的慢函数和 API
- 阿里云 ARMS:国内工具,支持多地域监控,提供性能优化建议
- Google Analytics 4:免费,支持 Core Web Vitals 报表,需配置 GTM
4.2 自动化性能检测(CI/CD 集成)
把性能检测集成到 CI/CD 流程中,每次代码提交或合并 PR 时自动检测,性能不达标则拦截上线。
以 GitHub Actions + Lighthouse CI 为例:
- 创建 LHCI 配置文件(lighthouserc.js)
module.exports = {
ci: {
collect: {
numberOfRuns: 3, // 运行3次取平均值
staticDistDir: './build', // 构建产物目录
},
assert: {
assertions: {
// 设定性能阈值
'largest-contentful-paint': ['error', { minScore: 0.9 }], // LCP≥0.9(对应≤2.5秒)
'interactive': ['error', { minScore: 0.9 }], // INP≥0.9(对应≤200ms)
'cumulative-layout-shift': ['error', { minScore: 0.9 }], // CLS≥0.9(对应≤0.1)
'first-contentful-paint': ['error', { minScore: 0.9 }],
},
},
upload: {
target: 'temporary-public-storage', // 临时存储报告
},
},
};
- 创建 GitHub Actions 配置(.github/workflows/performance.yml)
name: Performance Test
on: [pull_request] # PR时触发
jobs:
lighthouse-ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
configPath: './lighthouserc.js'
uploadArtifacts: true # 上传报告作为artifact
temporaryPublicStorage: true
这样每次提交 PR 时,都会自动运行 Lighthouse 检测,如果性能不达标,PR 会显示 "Failed",必须修复后才能合并。
五、避坑篇:90% 的人都会踩的 6 个陷阱

5.1 陷阱 1:过度优化
我见过有人为了优化 10ms 的执行时间,把代码改得晦涩难懂,维护成本飙升。记住:优化的收益要大于成本。
- 优先优化关键路径(首屏加载、用户交互),非关键路径的性能问题可以忽略
- 只有当指标不达标时才优化,达标后无需过度追求极致性能
- 保持代码可读性是前提,不要为了优化牺牲可维护性
5.2 陷阱 2:忽视移动端性能
PC 端测试没问题,到了移动端就卡顿 —— 这是因为移动端设备性能和网络环境更差。
- 测试时用 Chrome DevTools 模拟低端手机(如 Galaxy S9)和 3G 网络
- 移动端避免使用过多动画和复杂计算,优先保证流畅度
- 图片加载要更保守,优先使用低分辨率图片,再渐进式加载高清图
5.3 陷阱 3:滥用 will-change
will-change能触发 GPU 加速,但过度使用会导致 "层爆炸",内存占用暴涨。
- 只在频繁动画的元素上使用,动画结束后移除
- 避免同时给多个元素添加 will-change
- 正确用法:
/* 动画前添加 */
.animated-element {
will-change: transform, opacity;
transition: transform 0.3s ease;
}
/* 动画结束后通过JS移除 */
element.addEventListener('transitionend', () => {
element.style.willChange = 'auto';
});
5.4 陷阱 4:第三方脚本阻塞
广告、统计、分享等第三方脚本往往是性能杀手,很多开发者忽视了它们的影响。
- 用
async或defer加载第三方脚本,避免阻塞渲染
<!-- async:加载完成后立即执行,顺序不确定 -->
<script src="third-party-ad.js" async></script>
<!-- defer:加载完成后等待DOM解析完成,顺序确定 -->
<script src="third-party-stat.js" defer></script>
- 延迟加载非必要的第三方脚本(如用户滚动后再加载)
// 延迟加载第三方脚本
function loadThirdPartyScript(url) {
const script = document.createElement('script');
script.src = url;
script.async = true;
document.body.appendChild(script);
}
// 用户滚动后加载
window.addEventListener('scroll', () => {
loadThirdPartyScript('https://third-party.com/script.js');
}, { once: true }); // 只执行一次
5.5 陷阱 5:不测试真实数据
用少量测试数据优化,到了生产环境大数据量下依然卡顿。
- 优化时用真实数据量测试(如 1 万条列表、10 万条数据处理)
- 模拟真实用户行为(如快速滚动、频繁点击)
- 测试不同网络环境(3G、4G、Wi-Fi)下的性能表现
5.6 陷阱 6:忽视内存泄漏
内存泄漏的影响是渐进式的,初期很难发现,等到用户投诉时已经影响很大。
- 定期用 Chrome Memory 面板检测内存使用情况
- 组件销毁时清理所有资源(事件监听器、定时器、Worker)
- 用 WeakMap/WeakSet 存储临时数据,自动回收无引用对象
六、FAQ:前端开发者最常问的 7 个问题

Q1:性能优化的优先级是什么?
A:按影响用户体验的程度排序:
- 首屏加载速度(LCP):直接决定用户是否会离开
- 交互响应速度(INP):决定用户使用时是否流畅
- 页面稳定性(CLS):避免用户误触和烦躁
- 运行时性能(如滚动帧率):影响长期使用体验
Q2:如何平衡性能优化和开发效率?
A:
- 优先使用框架和工具的内置优化(如 React.lazy、Vue 的动态导入),开发成本低且效果好
- 制定团队性能规范(如图片必须懒加载、组件必须拆分),避免后期返工
- 把性能检测集成到 CI/CD,自动化拦截性能问题,减少人工排查成本
Q3:原生 JS 和框架哪个性能更好?
A:不能一概而论:
- 简单页面(如静态展示页):原生 JS 性能可能更好,因为框架有额外开销
- 复杂页面(如后台管理系统、电商平台):框架(React/Vue)性能更好,因为有虚拟 DOM、批量更新等优化
- 关键在于写法是否合理,糟糕的原生 JS 代码性能可能比优化后的框架代码差 10 倍
Q4:Web Workers 能解决所有卡顿问题吗?
A:不能。Web Workers 适合纯计算任务,不适合 DOM 操作和需要主线程 API 的场景。
- 适用场景:数据处理、加密解密、复杂计算
- 不适用场景:DOM 操作、事件监听、定时器(Worker 有自己的定时器,但无法访问主线程的)
Q5:如何量化性能优化的效果?
A:用数据说话:
- 核心指标:LCP、INP、CLS 的变化(如 LCP 从 3 秒降到 1.5 秒)
- 加载性能:首屏加载时间、资源加载大小(如 JS 体积减少 500KB)
- 运行时性能:长任务数量、滚动帧率(如从 30fps 升到 60fps)
- 业务指标:用户留存率、页面停留时间(优化后通常会提升)
Q6:老旧浏览器需要兼容性能优化吗?
A:根据用户群体决定:
- 如果用户以移动端和现代浏览器为主:可以大胆使用新特性(如 Web Workers、ES6+)
- 如果需要兼容 IE11 等老旧浏览器:
- 避免使用新 API,用 polyfill 需谨慎(会增加代码体积)
- 重点优化 DOM 操作和资源加载,老旧浏览器对这两点更敏感
Q7:性能优化有终点吗?
A:没有。性能优化是持续迭代的过程:
- 业务发展:新功能会引入新的性能问题
- 技术迭代:新的浏览器特性和框架版本会带来新的优化空间
- 用户期望:用户对性能的要求会越来越高(从 2 秒加载到 1 秒加载)
七、总结:性能优化的核心思维
做了几年前端性能优化,最大的感悟是:性能优化不是技术炫技,而是以用户体验为中心的工程实践。
它不需要你掌握多么高深的技术,而是需要你有 "用户视角"—— 当你点击按钮时,能想到用户等待 1 秒的烦躁;当你滑动列表时,能想到用户看到卡顿的无奈。
记住三个核心原则:
- 数据驱动:用 Core Web Vitals 等指标量化性能,避免凭感觉优化
- 渐进式优化:先解决最影响用户的问题(如 LCP 超标),再优化细节
- 持续监控:性能优化不是一锤子买卖,需要建立监控闭环确保持续达标
欢迎在评论区分享你的优化经验,让我们一起打造更丝滑的 Web 应用!
JavaScript性能优化全攻略
1323

被折叠的 条评论
为什么被折叠?



