Yank Note 系列 02 - Markdown 渲染性能优化之路

Yank Note (opens new window) 是我编写的一款面向程序员的笔记应用。这里我将会写下一些关于 Yank Note 的文章

# 前言

在 Yank Note 开发初期,基本没考虑过性能这方面的问题。后面因为一些性能问题影响到了交互体验,开始着手优化性能。

这里我将描述为性能优化所做的工作:

  1. 引入虚拟 DOM
  2. 延迟复杂内容渲染
  3. 微调用户体验
  4. 优化浏览器渲染

# 引入虚拟 DOM

Yank Note 采用 Markdown-it 做解析器。Markdown-it 默认使用 HTML 输出。因此在 Yank Note 3.0 以前,都是采用给渲染容器 innerHTML 直接赋值的方式来做渲染。

Img

从上图可以看到,HTML 直出缺点很明显,每一次编辑文本,都要走一遍渲染流程,重新构建的 HTML。在每一次打字编辑的时候,都会造成页面重新布局,一些嵌入的功能如脑图、小工具也会重头渲染,导致页面闪烁,用户体验差。

所以这里需要想办法做增量渲染,即在编辑时候,只渲染变动部分。

考察了一些增量渲染方案,如 markdown-it-incremental-dom (opens new window)。因为本身这个项目使用 Vue,用 Vue 也能更好和 Vue 组件配合工作,所以后面决定使用 Vue 虚拟 DOM 的方式来做增量渲染。

Img

# 收益

从交互角度,分为两个部分来讨论:

  1. 首次渲染: 打开一个新的文件,HTML 直出性能和 虚拟 DOM 相差不大,或许比虚拟 DOM 还好一些,毕竟省略了虚拟节点构造的流程。
  2. 编辑渲染: 编辑文件,生成的 Vue VNode 虚拟 DOM 来,后续的渲染均由 Vue 优化接管,做增量渲染,这一点就比 HTML 性能高了太多了。

增量渲染的好处不仅仅是在编辑时候有更高的渲染性能,也能更好的支持 Vue 组件,优化如脑图、HTML 小工具等的交互体验。

# 具体实现

1. Token 流转换

在这个文件里 (opens new window),我将 Markdown-it 默认的 HTML 渲染器,替换为了自己的渲染器。

Markdown-it parse 后产生的是 token 流而不是语法树,token nesting 属性表示层级关系(-1 关闭标签,0 自闭合,1 开标签)。

因此在 token 流遍历过程中,我构建了一个栈来把 token 流转换为 VNode 树。对于开标签和自闭合标签,产生一个新的 VNode 节点,开标签入栈;对于闭标签,出栈上一个 VNode。

这对于大部分的渲染场景是足够了。

2. 插件兼容

一些 Markdown-it 插件定义了渲染方法,会输出 HTML 字符串。自定义渲染器还是要能处理他们,不然就不能享受 Markdown-it 的生态优势了。

参考张鑫旭大牛的文章 盘点HTML字符串转DOM的各种方法及细节 (opens new window),经过权衡还是选择了使用 innerHTML 的方法,足以覆盖大部分场景了。

3. HTML 解析

如果不进行 HTML 解析,那么上面的步骤是足够了。但是 Yank Note 作为一款异常开放的 Markdown 编辑器,怎么能不放开 HTML 渲染呢?

Markdown-it 解析器是支持 HTML 的,但是这些 HTML 根本就是原样输出的,也就说,Markdown-it 对于这些 HTML,从技术上都当作了普通文本一样,根本没解析成有结构的 token。

因此我参照原有得 HTML 解析器,写了一个新的 HTML 解析器。具体逻辑都在这个文件里面 (opens new window)

相比原有的全面 HTML 支持,这个解析器不能做到任意位置写 HTML,但是大部分情况下是可以满足了。

# 总结

引入虚拟 DOM,相当于对 Markdown-it 原有 render 做了一次大手术。一些特殊情况可能不能覆盖到,有问题的地方也能规避,总体来说收益较大。

# 延迟复杂内容渲染

先主要关注首次渲染,即用户打开文件后,第一次将 Markdown 渲染出来的过程。

我创建了一个性能测试文件,内容是Yank Note 的 Features 文档,复制粘贴后有 3600 行,渲染后有大量的复杂 Dom 结构。

我的电脑配置是 M1 芯片,8G 内存。再 dev 模式下开启应用,使用 Chrome 性能面板录制结果如下

Img

简直太可怕了,从打开这个文件到展示出来竟然要 7 秒钟!

性能优化上,有这样一句话:“你不能让计算机变得更快,你只能让它少做一点事”。

但是在涉及用户体验的优化上,我自己也总结了一句话:“如果你不能让计算机少做一点事,那么想办法让用户感知不到计算机正在做事”。

所以我这里做的优化主要是把自定义的一些复杂功能如脑图,小工具,都延迟渲染,不要阻塞渲染主要内容。

Img

优化后,能在一秒内展示出内容了。

不过这里要注意:如果要显示的元素是尺寸不定的,那就千万不要延迟渲染了。否者会造成页面闪烁和重排,将会严重降低性能。例如如 Mermaid 图形,它的渲染是依赖 DOM 的,如果延迟渲染,性能将会变得更低。

这里测试 600 个图形,如果使用 setTimeout 0 延迟渲染,则将耗费 40 秒钟(CPU 重负荷时间计)!浏览器花了大量时间重排。

Img

而如果直接准备内容,则浏览器只用花费 2 秒多就完成,CPU 重载时间也不超过 5 秒。

Img

为什么不懒加载,等到了可视区域再渲染?其实我个人更希望渲染出来的东西直接一点就好,不要有额外的动作。另外平时使用不会像做测试这么极端,个人觉得不用懒加载会有更好的体验。

# 微调用户体验

这里主要关注编辑时,从用户输入文字到渲染的过程。

编辑这一部分是 Monaco 编辑器做的事,本身 Monaco 就已经做得很好了,无需操心。

渲染这块,上面已经引入了虚拟 DOM。一般的文件没什么问题,从按键到渲染出来只需要几毫秒。Markdown 解析过程,一般来说 1 毫秒都用不到。

但是如果编辑一个超大的文件,这里的 Markdown 解析成本就不可忽略了。

再创建一个性能测试文件,内容是 Yank Note 的 Readme 文档,复制粘贴后有一万行。

看一下 Markdown 解析时间

Img

解析这个一万行的文件,parse 过程都需要 100ms。

这个解析时间暂时我想不到什么办法来优化,所以那就优化用户体验吧。

未优化前,我是使用一个固定时间间隔的防抖方法来做渲染。在大文件中,这个防抖间隔就显得不够用了,打字非常卡。

那么就把渲染防抖函数的等待时间设置为动态的吧。记录 render 时间,然后动态更新 debounce 时间。小文件设置短一点,大文件长一点。在编辑大文件时,优先保证输入体验。

另外针对中文输入的情况,监听编辑器 composition 事件,在打字时候暂停渲染,这也能提升一部分的用户体验。

经过上面的处理和其他一些微调的优化后,在这个测试文件内打字,虽然还是有些卡顿,但情况好了不少。

# 优化浏览器渲染

我再次创建了一个两万行,60 万个字符的测试文件,编辑卡顿的情况进一步加剧了。

但是Yank Note 采用的是 Monaco 编辑器,理论上说文字编辑性能应该会和 VSCode 一致,然而我用 VScode 编辑这个两万行的文件,几乎没有卡顿,所以问题应当出现在我的代码上。

再次使用 Chrome 性能面板,排查问题。发现每一次按键后,浏览器花费了大量时间在布局上。

Img

当我尝试用 Chrome 的层面板功能来看渲染时候,不幸的事情发生了,页面竟然崩溃了。可能页面元素太多,调试器扛不住了吧。

那就猜想一下造成重新布局的原因:整个应用的布局我是采用的 Flex 布局方式,每一次渲染要根据内容情况来计算整个页面的样式和布局。然而理论上渲染 Markdown 的位置和其他地方是无关的,因为所有的内容都在一个可滚动的盒子里。这个盒子的高度是不会随着渲染内容变化而变化,应该不存在重排的问题。

验证猜想,我把 Markdown 渲染容器设置为 100vh,不管输入打字或者更改窗口尺寸,都不会有问题了,那问题就一定是出在这上面。

所以优化方式就是将 Markdown 渲染容器定位设置为绝对定位,问题解决。现在在这个 2 万行,60万字符的文件里面打字,基本上和 VSCode 没什么两样了,更改窗口尺寸也不卡顿了。

Img

# 进一步优化

上面的优化只是一些大的和容易的方面,还有一些不好优化的地方。

  1. markdown-it-attrs (opens new window) 插件性能比较低,在渲染大文档时候花费了不少时间,看代码还有很大的优化空间。后续:已经重写这个插件,性能提高了 132 倍。Yank Note 系列 07 - 性能暴增 132 倍的秘密——重写
  2. 尝试将解析 Markdown 的过程放入 worker 中,不阻塞主线程
  3. 如有可能,尝试 Markdown 增量编译,即只编译编辑的内容 (opens new window),但是这似乎不容易做到。

好了,上面就是我优化 Yank Note 渲染性能过程中的一些记录,如果你对 Yank Note 感兴趣,想使用或者参与贡献,可以到 Github (opens new window) 了解更多。

本文由「Yank Note - 一款面向程序员的 Markdown 笔记应用 (opens new window)」撰写