YankNote是我写的一款面向程序员的笔记应用。这里我会写一些关于YankNote的文章前言在之前的文章中,我讲了一些针对YankNote做性能优化的事情。然而,影响应用体验的不仅仅是性能,更重要的是功能是否满足用户需求,界面是否美观,交互操作是否合理流畅,运行是否稳定。为保证应用程序的稳定运行,除了减少bug外,内存占用也应尽可能小且稳定。YankNote是使用网络技术开发的。通常,Web应用不需要太在意内存占用,因为用户用完就走,如果卡住了,可以刷新页面解决。但是YankNote不同。用户通常会一直打开该应用程序,当前电脑通常不会长时间关机。因此,控制内存使用,减少内存泄漏成为YankNote稳定运行的必然。在v3.14.2版本中,YankNote对内存做了很多优化。本文将介绍在解决内存泄漏的过程中做了哪些工作。实验方法和排错工具实验场景用户在使用YankNote的过程中,会输入更多的文字,在文档之间切换。有两个场景是关注潜在内存泄漏的重要地方。得益于YankNote开放的API,我只需要写几个脚本就可以在实际应用中模拟以上两种操作。渲染脚本:模拟文本输入后渲染for(leti=0;i<100;i++){ctx.view.render();awaitctx.utils.sleep(50);}文档切换脚本:模拟文档切换for(leti=0;i<30;i++){awaitctx.doc.showHelp('FEATURES.md');等待ctx.utils.sleep(1000);等待ctx.doc.showHelp('README.md');awaitctx.utils.sleep(2000);}该工具使用Chrome隐私选项卡来排除插件干扰。使用了Chrome的性能监控面板、内存面板、性能面板。过程中运行模拟脚本,使用Chrome性能监视器查看内存增长曲线,配合内存快照功能找出内存泄漏点。vue虚拟节点内存泄漏运行渲染脚本后发现内存增加了很多。这意味着在用户正常打字后,系统占用的内存会逐渐增加。经过排查,我找到了问题的关键:在代码中使用h('div')不会导致内存泄漏,但是使用h(CustomComponent)会导致内存泄漏。在上一篇文章中,为了优化渲染性能,YankNote引入了Vue的VNode,效果非常好。每个元素都是一个新创建的VNode。但是对于自定义功能组件,使用的是最初创建的组件,我的代码会追加孩子而不是替换,所以造成内存泄漏。自定义组件的children现阶段是没用的,所以解决办法是先判断VNode类型type,type是string和fragment节点再插入children。百度脑图组件内存泄漏我使用百度的kitityminder-core可视化脑图。最初,这个库应该只服务于它自己的脑图服务。基本上它只考虑页面只有一个实例,所以在事件监听和全局资源使用上非常随意。虽然脑体实例提供了一个destroy方法,调用后抛出错误,hack一下就可以完成流程,但是很多事件还是没有取消,全局变量也没有释放。解决事件监听kityminder所有的全局DOM事件监听发生在类新建的时候。所以我干脆在新类之前重写了window.addEventListener记录监听了哪些方法,然后在销毁constrealAddEventListener=window.addEventListener.bind(window)constevents:{type:string,listener:any}[]=[]window.addEventListener=(type:string,listener:any)=>{logger.debug('hackaddEventListener',type)events.push({type,listener})realAddEventListener(type,listener)}constkm=newwindow.kityminder.Minder()//恢复原来的事件监听window.addEventListener=realAddEventListener//销毁并调用events.forEach(({type,listener})=>{window.removeEventListener(type,listener)})解决全局变量使用kityminder内部使用kity库,也是百度出品。这个库给原型绑定了很多内部事件,没有地方回收资源。而且YankNote可能会在页面中嵌入多个思维导图实例,所以粗略地杜绝这些全局变量的使用是不现实的。所以我借用了微前端的思想,所以每次新建一个类,使用一个全新的类。移动了kity的源代码,暴露了模块导入功能。Object.defineProperty(window,'kity',{get:()=>{logger.debug('newkitty')returnwindow.kityM()},})Object.defineProperty(window,'kityminder',{get:()=>{logger.debug('newkityminder')returnwindow.kityminderM()},})每次创建新类时,都会再次调用模块的初始化方法,创建新类。虽然初始化效率比共享一个类低,但也比iframe方式效率高。日志打印优化运行文件切换脚本后,内存使用量迅速飙升,一度达到1G,非常恐怖。抓拍后发现泄漏点在Luckysheet函数中。但是我通过iframe引入了Luckysheet。按理说,iframe销毁后,应该释放所有内部资源。为什么内存还没有释放?而且更要命的是,刷新页面后,这些记忆都不会被破坏!这让我很疑惑,会不会是我遇到了Chrome的bug?仔细观察heapsnapshot,发现有很多DetachedWindows,就是Lucksheet使用的iframe。你甚至可以在控制台中进入这个Window,看到this和globalThis都没有被清理干净。这真的是Chrome错误吗?后来做了很多实验,发现只有Luckysheetiframe有这种情况,其他iframe是不会的。那么一定是Luckysheet和其他的不一样造成的。最后发现Luckysheet会在控制台打印objectlog,而其他iframe不会!而这就是罪魁祸首!其实一开始查的时候看到了Devtoolsconsole这几个字,但是因为除了这个地方还有其他的mountreferences,所以一时没想起来。至于为什么刷新页面后内存还是没有释放,我猜测应该是Chrome控制台有保留日志的功能,即刷新页面后,日志仍然可以显示。如果要支持这个功能,不释放其中的内存是合理的。解决方案:生产环境嵌入iframe关闭日志打印,通过重写控制台实现。console.warn=()=>0console.log=()=>0console.info=()=>0console.dir=()=>0经过上面的处理,运行switchdocument脚本后,内存基本没有了更长的增长。结语:技术水平不够的时候,不要随便怀疑浏览器;日志中尽量不要打印多层引用的对象,生产环境关闭日志打印。其他使用Chrome的内存工具也发现了其他几个内存泄漏问题,基本都和闭包的使用有关,这里不再赘述。小结做了上面的工作之后,整个应用的内存占用就好了很多。在反复切换复杂文档后,Javascript堆内存占用也能保持在一个合理的水平。开发过程中需要注意闭包的合理使用,第三方库的使用也需要检查是否满足性能和内存使用要求。如果你对YankNote感兴趣,想使用或贡献,你可以去Github了解更多。本文由《YankNote——一款给程序员的Markdown笔记应用》撰写
