JavaScript 性能优化最佳实践
JavaScript 性能优化最佳实践
JavaScript 性能直接影响应用的用户体验。一个高性能的 JavaScript 应用应该具备快速响应、流畅动画、低内存占用等特点。性能优化是一个持续的过程,需要在开发的各个阶段关注。以下是一些关键的性能优化策略,涵盖代码优化、内存管理、异步处理、DOM 操作等多个方面。
为什么性能如此重要
性能不是锦上添花,而是直接关乎生意的核心指标。可以打个比方:网页加载就像餐厅上菜,顾客等得越久越不耐烦,超过一定时间就直接走人。业界有大量真实数据佐证:
如今谷歌把 Core Web Vitals(核心网页指标)纳入搜索排名,性能已经和 SEO、转化率深度绑定。理解并优化 JavaScript 性能,是每个前端工程师的必修课。
核心性能指标(Core Web Vitals)
| 指标 | 含义 | 优秀阈值 | 需改进 | 差 |
| --- | --- | --- | --- | --- |
| LCP 最大内容绘制 | 主内容加载速度 | 小于 2.5 秒 | 2.5 到 4 秒 | 大于 4 秒 |
| INP 交互到下次绘制 | 交互响应速度 | 小于 200 毫秒 | 200 到 500 毫秒 | 大于 500 毫秒 |
| CLS 累积布局偏移 | 视觉稳定性 | 小于 0.1 | 0.1 到 0.25 | 大于 0.25 |
代码优化
变量声明与作用域:
// ❌ 避免:全局变量污染
var globalVar = 'global';
// ✅ 推荐:使用模块作用域
const Module = (() => {
const privateVar = 'private';
return {
getPrivate: () => privateVar,
};
})();
// ❌ 避免:闭包导致的内存问题
function createHandlers() {
const handlers = [];
for (var i = 0; i < 10; i++) {
handlers.push(function() {
console.log(i); // 所有函数都输出 10
});
}
return handlers;
}
// ✅ 推荐:使用 let 或 IIFE
function createHandlers() {
const handlers = [];
for (let i = 0; i < 10; i++) {
handlers.push(() => console.log(i)); // 输出 0-9
}
return handlers;
}函数优化:
// ❌ 避免:循环中创建函数
const handlers = [];
for (let i = 0; i < 1000; i++) {
handlers.push(function() {
return i * 2;
});
}
// ✅ 推荐:提取函数
const double = (i) => i * 2;
const handlers = [];
for (let i = 0; i < 1000; i++) {
handlers.push(() => double(i));
}
// 函数柯里化优化
const curry = (fn) => {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...moreArgs) => curried.apply(this, args.concat(moreArgs));
};
};
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
const add5 = curriedAdd(5); // 部分应用,复用函数循环优化:
// ❌ 避免:每次迭代都计算长度
for (let i = 0; i < array.length; i++) {
// ...
}
// ✅ 推荐:缓存数组长度
for (let i = 0, len = array.length; i < len; i++) {
// ...
}
// ❌ 避免:循环中进行 DOM 操作
for (let i = 0; i < items.length; i++) {
const li = document.createElement('li');
li.textContent = items[i];
list.appendChild(li); // 每次都触发重排
}
// ✅ 推荐:使用 DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < items.length; i++) {
const li = document.createElement('li');
li.textContent = items[i];
fragment.appendChild(li);
}
list.appendChild(fragment); // 只触发一次重排条件优化:
// ❌ 避免:多个 if-else
function getStatus(status) {
if (status === 'pending') return '等待中';
else if (status === 'processing') return '处理中';
else if (status === 'completed') return '已完成';
else if (status === 'failed') return '失败';
else return '未知';
}
// ✅ 推荐:使用对象字面量
const statusMap = {
pending: '等待中',
processing: '处理中',
completed: '已完成',
failed: '失败',
};
function getStatus(status) {
return statusMap[status] || '未知';
}
// ✅ 推荐:使用 Map(更高效)
const statusMap = new Map([
['pending', '等待中'],
['processing', '处理中'],
['completed', '已完成'],
['failed', '失败'],
]);选择合适的数据结构: 数据结构的选择往往比微观代码优化影响更大。查找场景用 Map/Set(哈希表,O(1))远胜于数组的 `includes/indexOf`(O(n))。
// ❌ 数组查找:每次 includes 都是 O(n),10000 条数据里查找会很慢
const list = [/* 10000 个 id */];
function exists(id) {
return list.includes(id); // O(n)
}
// ✅ 用 Set:O(1) 查找
const set = new Set(list);
function exists(id) {
return set.has(id); // O(1)
}
// 在 1 万条数据、10 万次查询的基准下,Set 通常比数组快 100 倍以上内存管理
内存问题往往比 CPU 问题更隐蔽:应用不会立刻崩溃,而是随着使用时间越来越卡,最终标签页崩溃(Out Of Memory)。理解内存管理是写出长时间稳定运行应用的关键。
内存泄漏常见原因与预防:
// ❌ 避免:未清理的定时器
class Component {
constructor() {
this.timer = setInterval(() => {
this.updateData();
}, 1000);
}
// 缺少清理逻辑
}
// ✅ 推荐:正确清理
class Component {
constructor() {
this.timer = setInterval(() => {
this.updateData();
}, 1000);
}
destroy() {
clearInterval(this.timer);
}
}
// ❌ 避免:未移除的事件监听器
class Modal {
constructor() {
document.addEventListener('keydown', this.handleKeydown);
}
}
// ✅ 推荐:正确移除监听器
class Modal {
constructor() {
this.handleKeydown = this.handleKeydown.bind(this);
document.addEventListener('keydown', this.handleKeydown);
}
destroy() {
document.removeEventListener('keydown', this.handleKeydown);
}
}
// ❌ 避免:闭包持有大对象引用
function createProcessor(largeData) {
return function process() {
// 即使只用一部分数据,整个 largeData 都被引用
return largeData[0];
};
}
// ✅ 推荐:只保留需要的数据
function createProcessor(largeData) {
const neededData = largeData[0];
return function process() {
return neededData;
};
}垃圾回收机制理解:
// WeakMap 示例:不阻止垃圾回收
const privateData = new WeakMap();
class MyClass {
constructor() {
privateData.set(this, { secret: 'value' });
}
getSecret() {
return privateData.get(this)?.secret;
}
}
// 当 MyClass 实例被销毁时,WeakMap 中的数据也会被回收
// WeakSet 示例:跟踪对象而不阻止回收
const visitedObjects = new WeakSet();
function processObject(obj) {
if (visitedObjects.has(obj)) {
return; // 已处理过
}
visitedObjects.add(obj);
// 处理对象
}内存优化技巧:
// 对象池模式
class ObjectPool {
constructor(factory, initialSize = 10) {
this.factory = factory;
this.pool = [];
for (let i = 0; i < initialSize; i++) {
this.pool.push(factory());
}
}
acquire() {
return this.pool.length > 0 ? this.pool.pop() : this.factory();
}
release(obj) {
// 重置对象状态
Object.keys(obj).forEach(key => delete obj[key]);
this.pool.push(obj);
}
}
// 使用示例
const vectorPool = new ObjectPool(() => ({ x: 0, y: 0, z: 0 }));
function calculate() {
const v = vectorPool.acquire();
// 使用 v 进行计算
vectorPool.release(v);
}
// 数组复用
const tempArray = [];
function processItems(items) {
tempArray.length = 0; // 清空但保留内存
for (const item of items) {
if (item.active) {
tempArray.push(item);
}
}
return tempArray;
}带上限的 LRU 缓存: 缓存能提速,但无上限的缓存本身就是内存泄漏源头。
// 简单的 LRU(最近最少使用)缓存,利用 Map 的插入顺序特性
class LRUCache {
constructor(maxSize = 100) {
this.maxSize = maxSize;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return undefined;
const value = this.cache.get(key);
this.cache.delete(key); // 删了再放,更新为最近使用
this.cache.set(key, value);
return value;
}
set(key, value) {
if (this.cache.has(key)) this.cache.delete(key);
else if (this.cache.size >= this.maxSize) {
// 淘汰最久未使用的(Map 的第一个 key)
this.cache.delete(this.cache.keys().next().value);
}
this.cache.set(key, value);
}
}异步优化
JavaScript 是单线程的,长时间的同步任务会阻塞主线程,导致页面"假死"。异步优化的核心就是把耗时操作从主线程上挪开,保持界面流畅。
异步操作:
// 并发控制:限制同时进行的异步任务数量
async function asyncPool(limit, items, iteratorFn) {
const results = [];
const executing = new Set();
for (const item of items) {
const p = Promise.resolve().then(() => iteratorFn(item));
results.push(p);
executing.add(p);
p.finally(() => executing.delete(p));
if (executing.size >= limit) {
await Promise.race(executing); // 等任意一个完成再继续
}
}
return Promise.all(results);
}
// 用法:最多 3 个请求并发,避免一次性发起 100 个请求
const urls = [/* 100 个 url */];
await asyncPool(3, urls, url => fetch(url).then(r => r.json()));防抖(debounce)与节流(throttle)——高频事件优化利器:
防抖:事件停止触发一段时间后才执行(适合搜索框输入、窗口 resize)。节流:固定时间间隔最多执行一次(适合滚动、拖拽)。
// 防抖:连续触发只在最后一次后 wait 毫秒执行
function debounce(fn, wait) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
};
}
// 节流:每 limit 毫秒最多执行一次
function throttle(fn, limit) {
let inThrottle = false;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// 案例:搜索框输入,用户停止打字 300ms 才发请求
// 若不做防抖,输入 "javascript" 会发 10 次请求;防抖后只发 1 次,减少 90% 请求
searchInput.addEventListener('input', debounce(e => {
fetch(`/api/search?q=${e.target.value}`);
}, 300));
// 案例:滚动事件,最多每 100ms 执行一次
window.addEventListener('scroll', throttle(() => {
updateScrollProgress();
}, 100));用 requestAnimationFrame 处理动画,用 Web Worker 处理密集计算:
// requestAnimationFrame:与屏幕刷新率同步(通常 60fps,约 16.7ms 一帧),比 setInterval 更流畅
function animate() {
element.style.transform = `translateX(${pos}px)`;
pos += 2;
if (pos < 300) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
// Web Worker:把 CPU 密集计算放到独立线程,不阻塞 UI
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ numbers: bigArray });
worker.onmessage = e => console.log('计算结果:', e.data);
// worker.js
self.onmessage = e => {
const sum = e.data.numbers.reduce((a, b) => a + b, 0); // 耗时计算不卡 UI
self.postMessage(sum);
};网络请求: 合并请求、使用缓存、实现请求节流与失败重试。
资源加载: 延迟加载非关键资源、预加载关键资源、使用 CDN 分发静态资源。
浏览器渲染优化
理解浏览器渲染流水线(重排 Reflow → 重绘 Repaint → 合成 Composite)是渲染优化的基础。重排代价最高(要重新计算布局),重绘次之,合成最便宜(只在 GPU 上完成)。
DOM 操作:
// ❌ 布局抖动:交替读写触发多次强制重排
elements.forEach(el => {
const width = el.offsetWidth; // 读(强制重排)
el.style.width = width + 10 + 'px'; // 写
});
// ✅ 读写分离:先批量读,再批量写,只触发一次重排
const widths = elements.map(el => el.offsetWidth); // 批量读
elements.forEach((el, i) => {
el.style.width = widths[i] + 10 + 'px'; // 批量写
});渲染优化:
事件处理:
// ❌ 给 1000 个 li 各绑一个监听器
document.querySelectorAll('li').forEach(li => {
li.addEventListener('click', handleClick);
});
// ✅ 事件委托:只在父元素绑一个
document.querySelector('ul').addEventListener('click', e => {
if (e.target.tagName === 'LI') handleClick(e);
});工具和监控
性能分析:
// 精确测量代码执行耗时
const start = performance.now();
doExpensiveWork();
const end = performance.now();
console.log(`耗时 ${(end - start).toFixed(2)} 毫秒`);
// 用 PerformanceObserver 监控真实用户的长任务(大于 50ms 的任务会阻塞交互)
new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
console.warn('检测到长任务:', entry.duration, 'ms');
}
}).observe({ entryTypes: ['longtask'] });监控工具: Lighthouse(本地/CI 打分)、Web Vitals 库(采集真实用户指标)、自定义 RUM(真实用户监控)上报。
构建优化: 代码压缩(Terser)、代码分割(按路由懒加载)、Tree-shaking(删除未用代码)、图片压缩与懒加载。
真实案例:长列表性能优化
某后台管理系统要展示 1 万条订单记录,直接 `v-for` 渲染导致首屏卡死 3 秒以上,滚动掉帧严重。优化过程:
优化后,同样 1 万条数据,内存占用从约 180MB 降到约 40MB,用户体验从"卡到想砸电脑"变成丝般顺滑。
优化手段效果对比
| 优化手段 | 适用场景 | 典型收益 |
| --- | --- | --- |
| 防抖/节流 | 高频事件(输入、滚动、resize) | 请求/执行次数减少 80% 到 95% |
| 虚拟滚动 | 超长列表 | DOM 数量与内存降低 90% 以上 |
| Web Worker | CPU 密集计算 | 主线程不阻塞,交互流畅 |
| 事件委托 | 大量同类元素 | 监听器数量降至 1 个 |
| 代码分割 | 大型应用首屏 | 首屏包体积减少 40% 到 70% |
| DocumentFragment | 批量 DOM 插入 | 重排从 N 次降为 1 次 |
深入 V8 引擎:代码为什么会变快
要写出真正高性能的 JavaScript,得理解引擎在背后做了什么。以 V8(Chrome、Node.js 的引擎)为例,一段 JS 代码的执行经历如下流水线:源码 → 解析成 AST → Ignition 解释器生成字节码并执行 → 热点代码被 TurboFan 优化编译器编译成高度优化的机器码。这套"解释 + JIT(即时编译)"的混合策略,让 JS 兼顾启动速度和峰值性能。
关键在于:TurboFan 的优化是基于假设的。它观察到"这个函数每次都用 number 调用",就会编译出专门处理 number 的快速版本。一旦某次调用违反了假设(比如突然传了字符串),就会触发去优化(deoptimization),退回到解释器重新观察,性能瞬间打回原形。所以高性能 JS 的核心原则之一是:让函数处理的类型保持稳定和单一(monomorphic)。
// ❌ 多态(polymorphic):同一函数被不同形状的对象调用,难以优化
function getX(point) {
return point.x;
}
getX({ x: 1, y: 2 }); // 形状 A
getX({ x: 1, y: 2, z: 3 }); // 形状 B
getX({ y: 2, x: 1 }); // 形状 C(属性顺序不同也算不同形状)
// ✅ 单态(monomorphic):始终用相同形状的对象调用,V8 可用内联缓存加速
function makePoint(x, y) {
return { x, y }; // 始终相同的属性、相同的顺序
}
getX(makePoint(1, 2));
getX(makePoint(3, 4));隐藏类(Hidden Class)与内联缓存
V8 为每个对象维护一个"隐藏类"(也叫 Shape 或 Map),描述对象的属性布局。属性顺序一致的对象共享同一隐藏类,V8 就能像访问 C 结构体一样按固定偏移量快速取值。破坏隐藏类共享,会让属性访问退化成慢速的字典查找。
// ❌ 破坏隐藏类:动态增删属性、属性顺序不一致
function Bad() {}
const a = new Bad();
a.x = 1;
a.y = 2;
const b = new Bad();
b.y = 2; // 顺序与 a 相反
b.x = 1; // a 和 b 隐藏类不同,无法共享优化
delete a.x; // delete 会让对象退化为字典模式,务必避免
// ✅ 稳定隐藏类:构造时一次性、按固定顺序初始化所有属性
class Good {
constructor(x, y) {
this.x = x; // 顺序固定
this.y = y;
this.visible = true; // 即使暂时用不到也先初始化,避免后续动态添加
}
}内联缓存(Inline Cache,IC) 是 V8 记住"上次这个属性访问命中了哪个隐藏类"的机制。单态访问命中 IC 极快;多态(2 到 4 种形状)稍慢;巨态(megamorphic,5 种以上)则彻底放弃缓存。实测显示,单态属性访问可比巨态快 5 到 10 倍。
实践要点:
数组的性能陷阱
V8 对数组有专门优化,会根据元素类型给数组打上"元素种类(elements kind)"标签,从快到慢依次是:`PACKED_SMI`(紧凑小整数)→ `PACKED_DOUBLE`(紧凑浮点)→ `PACKED_ELEMENTS`(紧凑任意对象)→ 对应的 `HOLEY`(带空洞)版本。种类只能单向降级,不能升级。
// ✅ 快:紧凑的小整数数组
const fast = [1, 2, 3, 4, 5]; // PACKED_SMI_ELEMENTS
// ❌ 制造空洞:数组退化为 HOLEY,所有操作变慢
const holey = [1, 2, 3];
holey[100] = 4; // 中间出现 97 个空洞,降级为 HOLEY_ELEMENTS
// ❌ 用 new Array(n) 预分配会立即产生 HOLEY 数组
const bad = new Array(1000); // 全是空洞
// ✅ 需要预分配时,用 fill 填充成紧凑数组
const good = new Array(1000).fill(0); // PACKED
// ❌ 混合类型让数组降级到最慢的 PACKED_ELEMENTS
const mixed = [1, 'two', { three: 3 }];
// ✅ 类型统一,各存各的
const nums = [1, 2, 3];
const strs = ['a', 'b', 'c'];遍历方式的性能: 对紧凑数字数组,传统 `for` 循环通常最快;`for...of` 次之;`forEach` 因每次迭代都要调用回调函数略慢。但对绝大多数业务代码,可读性优先,只在真正的性能热点(如每帧执行的循环、处理百万级数据)才做微观优化。
// 基准:遍历 1000 万个元素求和(相对耗时,仅供参考)
// for 循环 100ms 基准
// for...of 115ms 约慢 15%
// forEach 140ms 约慢 40%
// reduce 150ms 约慢 50%
// 结论:热点用 for,业务代码用可读性最好的写法字符串拼接的正确姿势
// ❌ 循环里用 += 拼接大量字符串,早期引擎会产生大量中间字符串
let html = '';
for (let i = 0; i < 100000; i++) {
html += `<li>${i}</li>`;
}
// ✅ 用数组收集再 join,语义清晰且稳定
const parts = [];
for (let i = 0; i < 100000; i++) {
parts.push(`<li>${i}</li>`);
}
const html = parts.join('');
// 说明:现代 V8 对 += 做了 ConsString 优化,两者差距已缩小,
// 但 join 方式内存占用更可控,且在其他引擎上更稳妥记忆化(Memoization)详解
记忆化是"用空间换时间":缓存函数对相同输入的计算结果,避免重复计算。适用于纯函数(相同输入必得相同输出、无副作用)和计算昂贵的场景。
// 通用记忆化高阶函数
function memoize(fn, resolver) {
const cache = new Map();
return function (...args) {
// resolver 生成缓存键;默认用第一个参数或 JSON 序列化
const key = resolver ? resolver(...args) : JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// 案例:斐波那契从指数级降到线性
const fib = memoize(function (n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
});
console.log(fib(40)); // 未记忆化时约上亿次调用,记忆化后仅几十次
// 案例:昂贵的格式化函数
const expensiveFormat = memoize((data) => heavyTransform(data));用 WeakMap 做基于对象的记忆化,避免内存泄漏:
const cache = new WeakMap();
function computeLayout(node) {
if (cache.has(node)) return cache.get(node);
const layout = expensiveLayoutCalc(node);
cache.set(node, layout); // node 被回收时,缓存自动清除
return layout;
}记忆化的注意事项: 缓存本身占内存,热点少、输入多变的函数记忆化反而得不偿失;要设上限(配合 LRU)或用 WeakMap;只对纯函数用,带副作用的函数记忆化会导致逻辑错误。
Core Web Vitals 逐项优化
前面列出了三大核心指标的阈值,下面给出每一项的具体优化手段。
LCP(最大内容绘制)优化——让主内容更快出现:
<!-- 1. 预连接到关键第三方域,提前完成 DNS/TCP/TLS 握手 -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="dns-prefetch" href="https://cdn.example.com">
<!-- 2. 预加载 LCP 图片,让它尽早开始下载 -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
<!-- 3. 给首屏关键图片标高优先级,非关键图懒加载 -->
<img src="/hero.webp" fetchpriority="high" alt="主图">
<img src="/below-fold.webp" loading="lazy" alt="下方图">LCP 优化清单:压缩并用现代格式(WebP/AVIF,体积比 JPEG 小 25% 到 50%)、用 CDN、服务端渲染或流式 HTML、内联关键 CSS、移除阻塞渲染的资源、避免 LCP 元素依赖 JS 才渲染。
CLS(累积布局偏移)优化——防止内容乱跳:
/* 1. 给图片/视频显式声明宽高或宽高比,浏览器提前占位 */
img { aspect-ratio: 16 / 9; width: 100%; height: auto; }
/* 2. 为异步加载的广告/嵌入内容预留固定空间 */
.ad-slot { min-height: 250px; }
/* 3. 字体用 font-display: optional/swap,并预加载,减少字体切换导致的偏移 */
@font-face {
font-family: 'Inter';
src: url('/inter.woff2') format('woff2');
font-display: optional;
}CLS 优化清单:图片视频永远带尺寸、动态插入内容别插在已有内容上方、动画只用 transform 不用会引起布局的属性、字体加载优化。
INP(交互到下次绘制)优化——让交互更跟手:
INP 衡量的是从用户交互到界面响应的延迟,核心是别让长任务阻塞主线程。
// ❌ 点击后同步跑一个大计算,界面卡住直到算完
button.addEventListener('click', () => {
const result = processHugeDataset(data); // 阻塞 500ms
render(result);
});
// ✅ 方案一:让出主线程,先响应交互再计算
button.addEventListener('click', async () => {
showLoading(); // 立即反馈
await new Promise(r => setTimeout(r, 0)); // 让浏览器先绘制
const result = processHugeDataset(data);
render(result);
});
// ✅ 方案二:用 scheduler.yield 主动让出(现代浏览器)
async function handleClick() {
showLoading();
if (window.scheduler?.yield) await scheduler.yield();
render(processHugeDataset(data));
}时间切片(Time Slicing)
处理大数据时,把一个长任务切成很多小片,每片之间让出主线程,保证交互不卡顿。这正是 React Fiber 可中断渲染背后的思想。
// 把一个可能耗时几秒的循环,切成不阻塞的小片
async function processInChunks(items, processFn, chunkSize = 100) {
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
chunk.forEach(processFn);
// 每处理一片就让出主线程一次
await new Promise(resolve => {
if (window.scheduler?.postTask) {
scheduler.postTask(resolve, { priority: 'user-blocking' });
} else {
setTimeout(resolve, 0);
}
});
}
}
// 用 requestIdleCallback 版本:利用浏览器空闲时间处理低优先级任务
function processWhenIdle(items, processFn) {
let i = 0;
function work(deadline) {
while (i < items.length && deadline.timeRemaining() > 1) {
processFn(items[i++]);
}
if (i < items.length) requestIdleCallback(work);
}
requestIdleCallback(work);
}虚拟滚动完整实现
长列表只渲染可视区域,是列表性能优化的终极武器。核心是根据滚动位置计算出应该渲染哪些项,并用一个占位容器撑起总高度以保持滚动条正确。
class VirtualList {
constructor(container, { itemHeight, items, renderItem, buffer = 5 }) {
this.container = container; // 可视容器(固定高度、overflow: auto)
this.itemHeight = itemHeight; // 每项固定高度
this.items = items;
this.renderItem = renderItem;
this.buffer = buffer; // 上下额外多渲染几项,避免快速滚动露白
// 撑起总高度的占位层
this.phantom = document.createElement('div');
this.phantom.style.height = `${items.length * itemHeight}px`;
// 真正承载可见项的层,用 transform 定位
this.content = document.createElement('div');
this.content.style.position = 'absolute';
this.content.style.top = '0';
this.content.style.width = '100%';
this.container.style.position = 'relative';
this.container.appendChild(this.phantom);
this.container.appendChild(this.content);
// 滚动用 rAF 节流
let ticking = false;
this.container.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => { this.render(); ticking = false; });
ticking = true;
}
});
this.render();
}
render() {
const scrollTop = this.container.scrollTop;
const viewHeight = this.container.clientHeight;
// 计算可见范围(加缓冲)
const start = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer);
const visibleCount = Math.ceil(viewHeight / this.itemHeight) + this.buffer * 2;
const end = Math.min(this.items.length, start + visibleCount);
// 只渲染这一段
this.content.style.transform = `translateY(${start * this.itemHeight}px)`;
this.content.innerHTML = '';
const fragment = document.createDocumentFragment();
for (let i = start; i < end; i++) {
const el = this.renderItem(this.items[i], i);
el.style.height = `${this.itemHeight}px`;
fragment.appendChild(el);
}
this.content.appendChild(fragment);
}
}
// 用法:10 万条数据,DOM 里始终只有约 20 个节点
new VirtualList(document.querySelector('#list'), {
itemHeight: 40,
items: Array.from({ length: 100000 }, (_, i) => `第 ${i} 行`),
renderItem: (text) => {
const div = document.createElement('div');
div.textContent = text;
return div;
},
});对于不定高度的列表,需要维护每项实际高度的缓存并动态测量,实现更复杂,可直接用成熟库(如 TanStack Virtual)。虚拟滚动在 10 万条数据下,内存占用可从数百 MB 降到几十 MB,首屏渲染从数秒降到百毫秒内。
资源加载优化:resource hints
浏览器提供一组"资源提示",让你主动指导浏览器的加载优先级和时机。
| 提示 | 作用 | 时机 |
| --- | --- | --- |
| preconnect | 提前建立到某域的连接(DNS+TCP+TLS) | 确定会用到的关键第三方域 |
| dns-prefetch | 仅提前做 DNS 解析(preconnect 的降级) | 可能用到的域 |
| preload | 高优先级提前加载当前页关键资源 | 首屏关键 CSS/字体/图片 |
| prefetch | 低优先级提前加载未来可能用的资源 | 下一个页面的资源 |
| modulepreload | 提前加载 ESM 模块及其依赖 | 关键 JS 模块 |
<link rel="preconnect" href="https://api.example.com" crossorigin>
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="prefetch" href="/next-page.js">
<link rel="modulepreload" href="/app.mjs">警惕滥用 preload: 什么都 preload 等于什么都不 preload,还会挤占带宽拖慢真正关键的资源。只 preload 首屏渲染必需、且发现较晚的资源。
图片与字体优化
图片通常是网页最大的流量来源(常占页面总字节的 50% 以上),优化收益极高。
<!-- 响应式图片:浏览器按屏幕和 DPR 选最合适的尺寸,避免大图小用 -->
<img
srcset="/img-400.webp 400w, /img-800.webp 800w, /img-1200.webp 1200w"
sizes="(max-width: 600px) 400px, 800px"
src="/img-800.webp"
loading="lazy"
decoding="async"
width="800" height="600"
alt="示例">
<!-- picture:按格式渐进降级,优先 AVIF,其次 WebP,最后 JPEG -->
<picture>
<source srcset="/photo.avif" type="image/avif">
<source srcset="/photo.webp" type="image/webp">
<img src="/photo.jpg" alt="照片" width="800" height="600">
</picture>图片优化清单:用 AVIF/WebP、按需生成多尺寸、懒加载非首屏图、显式宽高防 CLS、用 CDN 做实时压缩与格式协商、SVG 用于图标、雪碧图已被 HTTP/2 多路复用取代。字体优化:只加载用到的字重、用 woff2、subset 子集化(中文字体尤其重要,全量中文字体动辄几 MB)、`font-display: swap`、preload 关键字体。
网络层与 HTTP 缓存
前端性能有相当一部分取决于网络。合理的缓存策略能让重复访问几乎瞬开。
Cache-Control: max-age=31536000, immutable 带 hash 的静态资源,缓存一年
Cache-Control: no-cache HTML,每次校验(配合 ETag)
Cache-Control: private, max-age=0, must-revalidate 用户相关数据强缓存 vs 协商缓存: 强缓存(max-age 内)直接用本地副本,0 网络请求;过期后走协商缓存,带 `If-None-Match`(ETag)或 `If-Modified-Since` 问服务器,没变则返回 304(无 body,省流量)。
// 构建产物用内容 hash 命名,实现"永久强缓存 + 内容变了 URL 就变"
// app.a1b2c3.js ← 内容不变则文件名不变,命中缓存
// app.d4e5f6.js ← 内容一变文件名就变,强制拉新其他网络优化:开启 gzip/Brotli 压缩(文本类资源体积可降 70% 以上)、用 HTTP/2 或 HTTP/3 多路复用避免队头阻塞、减少请求数(但 HTTP/2 下不必过度合并)、用 CDN 就近分发、关键 API 服务端聚合减少往返。
代码分割与懒加载
打包体积直接决定首屏解析执行 JS 的时间。JS 是"字节换性能最贵"的资源——同样 100KB,图片只需解码,JS 却要下载、解析、编译、执行。
// 1. 路由级懒加载(React 示例)
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
);
}
// 2. 组件级懒加载:重型组件(图表、编辑器)按需加载
const Chart = lazy(() => import('./HeavyChart'));
// 3. 交互时才加载:点击才拉取重库
button.onclick = async () => {
const { default: Editor } = await import('./RichTextEditor');
new Editor(container);
};
// 4. 空闲时预加载:首屏渲染完成后,浏览器空闲时偷偷加载下个页面
requestIdleCallback(() => import('./NextPage'));分包策略: 把不常变的第三方依赖单独打成 vendor chunk(利用长期缓存)、把多个入口共享的模块提取成 common chunk、按路由分割业务代码。
打包体积分析与优化
# 可视化分析 bundle 构成,找出体积大户
npx vite-bundle-visualizer
# 或 webpack-bundle-analyzer
# 常见体积杀手与对策:
# - moment.js(含全部语言包,约 230KB)→ 换 dayjs(约 7KB)
# - lodash 全量引入 → 改 lodash-es 按需 + Tree-shaking
# - 引入整个 UI 库 → 按需导入 + 配 babel-plugin-import
# - 未压缩的图标字体 → 换 SVG 组件按需引入建立性能预算(Performance Budget): 给关键指标设硬性上限并在 CI 中卡住,防止性能慢慢劣化。
{
"budgets": [
{ "type": "initial", "maximumWarning": "200kb", "maximumError": "300kb" },
{ "type": "anyComponentStyle", "maximumWarning": "6kb" }
]
}Web Worker 池:复用线程
频繁创建销毁 Worker 有开销(每个 Worker 约几 MB 内存 + 启动时间),用 Worker 池复用。
class WorkerPool {
constructor(workerScript, size = navigator.hardwareConcurrency || 4) {
this.workers = [];
this.queue = []; // 待处理任务队列
this.idle = []; // 空闲 worker
for (let i = 0; i < size; i++) {
const w = new Worker(workerScript);
this.workers.push(w);
this.idle.push(w);
}
}
run(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
const worker = this.idle.pop();
if (worker) this._dispatch(worker, task);
else this.queue.push(task); // 无空闲则排队
});
}
_dispatch(worker, task) {
const onMessage = (e) => {
worker.removeEventListener('message', onMessage);
task.resolve(e.data);
// 处理完取下一个任务,或标记空闲
const next = this.queue.shift();
if (next) this._dispatch(worker, next);
else this.idle.push(worker);
};
worker.addEventListener('message', onMessage);
worker.postMessage(task.data);
}
}
// 用 CPU 核心数个线程并行处理任务
const pool = new WorkerPool('/compute.js');
const results = await Promise.all(bigTasks.map(t => pool.run(t)));WebAssembly:极限性能场景
对图像处理、音视频编解码、加密、物理模拟等 CPU 密集且算法固定的场景,WebAssembly(Wasm)能达到接近原生的速度(通常比 JS 快 1.5 到 20 倍,视场景而定)。
// 加载并调用 Wasm 模块
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/image-filter.wasm'),
{ env: { memory: new WebAssembly.Memory({ initial: 256 }) } }
);
// 调用导出的高性能函数(如高斯模糊)
instance.exports.gaussianBlur(ptr, width, height, radius);何时用 Wasm: 纯计算、无大量 JS/DOM 交互(跨边界调用有开销)、算法确定且计算量大。多数业务代码用不上,别为了炫技引入复杂度。
高性能动画:FLIP 技巧
直接对 `width`、`top`、`left` 做动画会触发重排,帧率低。FLIP(First-Last-Invert-Play)用 transform 模拟布局变化,全程只走合成层,稳定 60fps。
function flip(element, mutate) {
const first = element.getBoundingClientRect(); // First:记录初始位置
mutate(); // 执行真实的布局变更
const last = element.getBoundingClientRect(); // Last:记录结束位置
// Invert:用 transform 把元素"拉回"起点,视觉上没动
const dx = first.left - last.left;
const dy = first.top - last.top;
element.style.transform = `translate(${dx}px, ${dy}px)`;
element.style.transition = 'none';
requestAnimationFrame(() => {
// Play:清除 transform,让它平滑过渡到真实位置(GPU 加速)
element.style.transition = 'transform 300ms ease';
element.style.transform = '';
});
}动画性能守则: 只对 `transform` 和 `opacity` 做动画(不触发重排重绘);用 `will-change` 提前提升合成层但别滥用(每个合成层都占显存);避免同时动画大量元素;用 `content-visibility: auto` 让浏览器跳过屏幕外元素的渲染。
内存泄漏排查实战
内存泄漏的排查流程(Chrome DevTools Memory 面板):
1. 打开 Memory 面板,勾选 "Allocation instrumentation on timeline"
2. 执行怀疑泄漏的操作若干次(如反复打开关闭弹窗 10 次)
3. 强制 GC(点击垃圾桶图标)
4. 拍摄 Heap Snapshot,重复操作后再拍一张
5. 用 "Comparison" 视图对比两张快照,看哪些对象数量只增不减
6. 展开 Retainers(引用链),找到是谁在持有这些本该释放的对象四大泄漏元凶及自查:
// 1. 遗忘的定时器 —— 组件销毁忘记 clearInterval
// 2. 遗忘的事件监听 —— 忘记 removeEventListener(尤其 window/document 上的)
// 3. 脱离 DOM 的引用 —— 元素从 DOM 移除但 JS 变量还引用着它
let detached = document.getElementById('modal');
document.body.removeChild(detached);
// detached 仍被变量持有,无法回收;应 detached = null;
// 4. 无上限的缓存/闭包 —— Map 只增不删;改用 WeakMap 或 LRU
// 用 AbortController 统一管理监听器,一次性全部移除
const controller = new AbortController();
window.addEventListener('resize', onResize, { signal: controller.signal });
document.addEventListener('scroll', onScroll, { signal: controller.signal });
// 组件卸载时:controller.abort(); 上面所有监听一次清除真实用户监控(RUM)
实验室数据(Lighthouse)和真实用户数据往往差异巨大——用户设备更差、网络更慢、场景更复杂。用 web-vitals 库采集真实指标上报。
import { onLCP, onINP, onCLS, onTTFB, onFCP } from 'web-vitals';
function report(metric) {
// 用 sendBeacon 上报,即使页面关闭也能可靠送达
navigator.sendBeacon('/rum', JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
id: metric.id,
page: location.pathname,
}));
}
onLCP(report);
onINP(report);
onCLS(report);
onTTFB(report);
onFCP(report);分析时看 P75(75 分位) 而非平均值——Core Web Vitals 官方就用 P75 作为达标口径,因为平均值会被少数极端值掩盖真实体验。
性能优化决策流程
面对性能问题,别凭感觉乱优化,遵循科学流程:
1. 测量:用 Lighthouse / DevTools / RUM 定位到底哪里慢、慢多少
2. 归因:是网络(TTFB 高)、还是 JS(长任务多)、还是渲染(重排多)?
3. 排序:按"影响面 × 收益 ÷ 成本"排优先级,先啃最大的瓶颈
4. 优化:一次只改一个变量
5. 验证:改完再测,用数据确认真的变快了(有时会适得其反)
6. 固化:加性能预算和 CI 卡点,防止回退常见性能反模式速查
| 反模式 | 问题 | 正解 |
| --- | --- | --- |
| 过早优化 | 浪费时间优化非瓶颈,增加复杂度 | 先测量后优化 |
| 循环内 DOM 操作 | 大量重排 | DocumentFragment 批量 |
| 布局抖动 | 读写交替强制同步布局 | 读写分离 |
| 无节流的高频事件 | 主线程被打满 | 防抖/节流 + rAF |
| 全量渲染长列表 | 内存爆、卡顿 | 虚拟滚动 |
| 无上限缓存 | 内存泄漏 | LRU / WeakMap |
| 主线程跑重计算 | 界面冻结、INP 差 | Web Worker / 时间切片 |
| 加载全部代码 | 首屏 JS 过大 | 代码分割 + 懒加载 |
| 用 top/left 做动画 | 掉帧 | transform + opacity |
| delete 对象属性 | 破坏隐藏类,属性访问变慢 | 设为 null 或用 Map |
首屏渲染策略:CSR、SSR、SSG、ISR
首屏性能很大程度取决于渲染策略。同一个应用,选错渲染方式,LCP 可能相差数秒。
| 策略 | 全称 | 首屏 | SEO | 适用 |
| --- | --- | --- | --- | --- |
| CSR | 客户端渲染 | 慢(等 JS) | 差 | 后台管理、内网应用 |
| SSR | 服务端渲染 | 快 | 好 | 内容型、需登录态的动态页 |
| SSG | 静态站点生成 | 极快 | 好 | 博客、文档、营销页 |
| ISR | 增量静态再生 | 极快 | 好 | 电商、需定期更新的大量页面 |
// CSR 的问题:浏览器拿到的是空壳,必须等 JS 下载执行完才有内容
// <div id="root"></div> ← 首屏白屏,LCP 依赖 JS
// SSR:服务端直接吐出带内容的 HTML,浏览器立刻能显示,JS 再来"注水"(hydration)
// Next.js 示例
export async function getServerSideProps() {
const data = await fetchData(); // 服务端拿数据
return { props: { data } }; // 随 HTML 一起下发,首屏即有内容
}
// SSG:构建时就生成好静态 HTML,请求时直接返回,最快
export async function getStaticProps() {
return { props: { posts: await getPosts() } };
}
// ISR:静态 + 定期后台再生,兼顾速度与新鲜度
export async function getStaticProps() {
return { props: { data: await getData() }, revalidate: 60 }; // 60 秒后台重新生成
}注水(hydration)是 SSR 的性能痛点: 服务端渲染出 HTML 后,客户端还要下载全量 JS 并重新"接管"整个页面,这段时间页面可见但不可交互(TTI 滞后于 FCP)。缓解手段:选择性注水(selective hydration)、渐进式注水、岛屿架构(islands architecture,只对交互组件注水)、React Server Components(服务端组件不占客户端 JS)。
关键渲染路径优化
浏览器把 HTML 变成像素要经历:解析 HTML 构建 DOM → 解析 CSS 构建 CSSOM → 合并成渲染树 → 布局 → 绘制。CSS 会阻塞渲染,JS 默认会阻塞解析,优化关键渲染路径就是缩短"首次绘制前必须完成的工作"。
<!-- 1. 内联首屏关键 CSS,避免额外请求阻塞渲染 -->
<style>/* 首屏必需的关键样式 */</style>
<!-- 非关键 CSS 异步加载,不阻塞渲染 -->
<link rel="preload" href="/rest.css" as="style" onload="this.rel='stylesheet'">
<!-- 2. JS 用 defer/async 避免阻塞 HTML 解析 -->
<!-- defer:下载不阻塞解析,按顺序在 DOMContentLoaded 前执行 -->
<script defer src="/app.js"></script>
<!-- async:下载不阻塞,下载完立即执行(顺序不保证),适合独立的第三方脚本 -->
<script async src="/analytics.js"></script>defer vs async 抉择: 有依赖关系、需操作 DOM 的用 defer;完全独立的埋点/广告用 async;现代 ESM `type="module"` 默认就是 defer 行为。
Service Worker 缓存策略详解
Service Worker 让你完全掌控网络请求,不同资源该用不同策略。
// 1. Cache First(缓存优先):静态资源,命中就用,最快
async function cacheFirst(request) {
const cached = await caches.match(request);
return cached || fetch(request);
}
// 2. Network First(网络优先):API 数据,优先拿最新,失败降级缓存
async function networkFirst(request) {
try {
const fresh = await fetch(request);
const cache = await caches.open('api');
cache.put(request, fresh.clone()); // 顺便更新缓存
return fresh;
} catch {
return caches.match(request); // 离线时用缓存兜底
}
}
// 3. Stale While Revalidate(先返回缓存,后台更新):兼顾速度与新鲜
async function staleWhileRevalidate(request) {
const cached = await caches.match(request);
const fetchPromise = fetch(request).then(fresh => {
caches.open('swr').then(c => c.put(request, fresh.clone()));
return fresh;
});
return cached || fetchPromise; // 有缓存立即返回,同时后台刷新
}
self.addEventListener('fetch', (e) => {
const url = new URL(e.request.url);
if (url.pathname.startsWith('/api/')) {
e.respondWith(networkFirst(e.request));
} else if (url.pathname.match(/\.(js|css|woff2|png|webp)$/)) {
e.respondWith(cacheFirst(e.request));
} else {
e.respondWith(staleWhileRevalidate(e.request));
}
});缓存更新难题: Service Worker 本身会被浏览器缓存,改版后要靠版本化缓存名 + `skipWaiting()` + `clients.claim()` 让新版本尽快生效,并在 activate 时清理旧缓存。
const CACHE_VERSION = 'v2';
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_VERSION).map(k => caches.delete(k)))
)
);
});框架层面的性能优化(React 示例)
框架的重渲染是常见性能杀手。核心原则是减少不必要的重渲染。
// 1. memo:props 不变则跳过重渲染
const ExpensiveItem = React.memo(function Item({ data }) {
return <div>{heavyRender(data)}</div>;
});
// 2. useMemo:缓存昂贵计算结果
const sorted = useMemo(() => hugeList.sort(compareFn), [hugeList]);
// 3. useCallback:缓存函数引用,避免子组件因函数变化而重渲染
const handleClick = useCallback((id) => dispatch(remove(id)), [dispatch]);
// 4. 用 key 帮助高效 diff,别用数组索引当动态列表的 key
{items.map(item => <Row key={item.id} item={item} />)}
// 5. 状态下沉/内容提升,缩小重渲染范围
// 把频繁变化的状态限制在最小的组件里,避免整棵树重渲染框架优化通用原则:
用 performance API 精准测量
除了 `performance.now()`,还有一套标记与测量 API,能在时间轴上标注自定义区间。
// User Timing:给关键阶段打标记,DevTools Performance 面板可视化
performance.mark('fetch-start');
await fetchData();
performance.mark('fetch-end');
performance.measure('数据加载', 'fetch-start', 'fetch-end');
const [entry] = performance.getEntriesByName('数据加载');
console.log(`数据加载耗时 ${entry.duration.toFixed(1)}ms`);
// Navigation Timing:拿到导航各阶段耗时
const nav = performance.getEntriesByType('navigation')[0];
console.log('TTFB:', nav.responseStart - nav.requestStart);
console.log('DOM 解析:', nav.domInteractive - nav.responseEnd);
console.log('总加载:', nav.loadEventEnd - nav.startTime);
// Resource Timing:分析每个资源的加载耗时,揪出慢资源
performance.getEntriesByType('resource')
.filter(r => r.duration > 500)
.forEach(r => console.warn('慢资源:', r.name, r.duration.toFixed(0), 'ms'));减少主线程工作量的调度器 API
新的 Scheduler API 让你能按优先级调度任务,把不紧急的工作往后放。
// postTask:按优先级调度('user-blocking' > 'user-visible' > 'background')
scheduler.postTask(() => updateCriticalUI(), { priority: 'user-blocking' });
scheduler.postTask(() => sendAnalytics(), { priority: 'background' });
// yield:在长任务中主动让出主线程,让高优先级的交互先处理
async function processLargeList(items) {
for (const item of items) {
doWork(item);
if (navigator.scheduling?.isInputPending?.()) {
await scheduler.yield(); // 有待处理输入时立即让出
}
}
}
// isInputPending:检测是否有待处理的用户输入,有则让路
function longLoop() {
while (tasks.length) {
if (navigator.scheduling.isInputPending()) {
setTimeout(longLoop, 0); // 让出,下次继续
return;
}
doTask(tasks.shift());
}
}微观优化基准数据参考
下面是一些常见操作的相对性能参考(不同引擎/版本会有差异,仅示意数量级):
| 操作 | 相对耗时 | 说明 |
| --- | --- | --- |
| 单态属性访问 | 1x | IC 命中,最快 |
| 巨态属性访问 | 5-10x | IC 失效 |
| Map.get vs 对象查找(大量键) | 相当或更快 | 键很多时 Map 更稳 |
| 数组 includes(1万条) | 100x | 相比 Set.has |
| Set.has | 1x | O(1) |
| JSON.parse/stringify(大对象) | 高 | 序列化是常见瓶颈 |
| 结构化克隆大对象 | 高 | Worker 通信考虑 Transferable |
| 正则回溯(灾难性) | 可达指数级 | 警惕 ReDoS |
警惕灾难性回溯(ReDoS): 某些正则遇到特定输入会指数级变慢,既是性能问题也是安全问题。
// ❌ 危险正则:嵌套量词,遇到 'aaaa...a!' 会指数级回溯
const bad = /^(a+)+$/;
// ✅ 避免嵌套量词,或用非回溯写法、限制输入长度
const good = /^a+$/;移动端与低端设备优化
实验室里在高端机上一切流畅,真实用户的中低端安卓机可能慢 5 到 10 倍。针对性优化:
// passive 监听器:告诉浏览器不会 preventDefault,滚动无需等待,更跟手
window.addEventListener('touchstart', onTouch, { passive: true });
window.addEventListener('wheel', onWheel, { passive: true });数据处理性能:批处理与惰性求值
处理大数据集时,链式调用多个数组方法会产生多个中间数组,遍历多遍。改用一次遍历或惰性求值可显著提速。
// ❌ 三次遍历、两个中间数组
const result = bigArray
.filter(x => x.active) // 遍历 1,产生中间数组
.map(x => x.value * 2) // 遍历 2,产生中间数组
.filter(x => x > 10); // 遍历 3
// ✅ 一次遍历,无中间数组
const result = [];
for (const x of bigArray) {
if (!x.active) continue;
const v = x.value * 2;
if (v > 10) result.push(v);
}
// ✅ 惰性求值(生成器):按需计算,适合超大或无限序列、提前 break 的场景
function* pipeline(source) {
for (const x of source) {
if (!x.active) continue;
const v = x.value * 2;
if (v > 10) yield v;
}
}
// 只取前 5 个就停,后面的元素根本不会被计算
let count = 0;
for (const v of pipeline(bigArray)) {
console.log(v);
if (++count >= 5) break;
}计算缓存与增量更新
对于"输入小幅变化、输出大量重算"的场景,增量更新(只重算变化的部分)远胜全量重算。
// ❌ 每次都全量重算总价
function getTotal(items) {
return items.reduce((sum, i) => sum + i.price * i.qty, 0);
}
// ✅ 维护一个总和,增删改时增量更新,O(1) 而非 O(n)
class Cart {
constructor() { this.items = new Map(); this.total = 0; }
add(item) {
this.items.set(item.id, item);
this.total += item.price * item.qty; // 增量
}
remove(id) {
const item = this.items.get(id);
if (item) {
this.total -= item.price * item.qty; // 增量
this.items.delete(id);
}
}
getTotal() { return this.total; } // 直接读,无需遍历
}这正是响应式框架(Vue 的计算属性、MobX 的 computed)背后的思想:依赖不变就复用上次结果,依赖变了才重算。
最佳实践与总结
| 优化维度 | 核心手段 | 关键指标 |
| --- | --- | --- |
| 代码执行 | 数据结构选型、减少全局变量、循环优化 | 函数执行耗时 |
| 内存 | 清理引用、WeakMap、对象池、LRU 缓存 | 堆内存大小 |
| 异步 | Promise、并发控制、防抖节流 | INP、请求数 |
| 渲染 | 读写分离、transform 动画、虚拟滚动 | LCP、CLS、帧率 |
| 工具 | DevTools、Lighthouse、Web Vitals | Core Web Vitals |
记住一句话:性能优化的第一原则是先测量、后优化,永远不要过早优化。 用数据说话,把精力花在真正的瓶颈上。