你有没有这样的经历,思源越来越卡了。
那么你想不想知道,思源内部正经历着什么?是什么导致它越来越卡了?能否揪出那些卡顿的家伙?
是的,这一切都可以用火焰图来查明真相。
如何查看进程占用 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 出现无限循环情况,已经修复了,建议升级。
传送门:
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于