你有没有这样的经历,思源越来越卡了。
那么你想不想知道,思源内部正经历着什么?是什么导致它越来越卡了?能否揪出那些卡顿的家伙?
是的,这一切都可以用火焰图来查明真相。
如何查看进程占用 cpu
为什么加了这个标题,因为有些萌新可能不知道怎么查看 cpu 占用。
思源的主要进程有:
1️⃣ 内核进程 2️⃣ 主进程 3️⃣ 渲染进程 4️⃣devtools 进程
怎么知道 4 是 devtools 进程?只需要关闭开发者工具,看哪个进程退出就知道了,没退出的就是渲染进程。
我们写的代码主要会影响 3 渲染进程,一般情况下看这个进程的 CPU 占用就行了,但,如果你频繁调用内核 api 的话,还要加上 1 内核进程的 CPU 占用。
不过,性能面板监测的是渲染进程的 CPU 占用。
如何使用
通常一些系统或软件卡顿由两部分组成,空闲负载和交互负载。
所谓空闲负载就是,这里指不做任何操作,静止状态的性能占用。如果这块就很高了,那么你稍微一操作就会卡顿了。
交互负载,这里是指,你的操作带来的性能占用啦,比如,输入,滚动,点击等。
然后针对你自己的需求,进行不同场景下的测试。
这里以空闲负载为例进行说明火焰图的使用。
首先在主菜单打开“开发者工具”,然后切换到“性能”选项卡。如图
然后点左上角的圆圈按钮即开始进行性能监测了。
如果你需要监测输入时的性能,这时候就要在文档中输入,点击或其他操作类似
然后,你就看到下图的监测结果
然后火焰图就在“主要”里,展开它
从图上可以看出,红色部分是耗能较大的部分。
x 轴代表任务占用时间,越长代表占用时长越久,也意味着可能有性能问题,一般超过 50ms 就能感觉到有卡顿感了(通常人眼能感知的操作响应延迟在 100ms 内较为流畅,但渲染任务如果超过 50ms,可能会导致掉帧(低于 20fps),进而造成卡顿感)。
y 轴是函数调用栈(调用层级关系,调用链),思源里是倒置火焰图,即上面是调用发起者,下面是被调用者。
滚轮下滚放大火焰图,上滚缩小火焰图,可用鼠标左右拖动。
最顶上的截图,亦可清晰的看到你操作的步骤。
点击火焰图,打开下方的调用树,即可看到调用关系
点击右侧链接,可进入代码耗时查看
其他功能
也可导入导出报告
怎样知道哪些有性能问题
1)如果一个任务占用时长较久,即存在性能问题。
2)虽然一个任务占用时长不太久,但频繁被执行,可能出现无限死循环,应当检查并消除。
3)任务引发了重绘和渲染,如果在大循环中或被执行较频繁,应当把会引起重绘或布局的操作放到循环外,待循环完成一次重绘或布局。
只要消除了这些性能问题,思源静止状态性能占用应该在 1% 以下,甚至趋近于 0%。
经过一番折腾后,我的思源从之前的静止状态 25% 左右降低到了 0.5% 左右。
whenElementExist 之坑
这里不得不提的一个函数 whenElementsExist
function whenElementsExist(selector) {
return new Promise(resolve => {
const checkForElement = () => {
let elements = null;
if (typeof selector === 'function') {
elements = selector();
} else {
elements = document.querySelectorAll(selector);
}
if (elements && elements.length > 0) {
resolve(elements);
} else {
requestAnimationFrame(checkForElement);
}
};
checkForElement();
});
}
自从这个函数进入社区后,感觉这个函数大有被滥用之势。
诚然,这个函数很常用,也很好用,但这个函数是有缺陷的。
下面分析下这个函数
这个函数最关键的代码是 requestAnimationFrame 函数,这个函数的意思是等待下一帧执行。
什么意思呢?
这里的帧是指屏幕刷新的意思,即每次屏幕刷新都会调用该函数,但仅当页面发生重绘时才会调用,如果页面没有发生变化也不会执行。
一般屏幕的刷新频率是 60HZ/秒,那么这个函数就会 16ms 被调用一次。
但它与 setInterval(()=>{}, 16) 不同的是,requestAnimationFrame 与刷新频率一致,这样就会让画面看起来更流畅,不会掉帧(但它不适合执行重量级或深层查询,否则会影响帧率)。
利用这个特性,whenElementsExist 就实现了定时监控元素出现的功能。
但这个 whenElementsExist 函数有逻辑上的问题,即只要你等待的元素一定会出现,就没有问题,但,如果不出现会怎样呢?
它会进入无限循环,每 16ms 执行一次检查,直到目标出现为止,如果目标一直不出现就会造成一定的性能浪费,虽然比较小,但如果这样的脚本多了也会有一定影响。
所以,解决办法是用下面的函数代替,它会设置超时时间,当目标一直不出现时会自动停止检测。
function whenElementExist(selector, node, timeout = 5000) {
return new Promise((resolve, reject) => {
let isResolved = false;
let requestId, timeoutId; // 保存 requestAnimationFrame 的 ID
const check = () => {
try {
const el = typeof selector === 'function' ? selector() : (node || document).querySelector(selector);
if (el) {
isResolved = true;
cancelAnimationFrame(requestId); // 找到元素时取消未执行的动画帧
if (timeoutId) clearTimeout(timeoutId);
resolve(el);
} else if (!isResolved) {
requestId = requestAnimationFrame(check); // 保存新的动画帧 ID
}
} catch (e) {
isResolved = true;
cancelAnimationFrame(requestId);
clearTimeout(timeoutId);
reject(new Error(`Timeout: Element not found for selector "${selector}" within ${timeout}ms`));
return;
}
};
check();
timeoutId = setTimeout(() => {
if (!isResolved) {
isResolved = true;
cancelAnimationFrame(requestId); // 超时后取消动画帧
reject(new Error(`Timeout: Element not found for selector "${selector}" within ${timeout}ms`));
}
}, timeout);
});
}
或 简洁版
function whenElementExist(selector, node = document, timeout = 5000) {
return new Promise((resolve, reject) => {
const start = Date.now();
function check() {
let el;
try {
el = typeof selector === 'function'
? selector()
: node.querySelector(selector);
} catch (err) {
return reject(err);
}
if (el) {
resolve(el);
} else if (Date.now() - start >= timeout) {
reject(new Error(`Timed out after ${timeout}ms waiting for element ${selector}`));
} else {
requestAnimationFrame(check);
}
}
check();
});
}
但,requestAnimationFrame 方法也不是万能的,有些情况它监测不到。
比如,一个元素或属性在 16ms 内出现并消失,它就监测不到了。
不过,这种情况下,可以用 whenElementsExist 的变种,来代替
function whenElementExistByObserver(selector, node, timeout = 5000) {
return new Promise((resolve, reject) => {
let disposed = false;
let timer = null;
const observer = new MutationObserver(() => {
if (disposed) return;
const el = typeof selector === 'function'
? selector()
: (node || document).querySelector(selector);
if (el) {
observer.disconnect();
clearTimeout(timer);
disposed = true;
resolve(el);
}
});
observer.observe(node || document.body, {
childList: true,
subtree: true,
});
timer = setTimeout(() => {
if (!disposed) {
observer.disconnect();
disposed = true;
reject(new Error(`Timeout: Element not found for selector "${selector}" within ${timeout}ms`));
}
}, timeout);
// 立即检查一次
const initialEl = typeof selector === 'function' ? selector() : (node || document).querySelector(selector);
if (initialEl) {
observer.disconnect();
clearTimeout(timer);
disposed = true;
return resolve(initialEl);
}
});
}
但,这种方法也有监测不到的情况,比如监测的不是 dom 变化,是数据变化等,它就无能为力了。
所以可以用 requestAnimationFrame 和 MutationObserver 两者配合使用。
不过,如果是数据监测也可以用对象数值变化事件监测,不过,一般情况 requestAnimationFrame 就够了。
发现的 bug
在测试过程中发现“文档树文档置顶和设置颜色”和“简单锁定笔记”代码有 whenElementsExist 出现无限循环情况,已经修复了,建议升级。
传送门:
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于