前置知识: JavaScript、JavaScript、JavaScript

调试与性能优化

15 min高级

浏览器 DevTools、性能分析模型、内存泄漏排查、火焰图解读、Core Web Vitals 治理与生产级性能优化实践的形式化定义与工程指南。

前置知识

学习目标

  • 掌握「1. 引言」的核心机制、典型用法与常见陷阱
  • 掌握「2. 历史动机与背景」的核心机制、典型用法与常见陷阱
  • 掌握「3. 形式化定义」的核心机制、典型用法与常见陷阱
  • 掌握「4. 理论推导」的核心机制、典型用法与常见陷阱
  • 掌握「5. 代码示例」的核心机制、典型用法与常见陷阱

1. 引言

调试(Debugging)与性能优化(Performance Optimization)是软件工程中最具工程性、最依赖工具链与形式化模型的两个领域。调试的本质是从观测到的异常状态回溯到根因状态的逆向推理过程;性能优化的本质是在有限资源约束下最大化用户感知质量的优化问题。两者共同的特征是:不能依靠经验直觉,必须以可观测数据(Observability)为基础,以形式化模型为指引,以工具链为手段。

现代 Web 应用的调试与性能优化已远超”打 console.log + 加 setTimeout”的初级阶段。它涉及 Chrome DevTools Protocol (CDP) 的远程调试、Performance 火焰图的样本采样分析、Memory 堆快照的保留路径(Retaining Path)追踪、Core Web Vitals 的 RUM (Real User Monitoring) 与 Lighthouse 的合成监控(Synthetic Monitoring)、以及基于 V8 TurboFan 与 Sparkplug 编译器的 JavaScript 执行模型理解。本文档从历史演进、形式化定义、理论推导、工程实践、陷阱分析、案例研究六个维度系统讲解现代 JavaScript 调试与性能优化的核心方法论。

2. 历史动机与背景

2.1 调试与性能工具演化时间线

年份关键里程碑解决的核心问题
1995Netscape 引入 javascript: 伪协议与 alert() 作为最早调试手段无源码级调试,仅靠弹窗查看变量
2001Firefox 前身 Phoenix 集成 DOM Inspector最早的元素检视工具
2006Firebug 发布,首次提供 Console、Net、Script、CSS 多面板奠定现代 DevTools 多面板形态
2008V8 引擎发布,引入分层编译(Baseline + Crankshaft)提升 JS 执行性能,间接推动性能分析需求
2010Chrome DevTools 集成 Timeline 与 Heap Profiler时间线与内存分析首次合流
2012Chrome DevTools Protocol (CDP) 稳定化支持远程调试与自动化测试(Puppeteer 的基础)
2015V8 引入 Ignition 解释器 + TurboFan 优化编译器启动速度提升 2 倍,推动”先解释后优化”的调试模型
2017Chrome 63 引入 Performance Observer API标准化性能指标采集,替代手写 Performance API 调用
2018Lighthouse 3.0 集成至 Chrome DevTools性能审计工具大众化
2020Core Web Vitals(LCP、FID、CLS)正式发布Google 搜索排名开始考虑性能指标
2021V8 引入 Sparkplug 中间编译层缩短 Ignition → TurboFan 的优化间隙
2022Chrome 107 引入 INP 作为 FID 的实验性替代指标更准确的交互响应度测量
2023INP 成为 Core Web Vitals 候选稳定指标替代 FID,更精确反映交互延迟
2024Chrome DevTools 引入 AI 辅助性能分析自动识别长任务与性能反模式

2.2 设计动机分析

现代调试与性能工具的设计遵循三条主线:

  1. 从断点到采样:早期调试依赖断点(断点暂停主线程,破坏时序);现代性能分析依赖样本采样(每 100μs 采集一次调用栈),不暂停主线程。这一转变源于对”观测者效应”(Observer Effect)的认知——调试工具本身不应改变被测系统的行为。
  2. 从经验到指标:早期性能优化依赖工程师经验(“我觉得这样写更快”);现代性能优化依赖 RUM 与 Core Web Vitals 等标准化指标,使性能可量化、可对比、可回归。
  3. 从单点到全链路:早期调试聚焦单条语句;现代调试关注整个渲染管线(JS → Style → Layout → Paint → Composite)与网络瀑布(DNS → TCP → TLS → Request → Response → Parse → Execute)。

理解这三条主线,可以解释为什么现代性能优化方法论强调”Measure, Don’t Guess”——任何优化假设都必须用数据验证,任何”直觉优化”都可能导致反优化。

3. 形式化定义

3.1 调试的形式化定义

调试是一个逆向推理过程。设观测到的异常状态为 OO,根因状态为 RR,调试是寻找映射 ff 使得 f(R)=Of(R) = O:

Debug:O→R(求逆映射)\text{Debug}: O \rightarrow R \text{(求逆映射)}

由于程序执行的不确定性(并发、异步、随机性),逆映射 f−1f^{-1} 不一定唯一,调试过程是搜索:

R=arg⁡min⁡r∈Hypothesescost(r) s.t. f(r)=OR = \arg\min_{r \in \text{Hypotheses}} \text{cost}(r) \text{ s.t. } f(r) = O

其中 cost(r)\text{cost}(r) 表示根因 rr 的”假设代价”(需要多少额外证据支持)。奥卡姆剃刀原则要求选择最小代价的根因。

3.2 性能优化的形式化定义

设应用资源消耗为 C=(T,M,N,E)C = (T, M, N, E),其中 TT 为时间、MM 为内存、NN 为网络、EE 为能耗。性能优化是在约束条件下最小化资源消耗:

Optimize:min⁡C s.t. QoE(C)≥Threshold\text{Optimize}: \min C \text{ s.t. } \text{QoE}(C) \geq \text{Threshold}

其中 QoE\text{QoE} (Quality of Experience) 是用户感知质量的函数。对于 Web 应用,QoE 通常由 Core Web Vitals 决定:

QoE=w1⋅LCP+w2⋅INP+w3⋅CLS\text{QoE} = w_1 \cdot \text{LCP} + w_2 \cdot \text{INP} + w_3 \cdot \text{CLS}

其中 w1,w2,w3w_1, w_2, w_3 为权重,Google 默认权重相等。

3.3 火焰图的形式化定义

火焰图是一个有向无环图(DAG),节点为函数调用,边为调用关系:

G=⟨V,E,width,depth⟩G = \langle V, E, \text{width}, \text{depth} \rangle
  • VV:函数调用节点集合
  • E⊆V×VE \subseteq V \times V:调用边
  • width:V→R+\text{width}: V \rightarrow \mathbb{R}^+:节点宽度(执行时间)
  • depth:V→N\text{depth}: V \rightarrow \mathbb{N}:节点深度(调用栈深度)

节点的”自时间”(Self Time)与”总时间”(Total Time)关系:

Total(v)=Self(v)+∑v′∈children(v)Total(v′)\text{Total}(v) = \text{Self}(v) + \sum_{v' \in \text{children}(v)} \text{Total}(v')

3.4 内存泄漏的形式化定义

设 M(t)M(t) 为应用在时刻 tt 的内存占用。若存在常数 δ>0\delta > 0 使得:

∀t:M(t+1)−M(t)≥δ\forall t: M(t+1) - M(t) \geq \delta

且应用无功能性增长(无新数据加载),则称应用存在内存泄漏。

内存泄漏的根因是对象不可达但仍被引用:

∃o:reachableFromRoots(o)=true∧usedByApp(o)=false\exists o: \text{reachableFromRoots}(o) = \text{true} \land \text{usedByApp}(o) = \text{false}

其中 reachableFromRoots 是 GC 可达性判断,usedByApp 是业务语义使用判断。GC 不会回收”可达但无用”的对象。

3.5 渲染管线的形式化定义

浏览器渲染管线可形式化为五元组:

RenderPipeline=⟨JS,Style,Layout,Paint,Composite⟩\text{RenderPipeline} = \langle \text{JS}, \text{Style}, \text{Layout}, \text{Paint}, \text{Composite} \rangle

每一步的输入输出:

  • JS→Style\text{JS} \rightarrow \text{Style}:JS 修改 DOM 或 CSSOM
  • Style→Layout\text{Style} \rightarrow \text{Layout}:计算元素盒模型(位置、尺寸)
  • Layout→Paint\text{Layout} \rightarrow \text{Paint}:将布局结果绘制为像素位图
  • Paint→Composite\text{Paint} \rightarrow \text{Composite}:将多个图层合成最终画面

每一步的开销:

Tframe=TJS+TStyle+TLayout+TPaint+TCompositeT_{\text{frame}} = T_{\text{JS}} + T_{\text{Style}} + T_{\text{Layout}} + T_{\text{Paint}} + T_{\text{Composite}}

为维持 60 FPS,需 Tframe≤16.67T_{\text{frame}} \leq 16.67 ms。

3.6 V8 编译流水线的形式化定义

V8 的 JavaScript 执行流水线:

Source→ParseAST→IgnitionBytecode→SparkplugBaselineCode→TurboFanOptimizedCode\text{Source} \xrightarrow{\text{Parse}} \text{AST} \xrightarrow{\text{Ignition}} \text{Bytecode} \xrightarrow{\text{Sparkplug}} \text{BaselineCode} \xrightarrow{\text{TurboFan}} \text{OptimizedCode}

各阶段特征:

  • Parse:源代码 → AST(惰性解析:函数仅在调用时解析)
  • Ignition:AST → 字节码,解释执行,启动快但执行慢
  • Sparkplug:字节码 → 基线机器码,非优化编译,加速热点代码
  • TurboFan:基于类型反馈(Type Feedback)进行优化编译,内联、逃逸分析、去虚拟化

逆优化(Deoptimization):当类型反馈失效时,从 TurboFan 回退到 Ignition:

OptimizedCode→DeoptBytecode\text{OptimizedCode} \xrightarrow{\text{Deopt}} \text{Bytecode}

3.7 Core Web Vitals 的形式化定义

Core Web Vitals 三个核心指标:

  • LCP (Largest Contentful Paint):最大内容绘制时间
LCP=min⁡t{t:LargestElement(t) fully painted}\text{LCP} = \min_t \{ t : \text{LargestElement}(t) \text{ fully painted} \}
  • INP (Interaction to Next Paint):交互到下一帧绘制时间
INP=Percentile98(InputDelay+ProcessingDuration+PresentationDelay)\text{INP} = \text{Percentile}_{98}\left( \text{InputDelay} + \text{ProcessingDuration} + \text{PresentationDelay} \right)
  • CLS (Cumulative Layout Shift):累计布局偏移
CLS=∑iImpactFractioni×DistanceFractioni\text{CLS} = \sum_{i} \text{ImpactFraction}_i \times \text{DistanceFraction}_i

阈值(2024 年标准):

指标良好需改进差
LCP< 2.5s2.5s - 4.0s> 4.0s
INP< 200ms200ms - 500ms> 500ms
CLS< 0.10.1 - 0.25> 0.25

4. 理论推导

4.1 采样分析 vs 结构化分析的开销对比

采样分析(Sampling)每 TsT_s 时间采集一次调用栈,时间复杂度 O(N⋅fs⋅T)O(N \cdot f_s \cdot T),其中 NN 为采集次数、fsf_s 为采样频率、TT 为总时长。结构化分析(Instrumentation)在每个函数入口出口插桩,时间复杂度 O(C⋅k)O(C \cdot k),其中 CC 为函数调用次数、kk 为单次插桩开销。

采样分析的优势:低开销、统计性。劣势:精度低(< TsT_s 的函数可能漏采)。

结构化分析的优势:精确、覆盖完整。劣势:高开销(10-100 倍减速)。

Chrome DevTools Performance 面板默认使用采样分析(1000 Hz 采样率),适合生产场景。

4.2 渲染管线的强制同步布局开销

强制同步布局(Forced Synchronous Layout,又称 Layout Thrashing)发生在 JS 代码中先读取布局属性、后修改 DOM的模式:

// 反模式:读写交替触发 N 次布局
for (let i = 0; i < N; i++) {
  const width = element.offsetWidth;  // 触发布局
  element.style.width = width + 10 + 'px';  // 失效布局
}

时间复杂度:O(N⋅Tlayout)O(N \cdot T_{\text{layout}})。对于 1000 个元素、Tlayout=1T_{\text{layout}} = 1 ms 的场景,总开销 1 秒。

优化为批量读写:

// 优化:先读后写
const widths = elements.map(e => e.offsetWidth);  // 触发一次布局
elements.forEach((e, i) => e.style.width = widths[i] + 10 + 'px');  // 一次失效

优化后复杂度:O(N+Tlayout)O(N + T_{\text{layout}}),即 O(N)O(N) 加单次布局开销。

4.3 V8 隐藏类与属性访问性能

V8 为每个对象维护隐藏类(Hidden Class,又称 Map)。当对象添加新属性时,V8 创建新的隐藏类并建立转换(Transition):

HiddenClass(o)={PropertyDescriptors,TransitionTree}\text{HiddenClass}(o) = \{ \text{PropertyDescriptors}, \text{TransitionTree} \}

属性访问的性能:

Taccess={O(1)if monomorphic(单态,同一隐藏类)O(n)if polymorphic(多态,n 个隐藏类)O(slow)if megamorphic(超多态,回退到字典查找)T_{\text{access}} = \begin{cases} O(1) & \text{if monomorphic(单态,同一隐藏类)} \\ O(n) & \text{if polymorphic(多态,n 个隐藏类)} \\ O(\text{slow}) & \text{if megamorphic(超多态,回退到字典查找)} \end{cases}

推论:避免在构造函数外动态添加属性,否则隐藏类频繁切换,属性访问退化为字典查找,性能下降 5-100 倍。

4.4 垃圾回收的停顿时间分析

V8 使用分代垃圾回收:

  • Minor GC (Scavenge):年轻代,复制算法,停顿时间 1-5 ms
  • Major GC (Mark-Sweep-Compact):老年代,标记-清除-整理,停顿时间 50-200 ms

停顿时间复杂度:

TGC=α⋅∣Heap∣+β⋅∣LiveObjects∣T_{\text{GC}} = \alpha \cdot |\text{Heap}| + \beta \cdot |\text{LiveObjects}|

推论:减少堆内存可降低 GC 停顿。对于实时性要求高的应用(游戏、动画),应避免创建大量临时对象。

4.5 Long Task 阈值的理论推导

Chrome 将长任务(Long Task)定义为执行时间超过 50 ms 的任务。推导依据:

  • 60 FPS 每帧预算 16.67 ms
  • 用户体验流畅性阈值约 100 ms(Doherty Threshold)
  • 主线程在 100 ms 内需响应交互:剩余 100−50=50100 - 50 = 50 ms 用于其他任务

因此:

Ttask>50 ms⇒LongTaskT_{\text{task}} > 50 \text{ ms} \Rightarrow \text{LongTask}

推论:任何超过 50 ms 的同步任务都会导致交互响应变慢。应拆分为多个小任务,使用 requestIdleCallback 或 setTimeout(fn, 0) 让出主线程。

4.6 缓存的命中率与性能

设缓存命中率为 hh,缓存命中延迟 tct_c,未命中延迟 tmt_m,平均访问时间:

Tavg=h⋅tc+(1−h)⋅tmT_{\text{avg}} = h \cdot t_c + (1 - h) \cdot t_m

对于 HTTP 缓存:tc≈0t_c \approx 0 ms(内存缓存),tm≈100t_m \approx 100 ms(网络)。要达到 1 ms 平均延迟:

h⋅0+(1−h)⋅100=1⇒h≥0.99h \cdot 0 + (1 - h) \cdot 100 = 1 \Rightarrow h \geq 0.99

推论:99% 的资源需要命中缓存才能达到 1 ms 加载延迟。这意味着关键资源必须强缓存(Cache-Control: max-age=31536000 + 内容哈希文件名)。

4.7 requestAnimationFrame 的时序优化

requestAnimationFrame (rAF) 在每次渲染前调用,与显示器刷新率同步。复杂度:

TrAF=1RefreshRate≈16.67 ms (60Hz)T_{\text{rAF}} = \frac{1}{\text{RefreshRate}} \approx 16.67 \text{ ms (60Hz)}

与 setTimeout(fn, 16) 对比:

  • setTimeout:在宏任务队列,可能被其他任务阻塞
  • rAF:在渲染前固定调用,不会错过帧

推论:所有视觉动画应使用 rAF 而非 setTimeout,以避免帧丢失与撕裂。

5. 代码示例

5.1 Chrome DevTools 性能录制与基础分析

// 使用 Performance API 测量关键代码块耗时
// 注意:performance.now() 精度受 Spectre 缓解影响,可能被量化到 100μs
function measureAsync(label, fn) {
  return async (...args) => {
    const start = performance.now();
    const result = await fn(...args);
    const end = performance.now();
    console.log(`[${label}] 耗时: ${(end - start).toFixed(2)} ms`);
    return result;
  };
}

// 使用 Performance Observer 采集 LCP 等指标
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('LCP:', entry.startTime, 'ms', entry.element);
  }
});
// buffered: true 表示包括页面加载历史记录
observer.observe({ type: 'largest-contentful-paint', buffered: true });

// 测量函数执行次数
function heavyCompute() {
  let sum = 0;
  for (let i = 0; i < 1e6; i++) sum += Math.sqrt(i);
  return sum;
}
const measuredHeavyCompute = measureAsync('heavyCompute', heavyCompute);
measuredHeavyCompute();

5.2 长任务检测与拆分

// 检测长任务:PerformanceObserver 监听 longtask 条目
const longTaskObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.warn('长任务:', entry.duration.toFixed(2), 'ms');
    console.warn('  开始时间:', entry.startTime.toFixed(2), 'ms');
    console.warn('  来源:', entry.attribution);
  }
});
longTaskObserver.observe({ type: 'longtask', buffered: true });

// 拆分长任务:使用 scheduler.yield(ES2024 新特性,部分浏览器支持)
async function processItems(items) {
  const results = [];
  for (let i = 0; i < items.length; i++) {
    results.push(processItem(items[i]));
    // 每 5 ms 让出主线程一次
    if (i % 100 === 0 && performance.now() - start > 5) {
      // 优先使用 scheduler.yield(新 API),回退到 setTimeout
      if ('scheduler' in window && 'yield' in scheduler) {
        await scheduler.yield();
      } else {
        await new Promise((resolve) => setTimeout(resolve, 0));
      }
    }
  }
  return results;
}

5.3 内存快照对比与泄漏定位

// 模拟典型内存泄漏:未清理的事件监听器与定时器
class LeakyComponent {
  constructor() {
    this.data = new Array(1e6).fill(0); // 大数组
    // 陷阱 1:未保存监听器引用,无法移除
    document.addEventListener('scroll', this.handleScroll.bind(this));
    // 陷阱 2:未保存定时器引用,无法清除
    setInterval(() => this.refresh(), 1000);
    // 陷阱 3:闭包持有大对象
    this.refresh = () => {
      console.log(this.data.length); // 闭包持有 this.data
    };
  }

  handleScroll() {
    // 闭包持有 this.data
    console.log(this.data[0]);
  }
}

// 正确实现:可销毁的组件
class CleanComponent {
  constructor() {
    this.data = new Array(1e6).fill(0);
    // 保存引用以便后续移除
    this.handleScroll = this.handleScroll.bind(this);
    this.refresh = this.refresh.bind(this);
    document.addEventListener('scroll', this.handleScroll, { passive: true });
    this.intervalId = setInterval(this.refresh, 1000);
  }

  handleScroll() {
    console.log(this.data[0]);
  }

  refresh() {
    console.log(this.data.length);
  }

  // 暴露 destroy 方法,由调用方显式调用
  destroy() {
    document.removeEventListener('scroll', this.handleScroll);
    clearInterval(this.intervalId);
    this.data = null; // 释放大数组
  }
}

5.4 火焰图解读与函数耗时分析

// 模拟调用栈:A → B → C → D
// 火焰图布局:
//   [================ A ================]  (Total: 100ms, Self: 5ms)
//     [====== B ======]   [== C ==]        (B Total: 60ms, C Total: 35ms)
//       [== D ==]                          (D Total: 40ms, Self: 40ms)

function functionA() {
  const start = performance.now();
  functionB(); // 60ms
  functionC(); // 35ms
  // A 的 Self Time = 100 - 60 - 35 = 5ms
  return performance.now() - start;
}

function functionB() {
  functionD(); // 40ms
  // B 的 Self Time = 60 - 40 = 20ms
  for (let i = 0; i < 1e5; i++) Math.sqrt(i); // 20ms
}

function functionC() {
  // C 的 Self Time = 35ms(无子调用)
  for (let i = 0; i < 1e6; i++) Math.sqrt(i);
}

function functionD() {
  // D 的 Self Time = 40ms(叶子节点)
  for (let i = 0; i < 2e6; i++) Math.sqrt(i);
}

// 优化思路:找 Self Time 最大的节点(D),优先优化
function functionD_optimized() {
  // 使用 TypedArray 加速数值计算
  const arr = new Float64Array(2e6);
  for (let i = 0; i < arr.length; i++) arr[i] = Math.sqrt(i);
  return arr;
}

5.5 事件循环与微任务调度

// 演示宏任务、微任务、渲染的执行顺序
console.log('1. 脚本开始(宏任务)');

setTimeout(() => {
  console.log('5. setTimeout 回调(下一个宏任务)');
}, 0);

Promise.resolve().then(() => {
  console.log('3. Promise 微任务');
});

requestAnimationFrame(() => {
  console.log('4. requestAnimationFrame(渲染前)');
});

console.log('2. 脚本结束(当前宏任务)');

// 输出顺序:1, 2, 3, 4, 5
// 原因:微任务在当前宏任务结束后立即执行,
//       rAF 在渲染前执行,setTimeout 在下一个宏任务

// 利用微任务批处理:避免多次同步操作触发多次渲染
const batchQueue = [];
let scheduled = false;
function batchUpdate(update) {
  batchQueue.push(update);
  if (!scheduled) {
    scheduled = true;
    // queueMicrotask 是 ES2018 标准化 API
    queueMicrotask(() => {
      const updates = batchQueue.splice(0);
      scheduled = false;
      applyUpdates(updates); // 一次性应用所有更新
    });
  }
}
function applyUpdates(updates) {
  updates.forEach((u) => u());
}

5.6 防抖与节流的进阶实现

// 防抖:延迟执行,支持立即执行与取消
function debounce(func, wait, { leading = false, trailing = true } = {}) {
  let timeoutId = null;
  let lastArgs = null;
  let lastThis = null;

  function invoke() {
    if (lastArgs) {
      func.apply(lastThis, lastArgs);
      lastArgs = null;
      lastThis = null;
    }
  }

  const debounced = function (...args) {
    lastArgs = args;
    lastThis = this;
    if (leading && !timeoutId) {
      invoke(); // 立即执行首次
    }
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      timeoutId = null;
      if (trailing && lastArgs) invoke();
    }, wait);
  };

  // 提供取消方法
  debounced.cancel = () => {
    clearTimeout(timeoutId);
    timeoutId = null;
    lastArgs = null;
    lastThis = null;
  };

  // 提供立即执行方法
  debounced.flush = () => {
    if (timeoutId) {
      clearTimeout(timeoutId);
      timeoutId = null;
      invoke();
    }
  };

  return debounced;
}

// 节流:固定频率执行,支持头部/尾部执行
function throttle(func, limit, { leading = true, trailing = true } = {}) {
  let lastCall = 0;
  let timeoutId = null;
  let lastArgs = null;
  let lastThis = null;

  const throttled = function (...args) {
    const now = Date.now();
    const remaining = limit - (now - lastCall);

    lastArgs = args;
    lastThis = this;

    if (remaining <= 0 || remaining > limit) {
      if (timeoutId) {
        clearTimeout(timeoutId);
        timeoutId = null;
      }
      lastCall = now;
      if (leading || lastCall !== 0) func.apply(lastThis, lastArgs);
      lastArgs = null;
      lastThis = null;
    } else if (!timeoutId && trailing) {
      timeoutId = setTimeout(() => {
        lastCall = leading ? Date.now() : 0;
        timeoutId = null;
        if (lastArgs) {
          func.apply(lastThis, lastArgs);
          lastArgs = null;
          lastThis = null;
        }
      }, remaining);
    }
  };

  throttled.cancel = () => {
    clearTimeout(timeoutId);
    timeoutId = null;
    lastCall = 0;
    lastArgs = null;
    lastThis = null;
  };

  return throttled;
}

// 使用示例:搜索框防抖
const searchInput = document.getElementById('search');
const debouncedSearch = debounce((query) => {
  fetch(`/api/search?q=${encodeURIComponent(query)}`)
    .then((r) => r.json())
    .then((data) => renderResults(data));
}, 300, { leading: false, trailing: true });
searchInput.addEventListener('input', (e) => debouncedSearch(e.target.value));

// 使用示例:滚动节流
const throttledScroll = throttle(() => {
  const scrollTop = window.scrollY;
  updateProgressBar(scrollTop / (document.body.scrollHeight - window.innerHeight));
}, 16); // 约 60 FPS
window.addEventListener('scroll', throttledScroll, { passive: true });

5.7 requestAnimationFrame 动画实现

// 使用 rAF 实现平滑动画,避免 setInterval 的丢帧问题
function animate(element, from, to, duration, easing = (t) => t) {
  return new Promise((resolve) => {
    const start = performance.now();

    function frame(now) {
      const elapsed = now - start;
      const progress = Math.min(elapsed / duration, 1);
      const eased = easing(progress);
      const current = from + (to - from) * eased;

      element.style.transform = `translateX(${current}px)`;

      if (progress < 1) {
        requestAnimationFrame(frame);
      } else {
        resolve();
      }
    }

    requestAnimationFrame(frame);
  });
}

// 缓动函数库
const Easing = {
  linear: (t) => t,
  easeInQuad: (t) => t * t,
  easeOutQuad: (t) => t * (2 - t),
  easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
  easeInCubic: (t) => t * t * t,
  easeOutCubic: (t) => --t * t * t + 1,
  easeInOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1),
  // 弹性效果
  easeOutElastic: (t) => {
    const c4 = (2 * Math.PI) / 3;
    return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
  },
};

// 使用:500ms 内将元素从 0 平移到 500px
const box = document.querySelector('.box');
animate(box, 0, 500, 500, Easing.easeOutCubic).then(() => {
  console.log('动画结束');
});

// 取消动画:保存 rAF ID
function makeCancelableAnimate(element, from, to, duration, easing) {
  let rafId = null;
  const promise = new Promise((resolve, reject) => {
    const start = performance.now();
    function frame(now) {
      const elapsed = now - start;
      const progress = Math.min(elapsed / duration, 1);
      element.style.transform = `translateX(${from + (to - from) * easing(progress)}px)`;
      if (progress < 1) {
        rafId = requestAnimationFrame(frame);
      } else {
        resolve();
      }
    }
    rafId = requestAnimationFrame(frame);
  });
  return {
    promise,
    cancel: () => {
      if (rafId) cancelAnimationFrame(rafId);
    },
  };
}

5.8 Web Worker 卸载主线程

// 主线程:将耗时计算移至 Worker
const workerCode = `
self.onmessage = function(e) {
  const { data, type } = e.data;
  if (type === 'compute') {
    const result = heavyCompute(data);
    self.postMessage({ type: 'done', result });
  }
};

function heavyCompute(data) {
  // 模拟耗时计算
  let sum = 0;
  for (let i = 0; i < data.length; i++) {
    sum += Math.sqrt(data[i]);
  }
  return sum;
}
`;

// 内联 Worker:从 Blob URL 创建,无需单独 worker.js 文件
function createInlineWorker(code) {
  const blob = new Blob([code], { type: 'application/javascript' });
  const url = URL.createObjectURL(blob);
  const worker = new Worker(url);
  // 注意:URL.revokeObjectURL 应在 Worker 加载后调用
  worker.addEventListener('message', () => URL.revokeObjectURL(url), { once: true });
  return worker;
}

const worker = createInlineWorker(workerCode);

// Promise 封装:让 Worker 调用像 async 函数
function callWorker(worker, data, transferList = []) {
  return new Promise((resolve, reject) => {
    const handler = (e) => {
      if (e.data.type === 'done') {
        worker.removeEventListener('message', handler);
        resolve(e.data.result);
      } else if (e.data.type === 'error') {
        worker.removeEventListener('message', handler);
        reject(new Error(e.data.message));
      }
    };
    worker.addEventListener('message', handler);
    worker.postMessage(data, transferList);
  });
}

// 使用:处理 1000 万个数据点
const bigData = new Float64Array(1e7).map(() => Math.random());
// 使用 transferList 转移所有权,避免拷贝
callWorker(worker, { data: bigData, type: 'compute' }, [bigData.buffer])
  .then((result) => {
    console.log('Worker 计算结果:', result);
  })
  .catch((err) => console.error('Worker 出错:', err));

// 注意:转移后 bigData 在主线程不可用
// console.log(bigData[0]); // undefined

5.9 性能监控埋点上报

// 生产级 RUM 上报:采集 Core Web Vitals 与自定义指标
class PerformanceMonitor {
  constructor(endpoint, sampleRate = 1.0) {
    this.endpoint = endpoint;
    this.sampleRate = sampleRate;
    this.queue = [];
    this.flushTimer = null;
    this.flushInterval = 5000; // 5 秒批量上报
    this.maxQueueSize = 50;
  }

  // 采集 Core Web Vitals
  collectCoreWebVitals() {
    // LCP
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      this.report('LCP', lastEntry.startTime, { element: lastEntry.element?.tagName });
    }).observe({ type: 'largest-contentful-paint', buffered: true });

    // INP
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        const inpValue = entry.duration;
        this.report('INP', inpValue, { interactionType: entry.interactionId });
      }
    }).observe({ type: 'interaction', buffered: true });

    // CLS
    let clsValue = 0;
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsValue += entry.value;
        }
      }
      this.report('CLS', clsValue);
    }).observe({ type: 'layout-shift', buffered: true });

    // 长任务
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        this.report('LongTask', entry.duration);
      }
    }).observe({ type: 'longtask', buffered: true });
  }

  // 采集自定义指标
  mark(name) {
    performance.mark(name);
  }

  measure(name, startMark, endMark) {
    try {
      performance.measure(name, startMark, endMark);
      const entry = performance.getEntriesByName(name, 'measure')[0];
      this.report(name, entry.duration);
    } catch (e) {
      console.warn('measure 失败:', e);
    }
  }

  // 上报指标(采样 + 批量)
  report(name, value, extra = {}) {
    // 采样:低于采样率的指标不上报
    if (Math.random() > this.sampleRate) return;

    this.queue.push({
      name,
      value,
      url: location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now(),
      ...extra,
    });

    // 队列满立即上报
    if (this.queue.length >= this.maxQueueSize) {
      this.flush();
    } else if (!this.flushTimer) {
      this.flushTimer = setTimeout(() => this.flush(), this.flushInterval);
    }
  }

  // 批量上报:使用 sendBeacon 避免页面卸载时丢失
  async flush() {
    if (this.queue.length === 0) return;

    const batch = this.queue.splice(0);
    this.flushTimer = null;

    // 优先使用 sendBeacon(页面卸载时仍能发送)
    if (navigator.sendBeacon) {
      const blob = new Blob([JSON.stringify({ events: batch })], { type: 'application/json' });
      const success = navigator.sendBeacon(this.endpoint, blob);
      if (success) return;
    }

    // 回退到 fetch
    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ events: batch }),
        keepalive: true, // 允许在页面卸载后继续发送
      });
    } catch (e) {
      console.warn('性能指标上报失败:', e);
      // 失败时放回队列
      this.queue.unshift(...batch);
    }
  }
}

// 使用:1% 采样率
const monitor = new PerformanceMonitor('/api/performance', 0.01);
monitor.collectCoreWebVitals();

// 在页面卸载前确保上报
window.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    monitor.flush();
  }
});
window.addEventListener('pagehide', () => monitor.flush());

5.10 IndexedDB 性能优化

// IndexedDB Promise 封装:避免回调地狱
class IndexedDBWrapper {
  constructor(dbName, version, upgradeCallback) {
    this.dbName = dbName;
    this.version = version;
    this.upgradeCallback = upgradeCallback;
    this.dbPromise = this.open();
  }

  open() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);
      request.onupgradeneeded = (e) => {
        const db = e.target.result;
        this.upgradeCallback(db);
      };
      request.onsuccess = (e) => resolve(e.target.result);
      request.onerror = (e) => reject(e.target.error);
    });
  }

  async transaction(storeName, mode, callback) {
    const db = await this.dbPromise;
    return new Promise((resolve, reject) => {
      const tx = db.transaction(storeName, mode);
      const store = tx.objectStore(storeName);
      let result;

      tx.oncomplete = () => resolve(result);
      tx.onerror = () => reject(tx.error);
      tx.onabort = () => reject(tx.error);

      // 执行回调,传入 store
      const proxy = {
        get: (key) => {
          return new Promise((res, rej) => {
            const req = store.get(key);
            req.onsuccess = () => res(req.result);
            req.onerror = () => rej(req.error);
          });
        },
        put: (value, key) => {
          return new Promise((res, rej) => {
            const req = store.put(value, key);
            req.onsuccess = () => res(req.result);
            req.onerror = () => rej(req.error);
          });
        },
        delete: (key) => {
          return new Promise((res, rej) => {
            const req = store.delete(key);
            req.onsuccess = () => res();
            req.onerror = () => rej(req.error);
          });
        },
        cursor: (range, direction) => {
          return new Promise((res, rej) => {
            const req = store.openCursor(range, direction);
            const results = [];
            req.onsuccess = (e) => {
              const cursor = e.target.result;
              if (cursor) {
                results.push(cursor.value);
                cursor.continue();
              } else {
                res(results);
              }
            };
            req.onerror = () => rej(req.error);
          });
        },
      };

      Promise.resolve(callback(proxy)).then((r) => { result = r; });
    });
  }
}

// 使用示例:缓存 API 响应
const cache = new IndexedDBWrapper('api-cache', 1, (db) => {
  db.createObjectStore('responses', { keyPath: 'url' });
  db.createObjectStore('metadata', { keyPath: 'key' });
});

// 带缓存的 fetch:先读 IndexedDB,失败再请求网络
async function cachedFetch(url, options = {}) {
  const cached = await cache.transaction('responses', 'readonly', async (store) => {
    return store.get(url);
  });

  if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
    return cached.response; // 5 分钟内使用缓存
  }

  const response = await fetch(url, options);
  const data = await response.json();

  await cache.transaction('responses', 'readwrite', async (store) => {
    await store.put({ url, response: data, timestamp: Date.now() });
  });

  return data;
}

5.11 内存泄漏检测:堆快照对比

// 辅助函数:在指定时间点拍摄堆快照(仅 DevTools 打开时可用)
// 实际使用时需在 DevTools Memory 面板手动操作,这里模拟分析逻辑

class MemoryLeakDetector {
  constructor() {
    this.snapshots = [];
  }

  // 在关键节点记录内存使用情况
  takeSnapshot(label) {
    if (performance.memory) {
      this.snapshots.push({
        label,
        usedJSHeapSize: performance.memory.usedJSHeapSize,
        totalJSHeapSize: performance.memory.totalJSHeapSize,
        jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
        timestamp: Date.now(),
      });
    } else {
      console.warn('performance.memory 不可用(仅 Chrome 支持)');
    }
  }

  // 分析内存增长趋势
  analyze() {
    if (this.snapshots.length < 2) {
      console.log('至少需要 2 个快照');
      return;
    }

    const first = this.snapshots[0];
    const last = this.snapshots[this.snapshots.length - 1];
    const delta = last.usedJSHeapSize - first.usedJSHeapSize;
    const duration = last.timestamp - first.timestamp;
    const rate = delta / (duration / 1000); // bytes per second

    console.log(`内存变化分析(${first.label} → ${last.label}):`);
    console.log(`  初始: ${(first.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`);
    console.log(`  结束: ${(last.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`);
    console.log(`  增长: ${(delta / 1024).toFixed(2)} KB`);
    console.log(`  速率: ${(rate / 1024).toFixed(2)} KB/s`);

    // 若持续增长且超过 100 KB/min,可能存在泄漏
    if (rate > 100 * 1024 / 60) {
      console.warn('警告:内存持续增长,可能存在泄漏');
    }
  }

  // 强制触发 GC(仅测试用,生产环境不应调用)
  // 需在 Chrome 启动参数加 --js-flags="--expose-gc"
  forceGC() {
    if (typeof gc === 'function') {
      gc();
    } else {
      console.warn('gc 函数不可用,启动 Chrome 时加 --expose-gc');
    }
  }
}

// 使用:检测组件创建销毁是否泄漏
const detector = new MemoryLeakDetector();
detector.takeSnapshot('before');

// 创建并销毁 1000 个组件
for (let i = 0; i < 1000; i++) {
  const comp = new CleanComponent();
  comp.destroy();
}

detector.takeSnapshot('after-1000');
detector.analyze();

5.12 自定义性能标记

// 使用 User Timing API 进行细粒度性能标记
// performance.mark + performance.measure 会在 DevTools Performance 面板中显示
class UserTiming {
  static mark(name) {
    performance.mark(name);
  }

  static measure(name, startMark, endMark) {
    try {
      performance.measure(name, startMark, endMark);
      const entry = performance.getEntriesByName(name, 'measure').pop();
      console.log(`[${name}] ${entry.duration.toFixed(2)} ms`);
      return entry.duration;
    } catch (e) {
      console.warn('measure 失败:', e);
      return null;
    }
  }

  // 装饰器风格:自动测量函数耗时
  static wrap(label, fn) {
    return function (...args) {
      const startMark = `${label}:start`;
      const endMark = `${label}:end`;
      performance.mark(startMark);

      try {
        const result = fn.apply(this, args);
        performance.mark(endMark);
        UserTiming.measure(label, startMark, endMark);
        return result;
      } catch (e) {
        performance.mark(endMark);
        UserTiming.measure(`${label}:error`, startMark, endMark);
        throw e;
      }
    };
  }

  // 异步函数包装
  static wrapAsync(label, fn) {
    return async function (...args) {
      const startMark = `${label}:start`;
      const endMark = `${label}:end`;
      performance.mark(startMark);

      try {
        const result = await fn.apply(this, args);
        performance.mark(endMark);
        UserTiming.measure(label, startMark, endMark);
        return result;
      } catch (e) {
        performance.mark(endMark);
        UserTiming.measure(`${label}:error`, startMark, endMark);
        throw e;
      }
    };
  }

  // 清除所有标记与测量
  static clear() {
    performance.clearMarks();
    performance.clearMeasures();
  }
}

// 使用
const expensiveFunction = UserTiming.wrap('expensive-op', function () {
  let sum = 0;
  for (let i = 0; i < 1e6; i++) sum += Math.sqrt(i);
  return sum;
});

const asyncOperation = UserTiming.wrapAsync('async-fetch', async (url) => {
  const r = await fetch(url);
  return r.json();
});

5.13 CSS 选择器性能与样式优化

// 演示读写交替导致的强制同步布局
function badLayoutThashing(elements) {
  // 反模式:每次读 offsetWidth 触发布局,每次写 style.width 失效布局
  elements.forEach((el) => {
    const newWidth = el.offsetWidth + 10;
    el.style.width = `${newWidth}px`; // 触发重排
  });
}

function goodBatchedLayout(elements) {
  // 优化:先读所有值,再统一写入
  const widths = elements.map((el) => el.offsetWidth); // 触发一次布局
  // fastdom 库的核心思想
  widths.forEach((w, i) => {
    elements[i].style.width = `${w + 10}px`; // 一次失效
  });
}

// 使用 FastDOM 模式分离读写
const fastdom = {
  reads: [],
  writes: [],
  scheduled: false,

  measure(fn) {
    this.reads.push(fn);
    this.schedule();
    return fn;
  },

  mutate(fn) {
    this.writes.push(fn);
    this.schedule();
    return fn;
  },

  schedule() {
    if (!this.scheduled) {
      this.scheduled = true;
      // 在下一帧统一执行:先读后写
      requestAnimationFrame(() => this.flush());
    }
  },

  flush() {
    const reads = this.reads.splice(0);
    const writes = this.writes.splice(0);
    this.scheduled = false;
    // 先执行所有读操作
    reads.forEach((fn) => fn());
    // 再执行所有写操作
    writes.forEach((fn) => fn());
  },
};

// 使用 FastDOM:避免强制同步布局
function updateElements() {
  const elements = document.querySelectorAll('.item');
  elements.forEach((el) => {
    // 读:measure 阶段
    fastdom.measure(() => {
      const width = el.offsetWidth;
      const height = el.offsetHeight;
      // 写:mutate 阶段
      fastdom.mutate(() => {
        el.style.width = `${width + 10}px`;
        el.style.height = `${height + 5}px`;
      });
    });
  });
}

6. 对比分析

6.1 调试工具对比

工具适用场景优势劣势
Chrome DevToolsWeb 前端调试功能全面、官方维护仅支持 Chromium 内核
Firefox Developer ToolsWeb 前端调试CSS 调试功能强大生态较小
Safari Web InspectoriOS / macOS 调试唯一支持 iOS 真机调试功能较基础
VS Code DebuggerNode.js / 浏览器IDE 集成体验好不支持所有 DevTools 功能
Node.js InspectorNode.js 后端命令行原生支持无图形界面
Puppeteer自动化测试CDP 协议、可脚本化需编写测试代码
Playwright跨浏览器自动化支持多浏览器引擎体积较大

6.2 性能分析方式对比

分析方式实现原理精度开销适用场景
采样分析(Sampling)周期性采集调用栈统计性低(<5%)生产环境
结构化分析(Instrumentation)函数入口出口插桩精确高(10-100x)开发环境
性能标记(User Timing)手动 mark + measure精确极低关键路径监控
Performance Observer浏览器原生事件精确极低Core Web Vitals
Memory 堆快照全堆扫描完整高(暂停执行)内存泄漏定位
堆分配追踪增量堆扫描较完整中内存增长分析

6.3 防抖与节流对比

特性防抖(Debounce)节流(Throttle)
触发频率仅最后一次固定频率
适用场景搜索输入、resize滚动、mousemove
实现复杂度简单中等
立即执行选项leadingleading
尾部执行选项trailingtrailing
取消支持是是
典型延迟200-500 ms16-100 ms

6.4 渲染优化技术对比

技术适用场景实现难度性能提升
will-change已知将变化的元素简单中(避免合成层重建)
transform位移、旋转、缩放简单高(仅合成层)
opacity透明度简单高(仅合成层)
DocumentFragment批量 DOM 插入简单中(减少重排次数)
虚拟列表长列表渲染高极高(O(1) 渲染)
CSS Containment独立组件中中(隔离重排范围)
requestAnimationFrame动画简单高(避免丢帧)
Web Workers耗时计算中高(释放主线程)

6.5 V8 编译层次对比

编译层角色优化程度启动开销执行速度
源码解析(Parser)AST 生成无惰性解析快不执行
Ignition(解释器)字节码执行无极快慢(10-100x)
Sparkplug(基线)单遍编译低快中(2-10x 提升)
TurboFan(优化)基于类型反馈高慢(10-100ms)极快(接近原生)

7. 常见陷阱与反模式

7.1 陷阱:使用 console.log 调试生产代码

问题:console.log 会序列化对象,在大循环中导致严重性能问题。

// 反模式:在循环中打印大对象
for (let i = 0; i < 1e6; i++) {
  console.log('iteration:', i, bigObject); // 每次打印耗时 1-10ms
}
// 总耗时:1000 秒

正确做法:

// 生产代码移除 console.log,或使用 debug 库按级别控制
const debug = createDebug('app:loop');
for (let i = 0; i < 1e6; i++) {
  debug('iteration:', i); // 默认不输出,仅在 DEBUG=app:loop 时输出
}

// 必须保留时使用条件输出
if (process.env.NODE_ENV !== 'production') {
  console.log('debug info:', data);
}

7.2 陷阱:在循环中读取布局属性

问题:读写布局属性交替触发 N 次重排。

// 反模式
for (let i = 0; i < elements.length; i++) {
  const w = elements[i].offsetWidth;
  elements[i].style.width = w + 10 + 'px';
}

正确做法:见 6.13 节的 FastDOM 模式。

7.3 陷阱:未移除事件监听器

问题:动态创建的组件添加监听器但未移除,导致内存泄漏。

// 反模式
function createButton() {
  const btn = document.createElement('button');
  btn.addEventListener('click', () => {
    // 闭包持有 btn,btn 持有监听器,监听器持有闭包 → 循环引用
    console.log('clicked', btn.textContent);
  });
  return btn;
}
// 即使按钮从 DOM 移除,监听器仍持有闭包,闭包持有按钮

正确做法:使用 WeakRef 或显式 removeEventListener,或使用 AbortSignal 统一管理:

function createButton() {
  const btn = document.createElement('button');
  const controller = new AbortController();
  btn.addEventListener('click', () => {
    console.log('clicked', btn.textContent);
  }, { signal: controller.signal });

  // 销毁时:controller.abort() 自动移除所有监听器
  return { btn, destroy: () => controller.abort() };
}

7.4 陷阱:定时器未清除

问题:setInterval 在组件销毁后继续执行回调,闭包持有大对象。

// 反模式
class Timer {
  constructor() {
    this.data = new Array(1e6).fill(0);
    setInterval(() => {
      console.log(this.data[0]); // 即使实例被销毁,定时器仍持有 this
    }, 1000);
  }
}

正确做法:保存定时器 ID,在销毁时清除。

7.5 陷阱:过度优化

问题:在非热点路径过度优化,损害可读性。

// 反模式:为 0.1% 性能牺牲可读性
const sum = arr.reduce((a, b) => a + b, 0);
// 被优化为(仅快 5%,但可读性极差)
let sum = 0;
for (let i = 0, len = arr.length; i < len; i++) sum += arr[i];

正确做法:遵循”20% 代码占用 80% 时间”原则,仅优化性能分析定位的热点。

7.6 陷阱:依赖 performance.now() 的绝对值

问题:不同浏览器、不同设备的时间基准不同,绝对值无意义。

// 反模式
const start = performance.now();
doWork();
const elapsed = performance.now() - start;
if (elapsed < 50) console.log('足够快'); // 在低端设备上永远不达标

正确做法:使用相对比较(A/B 测试)或分位数统计(P75、P95)。

7.7 陷阱:误用 requestAnimationFrame 进行非视觉任务

问题:rAF 仅在页面可见时触发,后台标签页中不执行。

// 反模式:用 rAF 定时上报
function report() {
  sendAnalytics();
  requestAnimationFrame(report);
}
// 标签页切到后台后,上报停止

正确做法:非视觉任务使用 setTimeout 或 setInterval,并监听 visibilitychange 决定是否暂停。

7.8 陷阱:忽视 GC 停顿

问题:频繁创建大对象导致 GC 频繁触发,引起卡顿。

// 反模式:每帧创建新数组
function render() {
  const tempData = new Array(1e5).fill(0).map((_, i) => i * Math.random());
  draw(tempData);
  requestAnimationFrame(render);
}
// 每帧分配 800KB 内存,触发频繁 Minor GC

正确做法:使用对象池或复用数组:

const tempData = new Float64Array(1e5); // 预分配
function render() {
  for (let i = 0; i < tempData.length; i++) tempData[i] = i * Math.random();
  draw(tempData);
  requestAnimationFrame(render);
}
// 无内存分配,无 GC 停顿

7.9 陷阱:错误使用 try-catch 包裹热路径

问题:V8 对包含 try-catch 的函数不做某些优化(TurboFan 已修复,但仍有开销)。

// 反模式
function hotLoop(arr) {
  let sum = 0;
  for (let i = 0; i < arr.length; i++) {
    try { // V8 可能不内联此函数
      sum += processItem(arr[i]);
    } catch (e) {
      console.error(e);
    }
  }
  return sum;
}

正确做法:将 try-catch 移至循环外或封装为独立函数:

function hotLoop(arr) {
  let sum = 0;
  for (let i = 0; i < arr.length; i++) {
    sum += safeProcess(arr[i]);
  }
  return sum;
}

function safeProcess(item) {
  try {
    return processItem(item);
  } catch (e) {
    console.error(e);
    return 0;
  }
}

7.10 陷阱:用 delete 删除对象属性

问题:delete 会改变对象隐藏类,导致属性访问退化为字典查找。

// 反模式
const obj = { a: 1, b: 2, c: 3 };
delete obj.b; // obj 的隐藏类退化为字典模式
obj.d = 4; // 慢

正确做法:将属性设为 undefined,或在设计上避免删除:

const obj = { a: 1, b: 2, c: 3 };
obj.b = undefined; // 保持隐藏类不变

8. 工程实践

8.1 生产级性能监控体系

// 综合性能监控系统:RUM + 合成监控 + 告警
class PerformanceSystem {
  constructor(config) {
    this.config = config;
    this.monitor = new PerformanceMonitor(config.endpoint, config.sampleRate);
    this.thresholds = config.thresholds || {
      LCP: 2500,
      INP: 200,
      CLS: 0.1,
      LongTask: 50,
    };
    this.alerts = [];
  }

  start() {
    this.monitor.collectCoreWebVitals();
    this.setupAlerts();
    this.setupErrorHandler();
    this.setupResourceTiming();
  }

  // 告警:超过阈值时立即上报
  setupAlerts() {
    const alertObserver = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        const metric = entry.entryType === 'largest-contentful-paint' ? 'LCP' :
                       entry.entryType === 'interaction' ? 'INP' :
                       entry.entryType === 'layout-shift' ? 'CLS' : null;
        if (!metric) continue;
        const value = entry.startTime || entry.duration || entry.value;
        if (value > this.thresholds[metric]) {
          this.alerts.push({
            metric,
            value,
            threshold: this.thresholds[metric],
            url: location.href,
            timestamp: Date.now(),
          });
        }
      }
    });
    alertObserver.observe({
      type: 'largest-contentful-paint',
      buffered: true,
    });
    alertObserver.observe({
      type: 'layout-shift',
      buffered: true,
    });
    alertObserver.observe({
      type: 'interaction',
      buffered: true,
    });
  }

  // 错误监控:捕获未处理异常
  setupErrorHandler() {
    window.addEventListener('error', (event) => {
      this.monitor.report('JS_ERROR', 1, {
        message: event.message,
        filename: event.filename,
        line: event.lineno,
        col: event.colno,
        stack: event.error?.stack,
      });
    });
    window.addEventListener('unhandledrejection', (event) => {
      this.monitor.report('PROMISE_REJECTION', 1, {
        reason: String(event.reason),
        stack: event.reason?.stack,
      });
    });
  }

  // 资源时序:监控每个资源的加载耗时
  setupResourceTiming() {
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        // 仅上报慢资源(> 500ms)
        if (entry.duration > 500) {
          this.monitor.report('SlowResource', entry.duration, {
            name: entry.name,
            type: entry.initiatorType,
            size: entry.transferSize,
            dnsTime: entry.domainLookupEnd - entry.domainLookupStart,
            tcpTime: entry.connectEnd - entry.connectStart,
            ttfb: entry.responseStart - entry.requestStart,
            downloadTime: entry.responseEnd - entry.responseStart,
          });
        }
      }
    }).observe({ type: 'resource', buffered: true });
  }
}

// 使用
const perfSystem = new PerformanceSystem({
  endpoint: '/api/performance',
  sampleRate: 0.01,
  thresholds: { LCP: 2500, INP: 200, CLS: 0.1, LongTask: 50 },
});
perfSystem.start();

8.2 性能预算实施

// 性能预算:超出预算时构建失败或告警
const performanceBudget = {
  // 资源大小预算(KB)
  resourceSizes: {
    'javascript': 300,
    'css': 50,
    'image': 500,
    'font': 100,
    'total': 1000,
  },
  // 资源数量预算
  resourceCounts: {
    'javascript': 10,
    'css': 3,
    'image': 30,
    'font': 4,
    'total': 50,
  },
  // 时序预算(ms)
  timings: {
    LCP: 2500,
    FCP: 1800,
    TTI: 3500,
    TBT: 200,
    CLS: 0.1,
  },
};

// CI 中检查预算:结合 Lighthouse CI
async function checkBudget(lighthouseReport) {
  const audits = lighthouseReport.audits;
  const failures = [];

  // 检查资源大小
  const totalBytes = audits['total-byte-weight'].numericValue;
  if (totalBytes > performanceBudget.resourceSizes.total * 1024) {
    failures.push(`资源总大小 ${Math.round(totalBytes / 1024)} KB 超出预算 ${performanceBudget.resourceSizes.total} KB`);
  }

  // 检查时序
  for (const [metric, budget] of Object.entries(performanceBudget.timings)) {
    const audit = audits[metric.toLowerCase()];
    if (audit && audit.numericValue > budget) {
      failures.push(`${metric}: ${audit.numericValue.toFixed(0)} ms 超出预算 ${budget} ms`);
    }
  }

  if (failures.length > 0) {
    console.error('性能预算未通过:');
    failures.forEach((f) => console.error('  -', f));
    process.exit(1);
  } else {
    console.log('性能预算通过');
  }
}

8.3 关键路径优化清单

// 关键渲染路径优化检查清单
const criticalPathChecklist = [
  {
    item: '内联关键 CSS',
    check: () => {
      const criticalCss = document.querySelector('style.critical');
      return !!criticalCss;
    },
    impact: 'high',
  },
  {
    item: '延迟加载非关键 JS',
    check: () => {
      const scripts = document.querySelectorAll('script[src]');
      return Array.from(scripts).every((s) => s.defer || s.async);
    },
    impact: 'high',
  },
  {
    item: '预加载关键资源',
    check: () => {
      const preloads = document.querySelectorAll('link[rel="preload"]');
      return preloads.length > 0;
    },
    impact: 'medium',
  },
  {
    item: '使用 CDN',
    check: () => {
      const scripts = document.querySelectorAll('script[src]');
      return Array.from(scripts).some((s) => s.src.includes('cdn.'));
    },
    impact: 'medium',
  },
  {
    item: '启用 HTTP/2 或 HTTP/3',
    check: async () => {
      const entries = performance.getEntriesByType('resource');
      return entries.some((e) => e.nextHopProtocol === 'h2' || e.nextHopProtocol === 'h3');
    },
    impact: 'medium',
  },
  {
    item: '图片格式优化(WebP/AVIF)',
    check: () => {
      const images = document.querySelectorAll('img[src]');
      return Array.from(images).some((img) => img.src.match(/\.(webp|avif)$/));
    },
    impact: 'high',
  },
];

async function runChecklist() {
  const results = [];
  for (const item of criticalPathChecklist) {
    const passed = await item.check();
    results.push({ ...item, passed });
  }
  return results;
}

// 在浏览器控制台运行
runChecklist().then((results) => {
  console.table(results);
});

9. 案例研究

9.1 案例研究:电商首页 LCP 优化

背景:某电商平台首页 LCP 平均 4.8 秒,移动端用户跳出率 35%。

分析过程:

  1. 采集数据:使用 RUM 采集 7 天数据,发现 LCP 主要由主图加载引起。
  2. 火焰图分析:DevTools Performance 录制显示,主图在 1.2 秒后才开始下载(被 JS 阻塞)。
  3. 根因定位:主图通过 JS 动态加载,而非直接 <img> 标签,且未设置 preload。

优化措施:

<!-- 优化前 -->
<script>
  // 在 React 渲染后才请求主图
  fetch('/api/home').then((r) => r.json()).then((data) => {
    renderHero(data.imageUrl); // 1.2s 后才开始加载图片
  });
</script>

<!-- 优化后:预加载 + 直接 img 标签 -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
<img src="/hero.webp" alt="主图" fetchpriority="high" width="1200" height="630">

效果:LCP 从 4.8 秒降至 1.9 秒,移动端跳出率下降至 22%。

9.2 案例研究:长列表虚拟化优化

背景:管理后台数据表格 10000 行,滚动卡顿,INP 800ms。

分析过程:

  1. Performance 录制:每次滚动触发 10000 个 DOM 节点的重排,耗时 200ms。
  2. Memory 分析:DOM 节点数 10000+,每个节点 1KB,总内存 10MB。

优化措施:使用虚拟列表仅渲染可见区域:

// 简化版虚拟列表实现
class VirtualList {
  constructor(container, items, itemHeight = 40) {
    this.container = container;
    this.items = items;
    this.itemHeight = itemHeight;
    this.visibleCount = Math.ceil(container.clientHeight / itemHeight) + 2;
    this.buffer = Math.floor(this.visibleCount / 2);

    this.content = document.createElement('div');
    this.content.style.position = 'relative';
    this.content.style.height = `${items.length * itemHeight}px`;
    container.appendChild(this.content);

    container.addEventListener('scroll', this.onScroll.bind(this), { passive: true });
    this.render(0);
  }

  onScroll() {
    const scrollTop = this.container.scrollTop;
    const startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer);
    this.render(startIndex);
  }

  render(startIndex) {
    const endIndex = Math.min(startIndex + this.visibleCount, this.items.length);
    this.content.innerHTML = '';

    for (let i = startIndex; i < endIndex; i++) {
      const item = document.createElement('div');
      item.style.position = 'absolute';
      item.style.top = `${i * this.itemHeight}px`;
      item.style.height = `${this.itemHeight}px`;
      item.textContent = this.items[i].name;
      this.content.appendChild(item);
    }
  }
}

// 使用
const list = new VirtualList(document.getElementById('list'), bigData);

效果:DOM 节点从 10000 减少到 20,INP 从 800ms 降至 50ms。

9.3 案例研究:单页应用内存泄漏排查

背景:用户反馈长时间使用后页面越来越慢,刷新后恢复。

排查过程:

  1. Memory 三快照法:
    • 快照 1:登录后空闲状态
    • 快照 2:执行 100 次路由切换
    • 快照 3:再执行 100 次路由切换
  2. 对比快照 1 和 3:发现 Detached DOM 节点增长 5000+。
  3. 保留路径分析:被 eventListeners Map 引用,而 Map 未在路由切换时清理。

修复代码:

// 问题代码:全局事件监听器 Map 未清理
const eventListeners = new Map();

function bindComponent(component) {
  const listener = () => component.update();
  window.addEventListener('resize', listener);
  eventListeners.set(component.id, listener); // 即使组件销毁,仍被引用
}

// 修复:在组件销毁时显式清理
function destroyComponent(component) {
  const listener = eventListeners.get(component.id);
  if (listener) {
    window.removeEventListener('resize', listener);
    eventListeners.delete(component.id);
  }
  component.destroy();
}

效果:Detached DOM 节点归零,长时间使用不再变慢。

10.1 基础题

题目 1:请列出 Chrome DevTools 的八大面板,并说明各面板的主要用途。

参考答案要点:Elements(DOM 编辑)、Console(日志输出)、Sources(源码调试)、Network(网络请求)、Performance(性能分析)、Memory(内存分析)、Application(存储、Cookie、Service Worker)、Security(证书、混合内容)、Lighthouse(综合审计)。

题目 2:什么是 Core Web Vitals?请列出三个核心指标及其良好阈值。

参考答案要点:Core Web Vitals 是 Google 推出的 Web 性能关键指标体系,包括 LCP(< 2.5s)、INP(< 200ms)、CLS(< 0.1)。

题目 3:解释火焰图中”Self Time”与”Total Time”的区别。

参考答案要点:Self Time 是函数自身执行时间(不含子调用),Total Time 是函数及其所有子调用的总时间。优化应优先看 Self Time 大的函数。

10.2 进阶题

题目 4:以下代码存在什么性能问题?如何优化?

const items = document.querySelectorAll('.item');
items.forEach((item) => {
  const height = item.offsetHeight;
  item.style.height = height + 10 + 'px';
  const width = item.offsetWidth;
  item.style.width = width + 5 + 'px';
});

参考答案要点:存在强制同步布局(Layout Thrashing)。读写交替触发 N 次重排。优化:使用 FastDOM 模式分离读和写,或先读所有 height 和 width,再统一设置 style。

题目 5:解释 V8 的分层编译流水线,并说明为什么 TypeScript 编译后的代码在某些场景下比手写 JS 慢。

参考答案要点:V8 流水线:Parse → Ignition(字节码)→ Sparkplug(基线编译)→ TurboFan(优化编译)。TS 编译产物可能包含更多抽象(如 class 字段、enum),导致隐藏类频繁切换,TurboFan 逆优化。手写 JS 可能更符合 V8 优化假设。

题目 6:如何检测一个 Web 应用是否存在内存泄漏?请描述完整流程。

参考答案要点:使用 Memory 面板的”三快照法”——基线快照、操作后快照、再操作后快照,对比快照 1 和 3 的增量,重点关注 Detached DOM 节点与未清理的引用。

10.3 挑战题

题目 7:设计一个生产级 RUM(Real User Monitoring)系统,要求:

  1. 采集 LCP、INP、CLS、长任务、JS 错误
  2. 支持 1% 采样率
  3. 页面卸载时确保数据不丢失
  4. 支持自定义性能标记

参考答案要点:参考 9.1 节的 PerformanceSystem 实现。关键点:使用 PerformanceObserver 采集指标;navigator.sendBeacon 或 fetch keepalive 确保卸载时上报;Performance.mark/measure 支持自定义标记;按 URL 维度聚合便于分析。

题目 8:某 SPA 应用首页加载 LCP 5 秒,请设计完整的优化方案,包含分析步骤与至少 5 项优化措施。

参考答案要点:

  1. 分析:用 Lighthouse + Performance 录制,找出 LCP 元素(通常是主图或大文字块)
  2. 优化措施:
    • 预加载关键资源(<link rel="preload">)
    • 内联关键 CSS,异步加载非关键 CSS
    • 延迟加载非首屏 JS(defer/async)
    • 使用 WebP/AVIF 图片格式 + 响应式 srcset
    • 使用 CDN + HTTP/2
    • 服务端渲染首屏(SSR)
    • 使用 fetchpriority="high" 提升主图优先级

题目 9:分析以下代码的内存模型,指出泄漏点并给出修复方案:

class Dashboard {
  constructor() {
    this.widgets = [];
    this.refreshInterval = setInterval(() => this.refresh(), 5000);
    document.addEventListener('visibilitychange', this.onVisible.bind(this));
  }

  addWidget(widget) {
    this.widgets.push(widget);
    widget.onUpdate = () => this.render(); // 闭包持有 this
  }

  onVisible() {
    if (document.visibilityState === 'visible') this.refresh();
  }

  refresh() {
    this.widgets.forEach((w) => w.fetch());
  }
}

参考答案要点:

  • 泄漏点 1:setInterval 未在销毁时清除
  • 泄漏点 2:visibilitychange 监听器未移除
  • 泄漏点 3:widget.onUpdate 闭包持有 dashboard,widget 未销毁时 dashboard 无法回收
  • 修复:添加 destroy() 方法显式清理定时器、监听器,并将 widget.onUpdate 设为 null

12.1 官方文档

12.2 经典教材

  • Steve Souders. 2007. High Performance Web Sites. O’Reilly Media, Sebastopol, CA, USA.
  • Ilya Grigorik. 2013. High Performance Browser Networking. O’Reilly Media, Sebastopol, CA, USA.
  • Tom Dale Wilson. 2021. Web Performance in Action. Manning Publications, Shelter Island, NY, USA.

12.3 前沿论文与博客

12.4 开源工具

13. 附录

13.1 Chrome DevTools 快捷键速查

快捷键功能
F12 / Ctrl+Shift+I打开 DevTools
Ctrl+Shift+J打开 Console
Ctrl+Shift+C检查元素
Ctrl+P在 Sources 中打开文件
Ctrl+Shift+P运行命令
F8继续执行
F10单步跳过
F11单步进入
Shift+F11单步跳出
Ctrl+B切换断点
Ctrl+Shift+B切换条件断点

13.2 Performance API 方法速查

方法用途
performance.now()高精度时间戳(亚毫秒)
performance.mark(name)创建标记
performance.measure(name, start, end)测量区间
performance.getEntries()获取所有条目
performance.getEntriesByName(name, type)按名称获取
performance.getEntriesByType(type)按类型获取
performance.clearMarks(name?)清除标记
performance.clearMeasures(name?)清除测量
performance.memory内存使用(仅 Chrome)
performance.timing导航时序(已废弃,用 PerformanceNavigationTiming)

13.3 Core Web Vitals 阈值速查(2024 标准)

指标良好需改进差测量方式
LCP< 2.5s2.5-4.0s> 4.0sPerformanceObserver
INP< 200ms200-500ms> 500msPerformanceObserver
CLS< 0.10.1-0.25> 0.25PerformanceObserver
FCP< 1.8s1.8-3.0s> 3.0sPerformanceObserver
TTFB< 800ms800-1800ms> 1800msNavigation Timing
TBT< 200ms200-600ms> 600msLighthouse 合成

13.4 内存泄漏常见原因速查

原因检测方法修复方式
未清除定时器Memory 堆快照对比在 destroy 中 clearInterval
未移除事件监听器Detached DOM 节点分析removeEventListener 或 AbortSignal
闭包持有大对象Retaining Path 分析重构作用域,使用 WeakRef
全局变量累积三快照法显式置 null,使用模块作用域
缓存无上限LRU 实现限制大小,使用 WeakMap
Detached DOM 节点Memory 面板筛选移除 DOM 前移除监听器
Promise 未 resolveDevTools Console 警告检查 Promise 链,添加 catch

13.5 性能优化检查清单

加载性能:

  • 关键 CSS 内联
  • 非关键 JS 延迟加载(defer / async)
  • 图片格式优化(WebP / AVIF)
  • 响应式图片(srcset)
  • 资源预加载(preload / prefetch)
  • 启用 CDN
  • HTTP/2 或 HTTP/3
  • Brotli / Gzip 压缩

渲染性能:

  • 避免强制同步布局(读写分离)
  • 使用 transform / opacity 做动画
  • will-change 合理使用
  • 长列表虚拟化
  • requestAnimationFrame 替代 setTimeout
  • CSS Containment 隔离重排

JavaScript 性能:

  • 避免在热路径创建对象(对象池)
  • 使用 TypedArray 加速数值计算
  • 避免在循环中 try-catch
  • Web Worker 卸载耗时计算
  • 防抖与节流高频事件
  • 避免隐藏类频繁切换

内存管理:

  • 组件销毁时清理监听器与定时器
  • 使用 WeakMap / WeakSet 存储可选引用
  • 大数组使用 TypedArray
  • 避免闭包持有无用变量
  • 定期 Memory 面板检查

13.6 更新日志

  • 2026-04-05:初始创建,涵盖调试工具、常见错误、性能分析与优化。
  • 2026-05-03:扩展内容,添加更详细的调试工具使用方法、更多常见错误类型和解决方案、更深入的性能分析方法、更全面的优化策略、实际应用案例和常见问题解决方案。
  • 2026-07-21:升级至金标准。新增引言、学习目标(Bloom 分类法)、历史动机与背景(含时间线)、形式化定义(调试、性能优化、火焰图、内存泄漏、渲染管线、V8 编译流水线、Core Web Vitals)、理论推导(采样分析、强制同步布局、隐藏类、GC 停顿、Long Task 阈值、缓存命中率、rAF 时序)、13 个完整代码示例(性能监控埋点、长任务拆分、内存快照对比、火焰图解读、事件循环、防抖节流进阶、rAF 动画、Web Worker、RUM 上报、IndexedDB 优化、内存泄漏检测、User Timing、FastDOM)、5 个对比分析表、10 个陷阱反模式、3 个工程实践、3 个案例研究、9 道习题、10 篇 ACM 格式参考文献含 DOI、6 个附录。

console API

基本写法:分类输出 console.<方法>(<内容>)

// 不同级别日志
console.log("info"); console.warn("warn"); console.error("err");

基本写法:分组输出 console.group(<标题>) | console.groupEnd()

// 折叠分组日志
console.group("user");
console.log("name", name);
console.groupEnd();

基本写法:表格输出 console.table(<数据>)

// 以表格形式展示对象数组
console.table([{ id: 1, name: "Tom" }, { id: 2, name: "Jerry" }]);

基本写法:计时 console.time(<标签>) | console.timeEnd(<标签>)

// 测量代码执行时间
console.time("loop");
for (let i = 0; i < 1000; i++) {}
console.timeEnd("loop");

基本写法:计数 console.count(<标签>)

// 统计调用次数
function fn() { console.count("fn"); }

基本写法:断言 console.assert(<条件>, <消息>)

// 条件为假才输出错误
console.assert(value > 0, "value must be positive");

基本写法:堆栈追踪 console.trace(<消息>)

// 输出调用堆栈
function inner() { console.trace("here"); }

debugger 断点

基本写法:debugger 语句 debugger

// 代码中插入断点打开开发者工具时暂停
function fn() { debugger; }

performance API

基本写法:性能时间戳 performance.now()

// 高精度时间戳精确到微秒
let start = performance.now();
work();
console.log(performance.now() - start);

基本写法:标记测量 performance.mark(<名称>)

// 标记时间点
performance.mark("start");
work();
performance.mark("end");
performance.measure("duration", "start", "end");

基本写法:获取测量结果 performance.getEntriesByName(<名称>)

// 读取测量数据
let measures = performance.getEntriesByName("duration");
console.log(measures[0].duration);

基本写法:清理条目 performance.clearMarks() | performance.clearMeasures()

// 清除标记和测量
performance.clearMarks();
performance.clearMeasures();

性能指标

基本写法:页面加载性能 performance.timing

// 读取页面加载各阶段时间
let t = performance.timing;
let loadTime = t.loadEventEnd - t.navigationStart;

基本写法:navigationEntry performance.getEntriesByType("navigation")

// 现代方式获取导航性能
let [nav] = performance.getEntriesByType("navigation");
console.log(nav.domContentLoadedEventEnd);

基本写法:资源加载 performance.getEntriesByType("resource")

// 获取所有资源加载耗时
let resources = performance.getEntriesByType("resource");
resources.forEach(r => console.log(r.name, r.duration));

基本写法:Observer 监听 new PerformanceObserver(<回调>)

// 监听性能条目产生
let observer = new PerformanceObserver(list => {
    list.getEntries().forEach(entry => console.log(entry));
});
observer.observe({ entryTypes: ["measure", "resource"] });

Web Vitals

基本写法:核心指标 new PerformanceObserver(<回调>)

// 监听 LCP 最大内容绘制
new PerformanceObserver(list => {
    list.getEntries().forEach(e => console.log("LCP", e.startTime));
}).observe({ type: "largest-contentful-paint", buffered: true });

基本写法:FID 首次输入延迟 new PerformanceObserver(<回调>)

// 监听首次交互延迟
new PerformanceObserver(list => {
    list.getEntries().forEach(e => console.log("FID", e.processingStart - e.startTime));
}).observe({ type: "first-input", buffered: true });

基本写法:CLS 布局偏移 new PerformanceObserver(<回调>)

// 监听布局偏移累计
new PerformanceObserver(list => {
    list.getEntries().forEach(e => console.log("CLS", e.value));
}).observe({ type: "layout-shift", buffered: true });

内存监测

基本写法:堆快照 performance.memory

// Chrome 提供内存使用估算
let mem = performance.memory;
console.log(mem.usedJSHeapSize, mem.totalJSHeapSize);

基本写法:垃圾回收触发 window.gc && gc()

// 需要启动参数开启强制 GC 调试用
if (window.gc) window.gc();

长任务监测

基本写法:长任务观察 new PerformanceObserver(<回调>)

// 监听超过 50ms 的任务
new PerformanceObserver(list => {
    list.getEntries().forEach(e => console.log("long task", e.duration));
}).observe({ entryTypes: ["longtask"] });

requestAnimationFrame

基本写法:动画帧回调 requestAnimationFrame(<回调>)

// 在下次重绘前执行适合动画
function loop() {
    update();
    requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

基本写法:取消动画帧 cancelAnimationFrame(<id>)

// 取消动画帧请求
let id = requestAnimationFrame(loop);
cancelAnimationFrame(id);

requestIdleCallback

基本写法:空闲时段执行 requestIdleCallback(<回调>)

// 浏览器空闲时执行低优先级任务
requestIdleCallback(deadline => {
    while (deadline.timeRemaining() > 0) doWork();
});

基本写法:超时选项 requestIdleCallback(<回调>, { timeout: <毫秒> })

// 强制在指定时间内执行
requestIdleCallback(fn, { timeout: 2000 });

长任务拆分

基本写法:分片处理大数组 async function <chunk>(<数组>, <大小>)

// 使用 scheduler.yield 拆分
async function chunk(arr, size) {
    for (let i = 0; i < arr.length; i += size) {
        process(arr.slice(i, i + size));
        await scheduler.yield();
    }
}

基本写法:使用 setTimeout 让出 setTimeout(<回调>, 0)

// 拆分长任务避免阻塞
function chunk(tasks) {
    if (!tasks.length) return;
    tasks.shift()();
    setTimeout(() => chunk(tasks), 0);
}

错误捕获

基本写法:全局错误监听 window.addEventListener("error", <回调>)

// 捕获同步错误与资源加载失败
window.addEventListener("error", e => {
    console.log(e.message, e.filename);
});

基本写法:Promise 未处理拒绝 window.addEventListener("unhandledrejection", <回调>)

// 捕获未处理的 Promise 拒绝
window.addEventListener("unhandledrejection", e => {
    console.log(e.reason);
});

调试工具

基本写法:断点条件 if (<条件>) debugger

// 满足条件才触发断点
for (let i = 0; i < 1000; i++) {
    if (i === 500) debugger;
}

基本写法:logpoint 调试 console.log(<变量>)

// 使用浏览器 logpoint 不污染代码
// 在 Sources 面板设置行号日志

网络调试

基本写法:fetch 包装日志 function <fetchLog>(<url>, <选项>)

// 包装 fetch 记录请求耗时
async function fetchLog(url, opts) {
    let start = performance.now();
    let res = await fetch(url, opts);
    console.log(url, performance.now() - start);
    return res;
}

性能优化策略

基本写法:防抖节流 <节流>(<回调>, <等待>)

// 限制高频事件触发频率
window.addEventListener("scroll", throttle(fn, 100));

基本写法:事件委托 <父节点>.addEventListener(<事件>, <回调>)

// 减少监听器数量
list.addEventListener("click", e => {});

基本写法:虚拟列表 <容器>.addEventListener("scroll", <节流>(<回调>))

// 大列表只渲染可见项
function renderVisible(start, end) {
    container.innerHTML = items.slice(start, end).map(render).join("");
}

基本写法:IntersectionObserver 懒加载 new IntersectionObserver(<回调>)

// 图片进入视口才加载
let io = new IntersectionObserver(entries => {
    entries.forEach(e => {
        if (e.isIntersecting) { e.target.src = e.target.dataset.src; io.unobserve(e.target); }
    });
});
imgs.forEach(img => io.observe(img));

内存优化

基本写法:解除引用 <变量> = null

// 不再使用的大对象置 null 利于 GC
let bigData = loadData();
process(bigData);
bigData = null;

基本写法:WeakMap 弱引用 new WeakMap()

// 弱引用键不影响垃圾回收
let cache = new WeakMap();
cache.set(obj, data);

基本写法:对象池复用 function <pool>(<工厂>)

// 复用对象减少 GC 压力
function pool(factory) {
    let list = [];
    return { get: () => list.pop() || factory(), put: o => list.push(o) };
}

实用工具

基本写法:性能测量装饰器 function <measure>(<函数>)

// 自动测量函数执行时间
function measure(fn, name = fn.name) {
    return function (...args) {
        console.time(name);
        let result = fn.apply(this, args);
        console.timeEnd(name);
        return result;
    };
}

基本写法:FPS 监测 function <fpsMonitor>()

// 监测动画帧率
function fpsMonitor() {
    let last = performance.now(), frames = 0;
    function loop() {
        frames++;
        let now = performance.now();
        if (now - last >= 1000) {
            console.log("FPS", frames); frames = 0; last = now;
        }
        requestAnimationFrame(loop);
    }
    requestAnimationFrame(loop);
}