Appearance
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。
额外两个“随时命中”的断点(不用录制)
- Elements:对你页面的根容器右键 Break on → Subtree modifications / Attribute modifications。 一旦某组件不停插 DOM 或改样式,调试器会直接在那行代码断住。
- 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)给最小修改片段,并在复杂行上一行加中文注释。