YankNote是我写的一款面向程序员的笔记应用。这里我会写一些关于YankNote的文章前言在YankNote开发的早期,性能问题基本没有被考虑过。后来因为一些性能问题影响了交互体验,我们开始优化性能。在这里我将描述为性能优化所做的工作:引入虚拟DOM以延迟复杂内容渲染和微调用户体验引入虚拟DOMYank注意使用Markdown-it作为解析器。Markdown-it默认使用HTML输出。所以在YankNote3.0之前,都是采用直接给渲染容器的innerHTML赋值的方式进行渲染。从上图可以看出,HTML的缺点很明显。每次编辑文本时,都必须经过渲染过程并重建HTML。每次打字编辑,页面都会重新布局,一些内嵌的功能如思维导图、widgets也会重新渲染,导致页面闪烁,用户体验差。所以这里我们需要想办法做增量渲染,也就是在编辑的时候只渲染变化的部分。查看了一些增量渲染解决方案,例如markdown-it-incremental-dom。因为项目本身使用了Vue,Vue也可以和Vue组件更好的协同工作,所以后来决定使用Vue虚拟DOM进行增量渲染。从交互的角度来说,好处分两部分来讨论:首先渲染:打开一个新文件,HTML直接输出的性能与虚拟DOM相差不大,可能比虚拟DOM好,之后all,省略了虚拟节点构建的过程。编辑渲染:编辑文件,生成VueVNode虚拟DOM,后续渲染由Vue优化接管做增量渲染,比HTML性能高很多。增量渲染的好处不仅是编辑时渲染性能更高,还可以更好地支持Vue组件,优化思维导图和HTML小部件等交互体验。具体实现1.Token流转换在这个文件中,我将Markdown-it默认的HTML渲染器替换为我自己的渲染器。Markdown-it解析后,生成一个token流,而不是语法树。token嵌套属性表示一种层次关系(-1关闭标签,0自关闭,1打开标签)。所以在token流的遍历过程中,我搭建了一个栈,将token流转化为VNode树。对于openlabels和self-closinglabels,生成一个新的VNode节点,并将openlabel入栈;对于闭合标签,前一个VNode从堆栈中弹出。这对于大多数渲染场景来说已经足够了。2.该插件兼容一些Markdown-it插件,这些插件定义了将输出HTML字符串的渲染方法。自定义渲染器还是需要能够处理的,否则无法享受到Markdown-it的生态优势。参考张新旭大牛的文章查看HTML字符串转DOM的各种方法和细节。权衡之后,我选择使用innerHTML方式,足以覆盖大部分场景。3.HTML解析如果不需要HTML解析,那么上面的步骤就足够了。但是YankNote作为一款极其开放的Markdown编辑器,怎么可能放不下HTML渲染呢?Markdown-it解析器支持HTML,但是这些HTML基本上是原样输出的,也就是说,Markdown-it在技术上将这些HTML视为普通文本,根本不会将它们解析成结构化的标记。所以我在原来的HTML解析器的基础上写了一个新的HTML解析器。具体逻辑在这个文件中。与原来全面的HTML支持相比,这个解析器不能在任何地方写HTML,但在大多数情况下是可以满足的。总结虚拟DOM的引入,相当于对Markdown-it的原始渲染进行了一次大的操作。一些特殊情况可能不会覆盖,有问题的地方可以避免,总体来说好处更大。延迟复杂内容渲染重点放在第一次渲染,也就是用户打开文件后第一次渲染Markdown的过程。我创建了一个性能测试文件,内容是YankNote的Features文档,复制粘贴后有3600行,渲染后有很多复杂的Dom结构。我的电脑配置是M1芯片,8G内存。以dev模式打开应用,使用Chrome性能面板记录结果如下,简直惨不忍睹。从打开这个文件到显示它需要7秒!在性能优化方面,有一句话说:“你不能让计算机更快,只能让它做的更少”。但是说到用户体验的优化,我也总结了一句话:“如果你不能让电脑少做点什么,那就想办法让用户感觉不到电脑在做某事”。所以我这里做的优化主要是延迟一些自定义的复杂功能比如思维导图和小工具的渲染,不至于挡住主要内容的渲染。虽然后面可能会重新布局页面,但是所有块元素的问题都会缓解很多。优化后,内容可在一秒内显示。微调用户体验这里我们主要关注用户输入文本到编辑时渲染的过程。编辑这部分是摩纳哥编辑器所做的。摩纳哥本身已经做好了,不用担心。渲染这块,虚拟DOM上面已经介绍过了。普通文件没问题,从击键到渲染只需要几毫秒。Markdown解析过程一般不到1毫秒。但是如果你编辑的是一个非常大的文件,这里的Markdown解析成本是不容忽视的。再新建一个性能测试文件,内容为YankNote的Readme文档,复制粘贴后10000行。看一下解析这个10000行文件的Markdown解析时间,解析过程需要100ms。暂时想不出有什么办法可以优化这个解析时间,那就优化一下用户体验吧。在优化之前,我采用固定时间间隔的防抖方式进行渲染。在大文件中,这个防抖间隔不够,打字很卡。然后将渲染防抖功能的等待时间设置为动态。记录渲染时间,然后动态更新去抖动时间。为小文件设置更短,为大文件设置更长。编辑大文件时,优先保证输入体验。另外,对于中文输入,监听编辑器合成事件,打字时暂停渲染,也可以提升部分用户体验。经过上面的处理和一些其他的微调优化,敲入这个测试文件,虽然还是有点卡顿,但是情况已经好了很多。优化浏览器渲染我再次创建了一个20,000行和600,000个字符的测试文件,编辑延迟进一步加剧。但是YankNote使用Monaco编辑器。理论上,文本编辑性能应该和VSCode一致。但是我用VScode编辑这个20000行的文件,几乎没有卡顿,所以问题应该出现在我的代码中。再次使用Chrome性能面板来解决问题。浏览器在发现每个按键后会花费大量时间进行布局。当我尝试使用Chrome的图层面板功能查看渲染时,不幸的是,页面崩溃了。可能是页面元素太多,调试器处理不了。然后猜测重新布局的原因:整个应用的布局我使用的是Flex布局方式,每次渲染都需要根据内容计算整个页面的样式和布局。不过理论上Markdown渲染的位置与其他地方无关,因为所有的内容都在一个可滚动的框中。这个盒子的高度不会随着渲染内容的变化而改变,所以应该不会有重排的问题。为了验证猜想,我将Markdown渲染容器设置为100vh,无论打字还是改变窗口大小都不会出现问题,所以问题一定出在这。所以优化的方法是将Markdown渲染容器的定位设置为绝对定位,问题就解决了。现在在这个20000行600000字符的文件中打字,基本上和VSCode一样,改变窗口大小也不卡顿。进一步优化上面的优化只是一些大而容易的方面,还有一些优化得不好。markdown-it-attrs插件的性能比较低,渲染大文档需要很多时间。代码上还有很大的优化空间。尽量把解析Markdown的过程放到worker中,不阻塞主线程。可能的话,试试Markdown的Incrementalcompilation,也就是只编译编辑好的内容,不过这好像不太好做。好了,以上就是优化YankNote渲染性能过程中的一些记录。如果你对YankNote感兴趣,想使用或贡献,你可以去Github了解更多。
