Skip to content

title: 不用录屏,让页面自动采样定位前端卡死 date: 2025-11-08 tags: [前端调试, 性能, 卡顿, Chrome, 采样]


可以不用一直录屏。思路是:让页面自己在“异常迹象”出现时自动开始采样/断点,或者按固定间隔做短采样。下面三段脚本直接贴到 Console 就能长期跑,开销很小;等到“CPU 突然 100%”或 DOM/监听器暴涨时,会自动留下可用的 Profile调用栈


① 长任务/卡顿即时抓包(自动开 5 秒 CPU Profile)

js
// 复杂:监控 Long Task,一旦 >200ms 立刻开始5秒 CPU Profile 并打印归因信息
(() => {
  let profiling = false;
  const po = new PerformanceObserver((list) => {
    for (const e of list.getEntries()) {
      if (e.duration > 200 && !profiling) {
        profiling = true;
        const label = `spike-${Date.now()}`;
        console.warn(
          "[LongTask]",
          e.duration.toFixed(1),
          "ms",
          e.attribution || []
        );
        console.profile(label); // 开始采样
        setTimeout(() => {
          console.profileEnd(label);
          profiling = false;
        }, 5000);
      }
    }
  });
  po.observe({ entryTypes: ["longtask"] });
})();

② DOM/监听器“暴增”触发采样(定位是谁在疯狂改动)

js
// 复杂:5 秒窗口统计 DOM 新增与监听器注册;超过阈值则自动 Profile + 断点
(() => {
  let addNodes = 0,
    addListeners = 0,
    profiling = false;

  // 统计 DOM 新增
  const mo = new MutationObserver((muts) => {
    for (const m of muts) addNodes += m.addedNodes?.length || 0;
  });
  mo.observe(document.documentElement, { childList: true, subtree: true });

  // hook addEventListener
  const origAdd = EventTarget.prototype.addEventListener;
  EventTarget.prototype.addEventListener = function (type, listener, opts) {
    addListeners++;
    return origAdd.call(this, type, listener, opts);
  };

  // 每 5 秒检查一次阈值
  setInterval(() => {
    const nodes = addNodes,
      lis = addListeners;
    addNodes = 0;
    addListeners = 0;
    if (!profiling && (nodes > 500 || lis > 300)) {
      profiling = true;
      const label = `surge-${Date.now()}`;
      console.warn("[surge]", { nodes, lis });
      debugger; // 复杂:命中时先断点查看当前栈
      console.profile(label);
      setTimeout(() => {
        console.profileEnd(label);
        profiling = false;
      }, 5000);
    }
  }, 5000);
})();

③ 轮转短采样(不用等触发,长时间“守株待兔”)

js
// 复杂:每 3 分钟自动采样 10 秒,形成滚动档案;长时间运行也很轻量
(() => {
  const period = 180000,
    duration = 10000,
    keep = 6;
  let idx = 0,
    labels = [];
  setInterval(() => {
    const label = `roll-${++idx}-${new Date().toLocaleTimeString()}`;
    labels.push(label);
    if (labels.length > keep) labels.shift();
    console.profile(label);
    setTimeout(() => console.profileEnd(label), duration);
  }, period);
})();

采样结果会出现在 Performance / Profiler 面板中(名称是 spike-*surge-*roll-*)。 打开任意一条后:看 Bottom-Up 排序 Self time,Top1 ~ Top3 的函数/URL:行号就是主因;或在火焰图直接点到函数跳转 Sources


额外两个“随时命中”的断点(不用录制)

  1. Elements:对你页面的根容器右键 Break on → Subtree modifications / Attribute modifications。 一旦某组件不停插 DOM 或改样式,调试器会直接在那行代码断住。
  2. Sources → Threads:把 Main 和每个 Dedicated Worker 都展开,勾上 Pause 图标; CPU 一满时,切到忙的线程按 || 暂停,看 Call Stack 直接定位。

得到“文件:行号”后怎么修(只给必要片段)

  • 监听器/定时器重复注册(路由切换/重连)
ts
// 复杂:挂载时注册,卸载时清理;避免累计
onMounted(() => {
  const onDoc = (e: MouseEvent) => {
    /* ... */
  };
  document.addEventListener("click", onDoc);
  const timer = setInterval(poll, 5000);
  onUnmounted(() => {
    document.removeEventListener("click", onDoc);
    clearInterval(timer);
  });
});
  • 遮罩/弹层隐藏仍拦截点击
css
/* 复杂:隐藏态不吃事件,根治“看不见的罩子” */
.mask,
[aria-hidden="true"] {
  pointer-events: none !important;
}
  • 重循环/批量 DOM 导致长任务
js
// 复杂:重循环每1000次让出主线程,避免一口气卡死
async function processBig(list) {
  let fuse = 0;
  for (let i = 0; i < list.length; i++) {
    if (++fuse % 1000 === 0) await Promise.resolve(); // 让步
    // ... 原逻辑 ...
  }
}

把你采样得到的 Top 函数/URL:行号(或截图)发来,我按你用的框架(Vue/React/Next)给最小修改片段,并在复杂行上一行加中文注释。

本站总访问