Canvas + DOM 混合渲染架构:高性能文本编辑器的性能优化实践

📅 发布时间:2026/10/20 16:29:10
Canvas + DOM 混合渲染架构:高性能文本编辑器的性能优化实践
1. 先聊聊为什么要把渲染层拆开如果一个项目已经走到了“必须用 Canvas DOM 混合渲染架构来做高性能文本编辑器”这一步那说明你多半已经见识过纯 DOM 方案在长文档场景下的真实表现了。正文从 5000 行开始每敲一个字符都感觉屏幕上冒出一层水雾那么重的延迟滚动时偶尔会掉帧选区拖拽一快就出现整块整块的空白搜个关键词高亮卡到让人怀疑是不是浏览器又崩了。这些问题的根源其实不是“DOM 能力不足”而是文本编辑器的交互密度和 DOM 树的维护成本不匹配。我做在线文档类产品做了快十年经历过从 contenteditable 硬刚、到虚拟滚动打补丁、再到彻底换成 Canvas 渲染内核的完整过程。说实话纯 DOM 方案不是不能用编辑器在 5000 字、一两屏、交互频繁的场景下体验完全能打。但当文档规模上去、或交互要求到了“跟手”“零闪烁”“丝滑滚长文”这个级别你迟早会撞上性能天花板。这时候把渲染工作交给 Canvas把交互锚点留给 DOM就是一种非常自然的架构演进方案。本文要聊的核心思路用一句话概括就是让 Canvas 负责“大面积、高频率、无结构语义”的文本渲染让 DOM 负责“小面积、低频、需要用户交互反馈”的局部界面。我会从分层规则、命中测试、输入法、协同编辑、性能实测到踩坑记录把这条混合架构的路线完整走一遍。适合那些正在做或准备做富文本编辑器、代码编辑器、笔记类产品、文档协作平台的前端同学参考。2. 纯 DOM 方案的长文档瓶颈问题到底出在哪2.1 DOM 节点数量膨胀是原罪先算一笔账。一份 10 万字符的文档假设每 200 字符切一个段落节点保底 500 个 p 标签每个字符本身不建节点但浏览器在排版的时候会为文本内容建立内部文本节点和行框如果还开了拼写检查、语法高亮、代码块换肤一次高亮就可能给几十个 span 染色。最终 DOM 树轻松破万是常态破三万也不是不可能。浏览器对几万个节点的文档不是渲染不了而是它没法只更新你改的那一段。任何一个输入操作都可能触发从 root 往下的样式重算和布局。React 这种框架还能靠 diff 算法减少实际改动的节点数但你挡不住浏览器自身的重排啊。比如你在第 8000 行插入一个字符浏览器要把后面所有文本再做一次几何计算。如果还叠加了 flex、grid 之类非确定性布局那一次击键触发整页布局的耗时可以直接翻倍。我之前实测过一个最坏案例某文档产品在 15 万字符的正文里插入一个回车整页 layout 耗时可以达到 260ms 以上。这个数字已经超过动画帧预算的四倍体感就是“键盘按下之后屏幕半天没反应”。2.2 布局抖动严重时滚动都在追帧长文本的第二痛点是滚动。很多编辑器为了支撑滚动会给正文挂 scroll 事件监听在滚动过程中动态隐藏远处的图片、懒加载代码块、更新阅读进度条。问题是滚动本身的高频触发很容易导致强制同步布局——只要你在 scroll 回调里读了 offsetTop、clientHeight 这类几何属性浏览器就得马上推翻它可能缓存的布局结果重新算一遍。如果每个滚动帧都触发一次强制同步布局帧率自然就掉到 30fps 以下。而且这种卡顿是“刚开始轻微、越滚越明显”的因为浏览器会不断在同步布局和异步渲染之间反复横跳页面滚到后半程就像陷入泥潭。2.3 contenteditable 的浏览器差异进一步放大成本还有个容易被忽视的问题contenteditable 的选区行为、光标移动、输入事件在不同浏览器里差异巨大。今天你花一整天调好的光标闪烁逻辑换一个浏览器内核可能直接失效。与其跟这些差异死磕不如自建渲染内核把文本怎么画、光标在哪里、选区怎么标完全握在自己手里。Canvas 方案恰好给了你这种掌控力代价是你得自己解决坐标映射、输入法和无障碍这些底层问题。这也就解释了为什么越来越大比例的在线文档编辑器和代码编辑器开始把渲染内核换成自绘方案。不是因为 DOM 不好而是因为“自绘 DOM 补充”的组合才能同时按得住性能和兼容性。3. 混合渲染架构的分工规则谁进 Canvas谁留 DOM3.1 我的分层依据频率、语义、交互密度混合渲染不是把页面一刀切成“上边是 Canvas、下边是 DOM”这么粗暴而是按照三者维度来切分第一是动态性。同一块区域内容变化频率越高越应该进 Canvas。正文文字会频繁插入/删除/调整排版选区在整个文档上随时随地出现和消失代码高亮要跟着输入实时变色——这三类就是高频内容。第二是语义交互。用户需要在文档上执行精确点击、选择、拖拽的区域如果 DOM 能更方便地响应事件就优先保留 DOM 来覆盖那部分交互热点。比如浮动工具栏、批注入口、图片上传按钮、链接预览卡片这些悬浮层如果也塞进 Canvas 去手绘和命中测试代码复杂度会爆炸。第三是无障碍需求。屏幕阅读器、浏览器翻译、选中即复制这些功能本质上依赖 DOM 文本节点的可访问性。Canvas 里画的字对辅助工具是不可见的。所以像可朗读的大标题、需要被搜索引用的目录文本、需要保持原生复制语义的代码块就得部分保留在 DOM 里。3.2 具体到编辑器里我画一个最小可行的分工清单下面这个表是我第一次落地混合架构时定出来的分工现在回看仍然觉得清晰实用渲染对象放在哪一层原因正文文本的绘制Canvas高频率更新、文本结构复杂、需要极致滚动性能光标闪烁Canvas光标位置要实时同步画成图形比移动 DOM 节点更节约选区高亮Canvas选中区域经常是任意形状用 Canvas 画菱形/不规则多边形最方便代码语法高亮颜色Canvas高亮 token 变化频繁一次性成片着色效率高搜索关键词标记Canvas需要快速遍历全文并批量绘制大量小矩形图片、视频、内容卡片的外框DOM这些资源需要加载进度、错误态、点击交互DOM 有天然的 img/video/button 能力浮动工具栏/评论气泡DOM交互密集、需要定位到某个锚点DOM 定位加样式组合最灵活编辑器的工具栏加粗、斜体、颜色DOM本身就是一组按钮直接原生 div 最省事大纲/目录侧栏DOM静态内容、低频更新没有性能压力回车后的输入提示下拉DOM需要复杂的文案异步加载、键盘上下键循环用 DOM 列表最稳注意这个分工不是固定答案不同产品可以根据自身形态调整。比如代码编辑器对代码块的行号、断点、折叠箭头的位置要求极高可能希望行号这一列单独留在 DOM实现更精确的纵向对齐文档编辑器则可能希望把标题和正文之间的层级线画进 Canvas因为层级线对位置精确度要求不高。关键在于你先定出“高频、动态、形状变化大”的内容进 Canvas“低频、交互、语义稳定”的内容留 DOM具体比例慢慢调。3.3 为什么我不建议全部自绘这几年也有不少项目追求“所有东西都画在 Canvas 里”从工具栏到弹窗到提示全自绘。我亲身体会下来这种做法的边际收益在遇到复杂交互时会迅速变负。你可能浪费三天去模拟一个原生 select 的下拉行为其实用一个 DOM 列表 20 分钟就能搞定。混合架构的核心价值不是“Canvas 比 DOM 好”而是“谁擅长干什么就让谁干什么”。Canvas 擅长的是无结构的像素级高频绘制DOM 擅长的是有语义、可交互、可聚焦的组件化界面。把两者揉在一起性能才有上限代码也才守得住底线。4. 核心难点坐标命中测试把鼠标点按翻译成文字位置4.1 为什么说这是混合架构的第一座大山纯 DOM 编辑器里你点击一下文本浏览器天然告诉你点到的是哪个字符——这是它的看家本领。但换了 Canvas 渲染正文后你手里只剩一个画布和一堆已经画完的像素浏览器只会告诉你“你在 (x, y) 点了一下”至于该把光标插到哪个字符后面纯靠你自己翻数学。这个问题的难度在于文本不是等宽的。中文字符宽度可能是一个汉字的 ch英文字母则是半个或三分之一个 ch不同字体、不同字号的混合排版行首缩进不一样代码块的等宽字体和正文的比例字体也不是一个度量体系。同一个 x 坐标在第 100 行和第 101 行对应的字符位置完全是两回事。4.2 我是怎么做的行索引 字符宽度逼近我采用的方案分三层第一层维护一个“行索引”。文本编辑器的渲染引擎在排版时会把文档按换行切成若干逻辑行每条逻辑行记录它的起始偏移、终止偏移、y 坐标、高度。这个索引在文档修改时同步更新可以用一个区间树或简单的数组来组织。有了它命中测试的第一步就变成遍历或者二分找到鼠标 y 坐标落在哪一行的 y 区间里。第二层过了行这关之后要把鼠标 x 坐标翻译成该行内部的章节偏移量。做法是从这行的 startOffset 开始按字符序列累加每个字符的宽度。每累计一段就与鼠标的 x 坐标做比较选最近的边界作为插入位置。这个过程听起来像是在逐字符遍历听起来 O(n) 会有点慢但实际上你只需要从行首开始跑几轮在命中目标位置附近就停下来整行的平均扫描长度很低。第三层对宽字符做特殊处理。遇到中文、日韩文、表情符号这类宽度不定的字符单纯用 width 相加不够稳。我平时会在排版阶段就把每一行渲染进一个离屏 Canvas然后通过 hitTestTextPosition 这一类的 Canvas API让浏览器直接告诉我“某个像素落在哪个字符范围内”。这比手写判断准得多代价是每次文本变更需要重建对应行的离屏快照。严格来说三种方法混着用的兼容性比较好。如果文档是纯英文只读场景用宽度逼近就够了如果涉及大量混排中文和表情符号离屏 Canvas 命中测试更稳如果要在协作竞态下保证准确性就拿行索引当兜底接受偶尔一次插入位置偏差。4.3 和 DOM 浮层的坐标换算混合架构里还有一个细节必须处理Canvas 和 DOM 浮层的坐标系统不是天然对齐的。Canvas 用的可能是内部的渲染坐标系——我一般会让 Canvas 的物理宽度等于 DOM 容器宽度乘以 devicePixelRatio同时让内部逻辑坐标保持 CSS 像素单位。这样浮层用 getBoundingClientRect 拿到的坐标在减去容器偏移后就能换算成 Canvas 的局部坐标。换算公式看起来很简单实际上坑点很多页面是否发生了 scale 缩放、容器有没有transform: translateZ(0)合成层、横向滚动和纵向滚动是否分离。我见过刚上混合架构的团队犯的最多的错就是忘了把滚动的 scrollLeft / scrollTop 加到换算里——结果浮动工具栏一滚就跑偏位置差个几十像素肉眼可见。核心原则是只允许“DOM 事件源坐标 - Canvas 内部坐标 - DOM 浮层坐标”这一条单向换算链路不要在多个坐标轴上跳来跳去否则 bug 排查成本高到你想把编辑器重写一遍。5. 渲染性能优化脏矩形、双缓冲与分块绘制5.1 脏矩形宁可多画一点也别整屏重刷Canvas 渲染最直观的性能陷阱就是每次输入都clearRect(0, 0, width, height)然后全量重画。这种写法在小的 readonly demo 里没问题但在长文档里等于每做一次编辑就把整个屏幕画一遍——白浪费 CPU/GPU。我的做法是引入“脏矩形”机制。每一帧里先把所有需要重绘的区域登记成一个或多个矩形再用clearRect(dirtyRect)清掉这些区域最后只在各区域内重新绘制受影响的文本。比如你输入一个字符涉及的重绘区域往往只有光标所在行附近的几百像素高一个选区变化也只涉及选区所在的若干行。把这些矩形合并后单次重绘的平均面积能控制在屏幕面积的 10%~30%滚动性能会有非常直观的提升。这里有个细节要提脏矩形的粒度不能太小。如果你每个字符的宽度变化都登记一个 5×20 的矩形浏览器对 Canvas 子区域的绘制效率反而会坍缩。一般的经验值是将横向位置按 10 像素、纵向位置按行高进行粗粒度的合并让重绘批次尽量稳定、数量尽量少。5.2 双缓冲先画到离屏再同步上屏双缓冲在游戏引擎里很常见在 Canvas 编辑器里同样适用。做法是维护一个离屏 Canvas 或者OffscreenCanvas所有绘制操作先打到离屏缓冲上全部完成后再用一次drawImage直接把缓冲推到可见 Canvas。这样一来屏幕不会出现“画了一半的残影”——要知道在连续滚动或高频选区变化时清屏和重绘之间哪怕只差 1 帧用户都可能看到闪烁。我实现的时候并没有让离屏缓冲永远是整页画布。对于编辑高频区域我只维护一个“视口重置缓冲”尺寸保持跟视口一致对于文本变更频繁的局部行维护一个更小的离屏 Canvas专门给行命中测试和脏矩形重绘用。这样双缓冲的成本被精确摊到真正有需要的局部区域上内存压力也小得多。5.3 分块绘制把文档切瓦片只画看得见的遇到超长文档整页 Canvas 在初始化的时候也没必要把 100 屏的内容全部画出来。我采用的是“瓦片地图”思路把文档按固定高度比如 2000px切成一个个瓦片区块每个瓦片对应一个独立的离屏 Canvas。视口滚动时加载或生成当前视口周边 1~2 个瓦片离开视口的瓦片可以直接释放或保留复用。这样做的收益非常明显。首屏渲染时间只等于首屏瓦片加相邻瓦片的绘制时间而不是整份文档的排版时间滚动时也只是新瓦片上台、旧瓦片离台几乎感受不到初始化压力。唯一要注意的是瓦片切换时的衔接如果瓦片生成得不够快滚动会有一部分区域出现空白。我的经验是“预加载当前视口前方的一块瓦片”配合滚动方向调整策略能达到不错的跟手效果。5.4 requestAnimationFrame 统一调度编辑器交互的事件频率非常高输入法 composition 事件每触发一次就可能有新的文本候选鼠标拖拽选区每秒移动几十次滚轮每滚一格都会改变视口。如果每个事件都立刻触发一次 Canvas 重绘你会发现自己陷入“重绘次数无限接近事件次数”的泥潭。我最终采用的是 rAF 统一调度。任何需要重绘的地方先把这个需求写进一个待重绘队列在下一帧开始时统一合并所有脏矩形再执行一次重绘。这样即使一帧里触发了 20 次事件最终也只会合并出一次绘制。这个方案的副作用是“轻微延迟”但因为只剩一个 16ms 级别的时间差人的感知基本可以忽略而帧率表现会稳定很多。6. 输入与光标Canvas 下的输入框、IME 合成与光标闪烁6.1 隐藏 DOM 文本输入框做焦点锚点要让 Canvas 编辑器像普通 contenteditable 一样接收键盘输入最稳的方案不是给 Canvas 绑 keydown而是在 Canvas 光标位置附近放一个透明的textarea或input作为隐藏输入锚点。原理上这个输入框不显示文本、不带边框、永远定位在当前光标所在坐标附近并保持足够小的视觉尺寸比如 1×1 然后裁剪掉滚动条。用户的所有输入事件、输入法合成事件、键盘事件都由这个元素天然接收。只需监听它的 input 事件把新增的文本送进渲染内核即可。这个方案最大的好处是你不用手动模拟输入法。中文输入法的候选窗弹出的位置、合成的过程、最后 confirm 的时刻浏览器和 OS 已经在系统层处理好了。你要做的只是把这个隐藏输入框的位置定准让候选窗在屏幕上出现在正确的位置。6.2 IME 合成窗位置修正是个大坑现在说说坑。隐藏输入框的位置如果只是静态定位在视口中心或者固定在左上角中文输入的候选窗就会出现漂移——你在文档第一行打字输入法候选窗却浮在屏幕最下方体验瞬间劝退。我的做法是每次光标移动或内容滚动后重新计算输入框的位置。计算时会拿当前光标所在 Canvas 内部坐标换算成屏幕坐标再减去滚动偏移最后把输入框 css top/left 设成这个值。同时要处理输入框尺寸如果输入框的宽度是默认的 100px在屏幕中间有一个大词块时输入法候选窗也会跟着错位。所以 IME 阶段我会把输入框设置成全宽但极小高度避免候选窗因为输入框尺寸太大而滑到奇怪的位置。另外compositionstart 到 compositionend 期间我不会去重绘选区或重置光标位置。因为 IME 合成期的文本变更和最终 confirm 的文本变更并不一致提前刷新渲染层很容易让光标“跳走”。正确做法是等 compositionend 拿到最终文本后再同步渲染内核。我最初忽略了这一点在输入法合成过程中监听 input 并立刻重绘结果用户用拼音输入“你好”的时候候选条还没选完正文就先乱了一下。6.3 光标闪烁与选区重绘的实现细节Canvas 光是画一条 1~2px 的竖线不算难难在让它“闪闪发光”而不产生额外性能损耗。我用的方案是光标状态记录为一个受时间控制的布尔值 —— 每 500ms 翻转一次但翻转后不立即重绘而是先做一个 8ms 左右的脏矩形登记下个 rAF 帧再把它画进去。曾试过用setInterval(() clearRect draw), 结果在慢速设备上光标闪动会变成“忽亮忽暗”观感极差改成 rAF 驱动后闪烁节奏就稳定了。选区的绘制更麻烦一点。Canvas 画选区时需要把文本区域按行拆成矩形每一行画一个透明度 0.3 左右的半透明矩形如果选区跨越多种字体、多种行高还得按字体边界切更多细块。为了性能我不会把选区画成一个连续路径而是直接用多矩形合并到一个离屏缓冲里一卷到底。RTL 模式和双向文本混排场景下选区形状会变成多边形到时候建议直接调用 Canvas 的Path2D去描形状而不是生硬地画矩形。7. 协同编辑适配远端增量如何驱动渲染层局部刷新7.1 从 OT/CRDT 定位受影响的行区间如果你的编辑器接入了协同那么除了用户本地输入还可能收到远端小伙伴的文本操作。在纯 DOM 场景里你只要把远端 op 应用到数据模型上再靠框架 diff 自动更新视图即可性能勉强能接受。但在 Canvas 渲染下你得自己判断“这份 op 改动了哪些视觉区域”。我的做法是把 op 拆到“增、删、改”三个基本动作然后去行索引里找这些动作影响的行区间。比如远端在第 500 行第 10 列插入 20 个字符受影响的是第 500 行的宽度和文本可能还牵动第 500 行后半段到第 510 行的换行位置。我先把这个区间定位出来打上脏矩形标记下一个重绘帧就只刷新这个区间而不会全屏重画。7.2 应用远端 op 的渲染视图更新顺序这里有一个关键时序问题是先改数据模型再重绘还是先重绘再改数据模型我的结论是先改数据模型再根据脏标记重绘。如果先重绘再改模型你会看到远端文本插入后旧文本和新文本在视觉上“叠”在一起然后突然跳到最终状态效果很生硬。合理顺序是接收远端 op更新文本数据模型包括行索引和渲染缓存计算受影响的行区间并注册脏矩形合并所有脏矩形统一在下一个动画帧内完成重绘7.3 与协同游标、在线头像的配合协同编辑器往往还有远端游标、选中高亮、在线用户头像这些元素。这些元素如果全部画进 Canvas性能很吃紧因为每个远端用户的光标位置和选区都可能实时变化。更稳的做法是把远端对象状态保存在渲染层之外由 Canvas 负责“画游标箭头和名字标签”但这部分也走脏矩形合并不逐个重画全屏。我一般建议远端用户列表和头像数字比如右上角一列放 DOM正文内联的远端光标和名字小气泡放 Canvas。如果需要让远端高亮跟随滚轮移动就把高亮区域拆成小矩形块与文本的脏矩形合并。这个方案在 5~10 个协作者同时在线时实测帧率依旧能稳定在 50fps 左右。8. 性能实测同一份长文档混合架构与纯 DOM 的对比光说不练不行。这里分享一组我自己的对比数据。测试环境是在某文档模拟项目上用同一份约 10 万字符、含 3000 个代码块标记的测试文档。对比了纯 DOM 方案使用虚拟滚动 contenteditable和 Canvas DOM 混合方案在几个关键指标上的差异指标纯 DOM 方案Canvas DOM 混合方案首屏渲染时间820ms410ms滚动平均 FPS31fps58fps连续输入 500 字符的输入延迟平均 210ms平均 98ms滚动时内存占用峰值430MB265MB选区拖拽时的掉帧比例11%1.2%需要说明的是不同实现的优化程度差异很大数字本身不绝对。但方向上比较一致的是混合方案在首屏时间、滚动流畅度、输入延迟上都明显占优尤其在内存占用和掉帧比例上提升空间更大。Canvas 把海量文本从 DOM 树中“抽”出来之后浏览器的重排压力一下子小了很多内存也因此降了不少。这个测试结果也侧面印证了如果你做的产品有“长文档 高频交互 实时协作”这三个要素那很值得考虑切换渲染内核。而如果只是短文案编辑、后台表单那大可不必折腾纯 DOM 足够。9. 踩坑记录三个让我 debug 到凌晨的混合渲染问题9.1 滚动不同步导致的“鬼影”残留刚上线后一段时间内有用户反馈说在 Windows 上快速上下滚动时Canvas 层边缘会残留上一帧的旧文本影子。排查下来发现是滚动监听和 Canvas 重绘顺序的问题滚轮事件触发了scrollTop更新但 Canvas 内部逻辑坐标没有同步更新理想情况下我应该在同一帧内完成两者对齐实际操作时却漏掉了滚动方向的下一帧偏差导致新旧内容在滚动衔接处“叠住”一帧。修复方式是给滚动事件增加一个节流队列先把滚动偏移计入一个待处理变量在下一次重绘帧内统一应用到 Canvas。同时给 Canvas 的内边距加一个超出容器高度的缓冲带让极端滚动帧不至于瞬间暴露空白。9.2 高分屏下 Canvas 文字发虚第一次在高分屏devicePixelRatio 2 或 3上测试正文文字明显发虚。原因很简单Canvas 的物理像素和 CSS 像素不一致如果不乘上 devicePixelRatio画出来的文字就会被浏览器按 2 倍不足的方式缩放拉伸。绕法我记得写过无数次了先canvas.width cssWidth * dpr再ctx.scale(dpr, dpr)然后所有绘制坐标全部使用 CSS 像素值。这样 Canvas 内部是以物理像素级别绘制的文字在屏幕上才会清晰。唯一要在意的代价是内存——高分屏下 Canvas 离屏缓冲的面积会变成 4 倍所以对瓦片缓存的数量要格外克制只保留视口附近的瓦片。9.3 输入法候选词连续换页时选区闪断用中文输入法连续翻候选词页的时候我遇到过一个很隐蔽的 bug候选词每翻一页正文选区就闪一下甚至偶尔光标位置跑偏一下。排查后发现问题出在 compositionupdate 事件触发时我没有区分“候选列表变化”和“文本确认变化”导致渲染层在合成期内执行了一次选区重绘。修复方式是给 composition 状态机加一个“合成中”标志在整个 composition 期间禁止重绘选区、禁止移动隐藏输入框的位置直到 compositionend 再统一同步。这个改动看起来很点单但对中文输入体验的改善是最直接的。10. 混合架构的边界什么时候不建议走这条路技术选型最怕的是“有了锤子看什么都是钉子”。Canvas DOM 混合渲染虽然解决了长文档性能问题但它并不是万能的。如果你的编辑器重度依赖以下能力那走纯 DOM 可能更稳复杂富文本格式多层嵌套列表、表格跨行合并、未定稿字体的大范围排版。Canvas 处理这种结构化布局线条宽度和边界计算会非常折磨人。无障碍与读屏支持屏幕阅读器依赖 DOM 的文本节点和 aria 语义。Canvas 画出来的内容读屏软件完全感知不到需要额外做大量屏幕外 DOM 镜像或 aria-live 通报成本不低。原生选区复制语义用户可能习惯用鼠标选择一段正文然后直接 CommandC 复制。DOM 原生支持Canvas 则需要自绘选区、自建剪贴板格式转换反而不如 DOM 顺手。频繁的第三方 DOM 组件嵌入像嵌入地图、流程图编辑器这些页面如果把容器盖在 Canvas 上又要处理 z-index、焦点穿透、缩放适配复杂度会比想象中高得多。混合架构更适合的是长文本阅读与编辑为核心富文本修饰为辅性能要求敏感的文档类产品。各产品的实际边界不尽相同最好的做法是先画出一条你服务的高频交互场景清单逐一判断“用户能不能接受这个交互在 1 帧内完成”能接受的进 DOM不能接受的进 Canvas。从我个人的实操体会来说不要一开始就全量替换。先加一个渲染层开关默认走 DOM 方案使用方打开“高性能模式”之后再切入 Canvas 内核对正文进行自绘渲染DOM 只承担浮层和信息展示这样遇到兼容问题还能随时回退。在迭代过程中你会发现真正让你放弃 DOM 方案的不是渲染速度本身而是你无法控制浏览器在超长文本上的布局细节而真正让你放弃全 Canvas 方案的也不是绘制性能而是你被输入法、无障碍和坐标映射折磨到怀疑人生。最终还是“各管一摊按需混合”的架构最能平衡开发效率与用户体验。