思源性能优化指南:如何使用火焰图排查卡顿问题

你有没有这样的经历,思源越来越卡了。

那么你想不想知道,思源内部正经历着什么?是什么导致它越来越卡了?能否揪出那些卡顿的家伙?

是的,这一切都可以用火焰图来查明真相。

如何查看进程占用 cpu

为什么加了这个标题,因为有些萌新可能不知道怎么查看 cpu 占用。

思源的主要进程有:

image.png

1️⃣ 内核进程 2️⃣ 主进程 3️⃣ 渲染进程 4️⃣devtools 进程

怎么知道 4 是 devtools 进程?只需要关闭开发者工具,看哪个进程退出就知道了,没退出的就是渲染进程。

我们写的代码主要会影响 3 渲染进程,一般情况下看这个进程的 CPU 占用就行了,但,如果你频繁调用内核 api 的话,还要加上 1 内核进程的 CPU 占用。

不过,性能面板监测的是渲染进程的 CPU 占用。

如何使用

通常一些系统或软件卡顿由两部分组成,空闲负载和交互负载。

所谓空闲负载就是,这里指不做任何操作,静止状态的性能占用。如果这块就很高了,那么你稍微一操作就会卡顿了。

交互负载,这里是指,你的操作带来的性能占用啦,比如,输入,滚动,点击等。

然后针对你自己的需求,进行不同场景下的测试。

这里以空闲负载为例进行说明火焰图的使用。

首先在主菜单打开“开发者工具”,然后切换到“性能”选项卡。如图

image.png

然后点左上角的圆圈按钮即开始进行性能监测了。

image.png

如果你需要监测输入时的性能,这时候就要在文档中输入,点击或其他操作类似

然后,你就看到下图的监测结果

image.png

然后火焰图就在“主要”里,展开它

image.png

从图上可以看出,红色部分是耗能较大的部分。

x 轴代表任务占用时间,越长代表占用时长越久,也意味着可能有性能问题,一般超过 50ms 就能感觉到有卡顿感了(通常人眼能感知的操作响应延迟在 100ms 内较为流畅,但渲染任务如果超过 50ms,可能会导致掉帧(低于 20fps),进而造成卡顿感)。

y 轴是函数调用栈(调用层级关系,调用链),思源里是倒置火焰图,即上面是调用发起者,下面是被调用者。

滚轮下滚放大火焰图,上滚缩小火焰图,可用鼠标左右拖动。

最顶上的截图,亦可清晰的看到你操作的步骤。

点击火焰图,打开下方的调用树,即可看到调用关系

image.png

点击右侧链接,可进入代码耗时查看

image.png

其他功能

image.png

也可导入导出报告

image.png

怎样知道哪些有性能问题

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 出现无限循环情况,已经修复了,建议升级。

传送门:

[js] 文档树文档置顶和设置颜色 [0.0.8 完美版]

分享代码片段实现简单锁定笔记

  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    26298 引用 • 109336 回帖
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    201 引用 • 1444 回帖 • 2 关注
3 操作
wilsons 在 2025-06-25 08:58:55 更新了该帖
wilsons 在 2025-06-20 09:46:57 更新了该帖
wilsons 在 2025-06-19 17:00:38 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • 我还发现 有部分插件或者 js 滥用 preventDefault , 会导致 mousewheel 被阻塞, 导致鼠标滚动文档时会严重卡顿(实际上没有啥开销的)

  • HugZephyr 1 评论

    whenElementExist 的死循环那个, 好早之前我就发现了, 然后优化了一波, 哈哈

    // 功能: 监听直到元素存在 // 找到 selector 时,执行 func_cb,监听超时时间默认为 4s // selector: string | #id | function function whenExist(selector, func_cb, time_out = 4000) { console.log("whenExist begin", selector); return new Promise((resolve) => { const startTime = Date.now(); // 记录开始时间 const checkForElement = () => { let element = null; // 根据selector类型进行查找 if (typeof selector === 'string') { if (selector.startsWith('#')) { element = document.getElementById(selector.slice(1)); } else { element = document.querySelector(selector); } } else if (typeof selector === 'function') { element = selector(); } else { // 若 selector 不合法,直接退出 console.error("Invalid selector type"); resolve(false); return; } if (element) { // 元素存在时,执行回调并解析Promise if (func_cb) func_cb(element); resolve(true); } else if (Date.now() - startTime >= time_out) { // 超时处理 console.log(selector, "whenExist timeout"); resolve(false); } else { // 元素不存在且未超时,继续检查 requestAnimationFrame(checkForElement); } }; // 开始检查元素是否存在 checkForElement(); }); }
    👍
    wilsons
wilsons
正在努力开发 wilsons 工具箱中 🛠️ 目前已正式入驻爱发电啦!💖 想催更、提需求?欢迎访问 👉 https://afdian.com/a/wilsons