JavaScript 性能优化最佳实践

中等 🟡Js/Ts
6 个标签
预计阅读时间:76 分钟
JavaScript性能优化内存执行速度Web Vitals防抖节流

JavaScript 性能优化最佳实践

JavaScript 性能直接影响应用的用户体验。一个高性能的 JavaScript 应用应该具备快速响应、流畅动画、低内存占用等特点。性能优化是一个持续的过程,需要在开发的各个阶段关注。以下是一些关键的性能优化策略,涵盖代码优化、内存管理、异步处理、DOM 操作等多个方面。

为什么性能如此重要

性能不是锦上添花,而是直接关乎生意的核心指标。可以打个比方:网页加载就像餐厅上菜,顾客等得越久越不耐烦,超过一定时间就直接走人。业界有大量真实数据佐证:

页面加载时间从 1 秒增加到 3 秒,跳出率上升约 32%;增加到 5 秒,跳出率翻倍。
亚马逊测算过,页面每慢 100 毫秒,销售额下降约 1%。
谷歌发现搜索结果慢 500 毫秒,流量下降 20%。
Pinterest 将等待时间缩短 40% 后,搜索引擎流量和注册量提升了 15%。

如今谷歌把 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 |

代码优化

变量声明与作用域:

使用 let 和 const 代替 var,利用块级作用域避免变量提升问题
减少全局变量的使用,全局变量会污染全局命名空间,增加查找时间
合理使用闭包,闭包会保持对外部变量的引用,可能导致内存泄漏
javascriptCode
// ❌ 避免:全局变量污染
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;
}

函数优化:

避免在循环中定义函数,每次迭代都会创建新的函数对象
使用箭头函数简化代码,但注意 this 绑定问题
合理使用函数柯里化,实现函数复用和延迟计算
javascriptCode
// ❌ 避免:循环中创建函数
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 循环代替 forEach,for 循环性能更好
缓存数组长度,避免每次迭代都计算
避免在循环中进行 DOM 操作,使用 DocumentFragment 批量操作
javascriptCode
// ❌ 避免:每次迭代都计算长度
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); // 只触发一次重排

条件优化:

使用 switch 代替多个 if-else,switch 使用跳转表优化
将最常见的条件放在前面,减少判断次数
使用对象字面量代替条件判断,提高可读性和性能
javascriptCode
// ❌ 避免:多个 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))。

javascriptCode
// ❌ 数组查找:每次 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)。理解内存管理是写出长时间稳定运行应用的关键。

内存泄漏常见原因与预防:

避免循环引用,特别是 DOM 元素与 JavaScript 对象之间的引用
清理定时器和事件监听器,组件销毁时必须清理
释放不再使用的对象引用,帮助垃圾回收器回收内存
javascriptCode
// ❌ 避免:未清理的定时器
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;
  };
}

垃圾回收机制理解:

JavaScript 使用标记清除(Mark-and-Sweep)算法进行垃圾回收:从根对象(全局对象、当前调用栈)出发标记所有可达对象,未被标记的即为垃圾并回收
V8 引擎采用分代回收:新生代(Scavenge 算法,回收频繁的短命对象)和老生代(标记清除 + 标记整理)
避免创建过多的临时对象,减少 GC 压力(GC 执行时会短暂暂停主线程,即 Stop-The-World)
使用 WeakMap 和 WeakSet 存储对象引用,不阻止垃圾回收
javascriptCode
// 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);
  // 处理对象
}

内存优化技巧:

使用对象池复用对象,减少对象创建和销毁的开销
合理使用缓存,但要注意缓存大小和过期策略
减少内存分配,重用数组和对象
javascriptCode
// 对象池模式
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 缓存: 缓存能提速,但无上限的缓存本身就是内存泄漏源头。

javascriptCode
// 简单的 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 是单线程的,长时间的同步任务会阻塞主线程,导致页面"假死"。异步优化的核心就是把耗时操作从主线程上挪开,保持界面流畅。

异步操作:

使用 Promise 和 async/await,避免回调地狱
合理控制并发数量,避免同时发起过多请求压垮服务器或浏览器
javascriptCode
// 并发控制:限制同时进行的异步任务数量
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)。节流:固定时间间隔最多执行一次(适合滚动、拖拽)。

javascriptCode
// 防抖:连续触发只在最后一次后 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 处理密集计算:

javascriptCode
// 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 操作:

减少 DOM 操作次数,读写分离避免强制同步布局(Layout Thrashing)
使用 DocumentFragment 批量插入
批量更新 DOM,缓存 DOM 查询结果
javascriptCode
// ❌ 布局抖动:交替读写触发多次强制重排
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';          // 批量写
});

渲染优化:

避免频繁触发重排和重绘,优先修改 transform 和 opacity(只走合成层,不触发重排重绘)
使用 CSS 动画代替 JavaScript 动画(浏览器可优化到 GPU)
用 `will-change` 或 `transform: translateZ(0)` 提升元素到独立合成层
长列表用虚拟滚动(virtual scrolling),只渲染可视区域的 DOM

事件处理:

使用事件委托:把子元素的监听器统一挂到父元素,利用事件冒泡,减少监听器数量
javascriptCode
// ❌ 给 1000 个 li 各绑一个监听器
document.querySelectorAll('li').forEach(li => {
  li.addEventListener('click', handleClick);
});

// ✅ 事件委托:只在父元素绑一个
document.querySelector('ul').addEventListener('click', e => {
  if (e.target.tagName === 'LI') handleClick(e);
});

工具和监控

性能分析:

使用 Chrome DevTools 的 Performance 面板分析执行时间、火焰图、长任务
用 Memory 面板抓堆快照对比,定位内存泄漏
用 `performance.now()` 做精确到微秒的代码计时
javascriptCode
// 精确测量代码执行耗时
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.虚拟滚动:只渲染可视区域约 20 条 DOM,滚动时动态替换内容。DOM 节点从 1 万降到约 30,首屏渲染从 3 秒降到 200 毫秒。
2.滚动事件节流:滚动回调用 requestAnimationFrame 节流,滚动帧率从 20fps 提升到稳定 60fps。
3.数据处理移出主线程:排序和筛选放到 Web Worker,交互不再卡顿。

优化后,同样 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)。

javascriptCode
// ❌ 多态(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 结构体一样按固定偏移量快速取值。破坏隐藏类共享,会让属性访问退化成慢速的字典查找。

javascriptCode
// ❌ 破坏隐藏类:动态增删属性、属性顺序不一致
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 倍。

实践要点:

对象一经创建就固定"形状",别后期动态加属性
绝不使用 `delete`,要"删除"就设为 `null` 或用 Map
数组保持元素类型一致(全是数字,别混入对象或空洞)

数组的性能陷阱

V8 对数组有专门优化,会根据元素类型给数组打上"元素种类(elements kind)"标签,从快到慢依次是:`PACKED_SMI`(紧凑小整数)→ `PACKED_DOUBLE`(紧凑浮点)→ `PACKED_ELEMENTS`(紧凑任意对象)→ 对应的 `HOLEY`(带空洞)版本。种类只能单向降级,不能升级。

javascriptCode
// ✅ 快:紧凑的小整数数组
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` 因每次迭代都要调用回调函数略慢。但对绝大多数业务代码,可读性优先,只在真正的性能热点(如每帧执行的循环、处理百万级数据)才做微观优化。

javascriptCode
// 基准:遍历 1000 万个元素求和(相对耗时,仅供参考)
// for 循环         100ms  基准
// for...of         115ms  约慢 15%
// forEach          140ms  约慢 40%
// reduce           150ms  约慢 50%
// 结论:热点用 for,业务代码用可读性最好的写法

字符串拼接的正确姿势

javascriptCode
// ❌ 循环里用 += 拼接大量字符串,早期引擎会产生大量中间字符串
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)详解

记忆化是"用空间换时间":缓存函数对相同输入的计算结果,避免重复计算。适用于纯函数(相同输入必得相同输出、无副作用)和计算昂贵的场景。

javascriptCode
// 通用记忆化高阶函数
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 做基于对象的记忆化,避免内存泄漏:

javascriptCode
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(最大内容绘制)优化——让主内容更快出现:

htmlCode
<!-- 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(累积布局偏移)优化——防止内容乱跳:

cssCode
/* 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 衡量的是从用户交互到界面响应的延迟,核心是别让长任务阻塞主线程

javascriptCode
// ❌ 点击后同步跑一个大计算,界面卡住直到算完
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 可中断渲染背后的思想。

javascriptCode
// 把一个可能耗时几秒的循环,切成不阻塞的小片
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);
}

虚拟滚动完整实现

长列表只渲染可视区域,是列表性能优化的终极武器。核心是根据滚动位置计算出应该渲染哪些项,并用一个占位容器撑起总高度以保持滚动条正确。

javascriptCode
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 模块 |

htmlCode
<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% 以上),优化收益极高。

htmlCode
<!-- 响应式图片:浏览器按屏幕和 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 缓存

前端性能有相当一部分取决于网络。合理的缓存策略能让重复访问几乎瞬开。

textCode
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,省流量)。

javascriptCode
// 构建产物用内容 hash 命名,实现"永久强缓存 + 内容变了 URL 就变"
// app.a1b2c3.js   ← 内容不变则文件名不变,命中缓存
// app.d4e5f6.js   ← 内容一变文件名就变,强制拉新

其他网络优化:开启 gzip/Brotli 压缩(文本类资源体积可降 70% 以上)、用 HTTP/2 或 HTTP/3 多路复用避免队头阻塞、减少请求数(但 HTTP/2 下不必过度合并)、用 CDN 就近分发、关键 API 服务端聚合减少往返。

代码分割与懒加载

打包体积直接决定首屏解析执行 JS 的时间。JS 是"字节换性能最贵"的资源——同样 100KB,图片只需解码,JS 却要下载、解析、编译、执行。

javascriptCode
// 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、按路由分割业务代码。

打包体积分析与优化

bashCode
# 可视化分析 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 中卡住,防止性能慢慢劣化。

jsonCode
{
  "budgets": [
    { "type": "initial", "maximumWarning": "200kb", "maximumError": "300kb" },
    { "type": "anyComponentStyle", "maximumWarning": "6kb" }
  ]
}

Web Worker 池:复用线程

频繁创建销毁 Worker 有开销(每个 Worker 约几 MB 内存 + 启动时间),用 Worker 池复用。

javascriptCode
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 倍,视场景而定)。

javascriptCode
// 加载并调用 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。

javascriptCode
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 面板):

textCode
1. 打开 Memory 面板,勾选 "Allocation instrumentation on timeline"
2. 执行怀疑泄漏的操作若干次(如反复打开关闭弹窗 10 次)
3. 强制 GC(点击垃圾桶图标)
4. 拍摄 Heap Snapshot,重复操作后再拍一张
5. 用 "Comparison" 视图对比两张快照,看哪些对象数量只增不减
6. 展开 Retainers(引用链),找到是谁在持有这些本该释放的对象

四大泄漏元凶及自查:

javascriptCode
// 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 库采集真实指标上报。

javascriptCode
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 作为达标口径,因为平均值会被少数极端值掩盖真实体验。

性能优化决策流程

面对性能问题,别凭感觉乱优化,遵循科学流程:

textCode
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 | 增量静态再生 | 极快 | 好 | 电商、需定期更新的大量页面 |

javascriptCode
// 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 默认会阻塞解析,优化关键渲染路径就是缩短"首次绘制前必须完成的工作"。

htmlCode
<!-- 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 让你完全掌控网络请求,不同资源该用不同策略。

javascriptCode
// 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 时清理旧缓存。

javascriptCode
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 示例)

框架的重渲染是常见性能杀手。核心原则是减少不必要的重渲染

javascriptCode
// 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. 状态下沉/内容提升,缩小重渲染范围
// 把频繁变化的状态限制在最小的组件里,避免整棵树重渲染

框架优化通用原则:

拆分组件,让状态变化只影响最小范围
列表用稳定唯一的 key
避免在 render 里创建新对象/新函数(会破坏 memo)
大列表用虚拟滚动
用 Profiler / DevTools 找出重渲染热点,别盲目加 memo(memo 本身也有成本)

用 performance API 精准测量

除了 `performance.now()`,还有一套标记与测量 API,能在时间轴上标注自定义区间。

javascriptCode
// 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 让你能按优先级调度任务,把不紧急的工作往后放。

javascriptCode
// 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): 某些正则遇到特定输入会指数级变慢,既是性能问题也是安全问题。

javascriptCode
// ❌ 危险正则:嵌套量词,遇到 'aaaa...a!' 会指数级回溯
const bad = /^(a+)+$/;

// ✅ 避免嵌套量词,或用非回溯写法、限制输入长度
const good = /^a+$/;

移动端与低端设备优化

实验室里在高端机上一切流畅,真实用户的中低端安卓机可能慢 5 到 10 倍。针对性优化:

用 Network Information API 检测弱网,降级加载低清资源
用 `navigator.hardwareConcurrency` 感知 CPU 核心数,动态调整 Worker 数量
减少 JS 体积(低端机 JS 解析执行是主要瓶颈)
用 `content-visibility: auto` 跳过屏幕外渲染
触摸事件监听器加 `{ passive: true }`,避免阻塞滚动
javascriptCode
// passive 监听器:告诉浏览器不会 preventDefault,滚动无需等待,更跟手
window.addEventListener('touchstart', onTouch, { passive: true });
window.addEventListener('wheel', onWheel, { passive: true });

数据处理性能:批处理与惰性求值

处理大数据集时,链式调用多个数组方法会产生多个中间数组,遍历多遍。改用一次遍历或惰性求值可显著提速。

javascriptCode
// ❌ 三次遍历、两个中间数组
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;
}

计算缓存与增量更新

对于"输入小幅变化、输出大量重算"的场景,增量更新(只重算变化的部分)远胜全量重算。

javascriptCode
// ❌ 每次都全量重算总价
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)背后的思想:依赖不变就复用上次结果,依赖变了才重算。

最佳实践与总结

编写高效的算法,优先选择合适的数据结构(Map/Set 查找、避免嵌套循环)
避免不必要的计算,善用缓存与记忆化(memoization)
优化关键渲染路径,减少重排重绘
高频事件必做防抖/节流,密集计算交给 Web Worker
长列表用虚拟滚动,大应用做代码分割与懒加载
及时清理定时器、事件监听器和引用,杜绝内存泄漏
先测量后优化:不要凭感觉优化,用 DevTools 和真实指标定位瓶颈
性能优化是持续过程:上线后用 RUM 持续监控,及时发现回退

| 优化维度 | 核心手段 | 关键指标 |

| --- | --- | --- |

| 代码执行 | 数据结构选型、减少全局变量、循环优化 | 函数执行耗时 |

| 内存 | 清理引用、WeakMap、对象池、LRU 缓存 | 堆内存大小 |

| 异步 | Promise、并发控制、防抖节流 | INP、请求数 |

| 渲染 | 读写分离、transform 动画、虚拟滚动 | LCP、CLS、帧率 |

| 工具 | DevTools、Lighthouse、Web Vitals | Core Web Vitals |

记住一句话:性能优化的第一原则是先测量、后优化,永远不要过早优化。 用数据说话,把精力花在真正的瓶颈上。