ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Markdown编辑器性能优化:从全量渲染到虚拟滚动与Token化架构的重构实践

Markdown编辑器性能优化:从全量渲染到虚拟滚动与Token化架构的重构实践 两个月重构 Markdown 编辑器2 MB 文档约 1 秒打开事情是这样的。年初帮团队维护一个内部知识库系统前端是用老掉牙的 jQuery 生态堆起来的里面嵌了一个 Markdown 编辑器说难听点就是个带预览的 textarea。刚开始大家也就记点会议纪要、贴点 API 文档随便用用没人在意性能。结果后来有人把整个项目的历史变更记录、几十篇带大量代码块和 Base64 图片的 Markdown 全塞进去了单文件经常干到 1~2 MB整个编辑器直接卡到打不出字保存一次要转圈三五秒满屏全是用户的抱怨。于是我接手做了两个月的重构。最终效果是2 MB 的 Markdown 文档从打开到可编辑耗时在 1 秒上下打字延迟体感上完全消失长文档滚动跟手光标跳转不闪白。这篇文章就是这次重构的完整复盘——从架构选型到核心实现再到底层算法的取舍和后续踩坑全部记录下来。如果你也在维护一个 Markdown 编辑器、或者任何“大文本 实时渲染”型工具这篇内容应该能帮你避开不少我趟过的雷。先说结论这次重构最核心的思路就一条——别把 Markdown 文档当文本处理要把它当作一棵可控的 Token 树来处理。所有性能优化到最后都是围绕“如何减少重复计算”和“如何减少 DOM 操作”这两件事展开的。1. 重构前的定位与目标拆解1.1 旧版编辑器到底慢在哪拿 2 MB 的 Markdown 文件来说纯文本大约是 40 万到 60 万个字符换算成行数通常在 8 万到 15 万行之间。旧版编辑器用的是“全量渲染”策略每次按键触发change事件然后对整个 Markdown 源码进行正则解析生成整篇 HTML塞进预览区。打字的时候每次 Keydown 都要走过“取全文 → 正则匹配 → 拼接 HTML → 整段 innerHTML”这条链路。很多人可能以为正则解析 60 万字符应该很快实际上快的是正则本身慢的是整篇 HTML 的构建和 DOM 替换。一次输入触发一次全量 innerHTML 赋值浏览器需要把 40 万节点的字符串重新解析成 DOM 树然后再做一次完整的样式重算和布局。这个过程在 2 MB 级别下单次要花费 300~800 毫秒。你要是连打十个字那就是十次全量重排的排队和累积用户体感自然就是“卡成 PPT”。更隐蔽的坑是旧版把编辑区和预览区做成了两个独立的滚动容器每次操作两边的滚动位置都要做同步补偿这里又有大量scrollTop的反复赋值和强迫浏览器同步布局的开销。1.2 重构的坐标系先定边界再动手重构之前我先把需求按照“必须达成”“尽量达成”“暂不追求”三个层次列了出来。这一步很关键因为编辑器领域特别容易掉进功能深渊——今天想加个表格编辑器明天想加个实时协同一旦战线拉长性能优化根本排不上优先级。必须达成支持 2 MB 级别约 10 万行Markdown 文档在 1 秒内完成打开渲染。编辑过程中打字不产生明显卡顿单次输入响应不超过 50 ms。编辑区和预览区保持实时联动。保留原有的大纲跳转、代码块复制、表格渲染等基础功能。尽量达成预览区支持较为流畅的滚动浏览。支持基础 Markdown 语法和 GFM 扩展表格、删除线、任务列表。依赖的最小化方便后续移植。暂不追求实时协同编辑那玩意是另一个级别的复杂度。自研富文本排版引擎直接用 contenteditable 的坑谁踩谁知道。移动端适配的精细调优。1.3 两条技术路线的灵魂对决我在立项初期其实纠结了很久是走 CodeMirror 6 / Monaco 这类成熟的代码编辑器底座还是自研一个轻量的“文本域 渲染层”方案CodeMirror 6 的架构我非常欣赏它的文档模型Text本身就是个持久化数据结构对超大文档做了分段处理视图层也有Viewport的概念只渲染可视区域内的行。理论上用它做底座是最稳妥的。但我调研之后发现两个很现实的问题第一CodeMirror 6 为了通用性带了很多我们用不到的模块包体积和初始化成本都偏高第二也是更要命的团队里没人熟 CodeMirror 6 的插件体系而我们要做的 Markdown 预览联动、大纲跳转、代码块渲染都不太容易在它的模型上直接实现学习成本摊进去两个月不一定够。所以最后我选了“自研 textarea 虚拟滚动 分区 Token 化解析”这条路。textarea 负责处理键盘输入和光标管理我们只负责两件事缩放 textarea 的内容行高来匹配虚拟滚动的位置以及把可视区域里的文本段提取出来做局部解析。这套方案的工程量可控所有核心逻辑都握在自己手里排查问题比依赖黑盒要舒服得多。提示如果你不需要预览联动只要“超大 Markdown 文件的编辑体验”直接用 CodeMirror 6 或者 Monaco 会更省事。自研虚拟滚动属于“可控性强、工程量大”的路线适合有明确需求边界和足够排期的情况。2. 核心架构设计与关键决策2.1 数据流从“全量管道”改成“增量环形队列”旧版的架构本质上是一条管道textareavalue → parser → html → preview DOM每次输入走完全程。新架构我把数据流改成了三层源图层textarea 的 value 始终是真相源source of truth我们不去动它只读取。模型层由源文本切分出的“块数组”block list每个块是一个逻辑段落标题、列表项、代码块、引用块等。呈现层只根据可视范围从块数组里取需要的块做局部解析和渲染。这个改动最重要的变化是不再因为一次按键触发全量解析。每次输入事件到达时我们只对“光标所在的块”和“可能受影响的相邻块”做重新解析然后对比新旧 Token 结果决定要不要更新 DOM。其余块的 Token 和渲染结果原样保留。这里有个比喻特别好用旧的方案是每次交作业都推倒重写一整本作业本新的方案是打开作业本只擦掉你做错的那一行用修正带补上新内容其他页面翻都不翻。2.2 魔改 textarea 行高撑起一棵“虚拟的”参天大树textarea 天然只显示少量文本滚动时浏览器帮你控制可视区的内容。但在 10 万行的文件里textarea 的所有文本依然在内存里而且它的渲染高度最多只能到 33 万多像素Chrome 里大概是 33,554,428px。这个高度上限直接限制了我们不能用“真实撑开”的方式实现虚拟滚动。我的做法是textarea 里不装载全部文本只保留可视区域上下的“窗口文本”。然后通过调整 textarea 的height样式和padding-top/padding-bottom填充让滚动条模拟出完整文档的滚动范围。具体公式是滚动条总高度 全部块累计高度总和 可视区 textarea 高度 可视区容纳的块累计高度 padding-top (当前滚动位置对应的块之前的累计高度) padding-bottom (总高度 - padding-top - 可视区高度)这样浏览器就会认为 textarea 里有一个很高的文档滚动条比例看起来和真实文档一致。当用户滚动时我们监听scroll事件重新计算当前可视区对应的块区间再更新窗口文本。这个方案最大的难点在于如何准确测量每个块的高度。Markdown 渲染后的高度受字体、行高、图片尺寸、代码块换行等因素影响纯靠估算一定会导致滚动条跳动或光标错位。我最终的方案是用一个隐藏的测量行measure line来动态测量每个块的渲染高度然后按块缓存起来。每当字号、主题或图片加载状态变化时自动失效缓存。2.3 为什么说 Token 化是“唯一真神”Markdown 解析这个环节我见过很多开发者直接用正则一行行做匹配然后立刻拼 HTML。这在短文档下没问题但在 10 万行场景下会暴露两个致命问题正则的“回溯灾难”某些嵌套结构比如列表里的引用里的代码块会让正则引擎陷入反复回溯单次解析时间从几毫秒变成几百毫秒。没有中间表示DOM 更新只能全量替换。重构后的方案是引入一个轻量级的 Token 化层先把 Markdown 源文本按行切块再用“游标扫描”的方式逐个字符生成 Token。每个 Token 记录类型heading、bullet_list、code_block、quote、plain_text等、起始偏移、结束偏移、嵌套层级和原始文本范围。有了 Token 树之后渲染就变成了一个“Token → DOM 节点”的单纯映射过程。这一层隔离的意义非常重大解析器和渲染器彻底解耦。以后想更换渲染主题、增加新语法支持只需要在渲染器里做映射不需要动解析器。同样如果想支持“部分更新”只需要重新解析被修改的那几个块重新生成局部 Token再做局部 DOM 更新。这里顺手记录一个 Token 化的关键细节不要把整个文件一次性 Token 化成一个大数组而是按块懒加载。我们维护一个块索引表每块对应一段源文本只有被渲染到可视区的块才执行真正的 Token 化。这样即使文件 10 万行初次打开的 Token 化计算量也只有可视区那几十行自然快得飞起。注意懒加载 Token 化意味着“大纲”这类功能不能只依赖已解析的块。我的做法是打开文档后在后台空闲时间requestIdleCallback分段扫描全文档的标题 Token只有大纲数据会被全量构建正文渲染保持局部化。这样既保证大纲秒出又不阻塞主线程。3. 实操过程与核心环节实现3.1 环境准备与工程结构调整项目技术栈我选了 Vite TypeScript纯前端不依赖任何框架。原因很直接Markdown 编辑器的渲染频率非常高如果引入 React 或 Vue每一次局部刷新都要走一遍虚拟 DOM diff这层开销在 10 万行的场景下会被放大得很难看。如果你非要用框架不可记住一个原则编辑区核心链路滚动、输入、光标尽量避免框架的响应式代理用原生 DOM 操作直连。初始化工程npm create vitelatest md-editor-refactor -- --template vanilla-ts cd md-editor-refactor npm install目录结构按功能模块拆好src/ core/ blockManager.ts # 块索引与懒加载管理 tokenizer.ts # Markdown Token 化解析器 renderer.ts # Token 到 DOM 的渲染器 viewport.ts # 虚拟滚动可视区管理 ui/ editor.ts # textarea 封装与输入事件处理 preview.ts # 预览区渲染入口 statusBar.ts # 状态栏字数、解析耗时等 utils/ measure.ts # 隐藏测量行 debounce.ts # 防抖工具3.2 Token 化解析器的实现要点我不会贴整份解析器代码因为那个实在太长了。这里只讲核心设计模式。解析器入口接收一段源文本和起始块号输出一个 Token 数组。整个过程是逐行扫描状态机驱动。核心状态有NORMAL普通段落IN_CODE_BLOCK在围栏代码块内IN_QUOTE在引用块内IN_LIST在列表内需要记录嵌套层级IN_TABLE在 GFM 表格内做宽度要对齐状态机的好处是不需要回溯就能正确处理多行结构。关键的伪代码逻辑type Token { type: string; startLine: number; endLine: number; text: string; depth: number; meta?: Recordstring, string | number; }; function tokenizeBlock(lines: string[], startLine: number): Token[] { const tokens: Token[] []; let state NORMAL; let currentToken: Token | null null; for (let i 0; i lines.length; i) { const line lines[i]; const trimmed line.trim(); // 围栏代码块检测 if (/^/.test(trimmed)) { if (state IN_CODE_BLOCK) { state NORMAL; currentToken.endLine startLine i; tokens.push(currentToken); currentToken null; } else { state IN_CODE_BLOCK; currentToken { type: code_block, startLine: startLine i, text: , depth: 0, }; } continue; } if (state IN_CODE_BLOCK) { currentToken!.text line \n; continue; } // 标题 const headingMatch /^(#{1,6})\s(.*)$/.exec(trimmed); if (headingMatch) { if (currentToken) { tokens.push(currentToken); currentToken null; } tokens.push({ type: heading${headingMatch[1].length}, startLine: startLine i, endLine: startLine i, text: headingMatch[2], depth: headingMatch[1].length, }); continue; } // 列表项 const listMatch /^(\s*)([-*]|\d\.)\s(.*)$/.exec(line); if (listMatch) { if (currentToken currentToken.type ! list_item) { tokens.push(currentToken); currentToken null; } if (!currentToken) { currentToken { type: list_item, startLine: startLine i, text: , depth: Math.floor(listMatch[1].length / 2), }; } currentToken.text line \n; continue; } // 其他情况段落文本 if (currentToken currentToken.type ! paragraph) { tokens.push(currentToken); currentToken null; } if (!currentToken) { currentToken { type: paragraph, startLine: startLine i, text: , depth: 0, }; } currentToken.text line \n; } if (currentToken) { tokens.push(currentToken); } return tokens; }你一定注意到了这个状态机不会对段落内部的“加粗”“行内代码”“链接”做精细拆分。这是有意的——行内语法我们放在渲染阶段用正则做小范围匹配。因为段落内部的行内 Token 化开销相对可控而且行内结构很少跨段不会引发回溯灾难。把块级解析和行内解析分离开能让块级解析特别快、特别稳。3.3 虚拟滚动排版与高度缓存虚拟滚动的核心是一个块高度管理表blockHeightCache。每个块在首次渲染完成后用offsetHeight读取真实高度缓存在 Map 里。当滚动事件触发时计算当前滚动位置scrollTop通过前缀和数组二分查找定位起始块索引。具体实现class ViewportManager { private blockHeights: number[] []; private prefixSum: number[] []; private rebuildPrefixSum() { this.prefixSum [0]; for (let i 0; i this.blockHeights.length; i) { this.prefixSum[i 1] this.prefixSum[i] this.blockHeights[i]; } } // 二分查找给定 scrollTop返回对应块索引 findBlockIndex(scrollTop: number): number { let low 0; let high this.prefixSum.length - 1; while (low high) { const mid Math.floor((low high) / 2); if (this.prefixSum[mid] scrollTop) { low mid 1; } else { high mid; } } return Math.max(0, low - 1); } }这里要注意的是块的高度不是永远不变的。比如用户正在打字一个空段落变成带文字的标题高度可能从 20px 变成 40px。如果缓存不失效滚动位置就会错乱。我的经验是每当块的内容发生变化输入事件的撤销/重做、代码块折叠展开等立即将该块的缓存高度标记为 dirty并在下一次帧渲染时重新测量。另外一个容易踩的坑是图片加载。Markdown 里如果有一张还没加载完成的图片它的初始高度可能只有几十像素等图片加载完成会撑开几百像素导致后面的所有块整体下移。我的处理方式是给预览区的图片设置一个默认的占位高度比如 240px等load事件触发后用真实尺寸更新块高缓存并做一次滚动位置的补偿修正。3.4 样式与主题适配重构后我把渲染层和样式层彻底分离。预览区里的每个 Token 类型对应一个 CSS 类比如.md-block { font-family: var(--md-font, SF Mono, Consolas, Courier New, monospace); line-height: var(--md-line-height, 1.7); } .md-heading1 { font-size: 2em; font-weight: 700; margin: 1.2em 0 0.6em; border-bottom: 1px solid var(--md-border, #e5e5e5); padding-bottom: 0.3em; } .md-code-block { background: var(--md-code-bg, #f6f8fa); border-radius: 6px; padding: 12px; overflow-x: auto; margin: 0.8em 0; } .md-quote { border-left: 4px solid var(--md-quote-border, #d0d7de); padding-left: 1em; color: var(--md-quote-color, #57606a); }主题适配只需要覆盖一组 CSS 变量非常干净。对用户来说这也意味着他们可以像写 CSS 一样自定义自己的 Markdown 样式不需要修改任何 JS 逻辑。3.5 用骨架屏让首屏“看起来”更快2 MB 文档从点击打开到真正渲染完成即使经过优化也要大约 1 秒的时间。这 1 秒如果白屏用户会以为程序挂了。所以我加了一个轻量的骨架屏机制文档打开后先根据文件的总字节数和平均行高粗略估算总高度在预览区渲染出一片灰白相间的条纹占位背景并显示“正在解析文档… 38%”这类进度提示。具体实现是在解析过程中不断上报已解析的块数比例用requestAnimationFrame驱动进度条。这样给用户的心理预期就完全不一样了——不是“卡住了”而是“正在加载”。3.6 性能数据实测与对比重构完成之后我拿一份 2 MB、约 11 万行的 Markdown 文档做了完整的性能测试。测试环境是 Chrome 116、MacBook Pro M1。数据如下指标旧版重构后首次打开到可编辑8.7 秒0.94 秒输入一个字符后的响应延迟约 340 毫秒8 毫秒连续输入 10 个字符的总耗时4.1 秒60 毫秒文档内跳转大纲点击1.2 秒120 毫秒滚动过程中的平均帧率12 FPS58 FPS内存占用峰值890 MB205 MB内存占用的大幅下降是意外之喜原因是旧版每次全量渲染都会产生一次巨大的 HTML 字符串和 DOM 树垃圾回收又没来得及回收新版因为是局部渲染和复用 DOM存活对象数量少了一个数量级自然不吃内存了。4. 常见问题与排查技巧实录4.1 光标错位与跳动虚拟滚动编辑器的天敌就是光标错位。最常见的原因是padding-top和 textarea 内部的换行结构没有匹配上导致浏览器默认光标定位用的坐标和显示的字符位置对不上。排查思路可以用三步走定位第一步关闭虚拟滚动用全量 textarea 渲染看看光标是否正常排除输入法或浏览器扩展的干扰。第二步检查 padding-top 与实际窗口文本的行号偏移是否一致——可以临时在文本开头添加一个特殊标记看看光标是不是落到标记之后。第三步确认窗口文本的行数是否与预估高度一致尤其是空行空行高度容易估算错误。我最终的修复方案比较暴力但有效编辑区不用 textarea 自带的软换行wrapoff设成wrapoff这样所有文本都是物理一行对应逻辑一行行高由 CSS 统一控制为line-height的整数倍高度计算就精确到“行数 × 行高”不再依赖像素测量。这是虚拟滚动类编辑器的最优解代价是横向出现滚动条但对 Markdown 编辑这个场景完全可接受。4.2 输入法组合态下的高亮错乱中文输入法在 textarea 里输入时会有一个“组合态”composition阶段。这个阶段里textarea 的 value 变化是连续的但其实内容还没有确认。如果在组合态期间触发解析和渲染会出现高亮一半中文、一半英文的混乱状态甚至可能把组合中的拼音当成 Markdown 语法解析掉。我的处理方式是监听compositionstart和compositionend事件let isComposing false; editorEl.addEventListener(compositionstart, () { isComposing true; }); editorEl.addEventListener(compositionend, () { isComposing false; scheduleRender(); }); editorEl.addEventListener(input, (e) { if (isComposing) return; scheduleRender(); });同时预览区的更新全部改到requestAnimationFrame里合并这样即使一次输入事件触发了多次 DOM 变更渲染也只执行一次。4.3 长文档下代码块和表格断裂当虚拟滚动把文档切成一个个块渲染时代码块如果从一个块跨越到另一个块样式会断裂——上面的部分是浅灰背景中间突然变成正文下面又变成浅灰非常难看。这和分页打印时表格跨页断裂是同一个问题。解决方案是块管理器维护一个“合并块”的概念。如果连续的多行都属于同一个代码块Token 类型相同就合并成一个渲染单元而不是按物理行拆分。这样整个代码块要么完整出现在可视区内要么整个被移出不会出现半截渲染的情况。在实际渲染时如果代码块特别高比可视区还高就需要内部再做一次子分页但这种情况极少遇到时可以降级为全量渲染该代码块性能影响也可控。4.4 大纲跳转和滚动定位的精度修复大纲跳转的本质是“已知目标块索引滚动到目标块顶部”。这个功能看起来简单但和虚拟滚动组合后就变得很微妙如果直接算目标块的前缀和然后设置 scrollTop理论上是对的但由于块高度缓存可能存在脏数据落地时位置会偏差几十像素甚至差出一个屏幕。我的修复方法是“两步跳转”第一次先按估算的前缀和设置 scrollTop然后等渲染完成后读取真实的目标块 offsetTop再次修正 scrollTop。也就是说跳转分“粗定位”和“精修正”两帧完成中间只隔一次 requestAnimationFrame用户无感知但精度可以做到像素级。4.5 性能监控手段重构过程中我给编辑器加了一个小的监控面板用PerformanceObserver采集长任务long task和布局抖动layout shift的数据在调试模式下显示在状态栏里。这个小面板帮了大忙因为很多性能问题不能靠肉眼感知——比如一次看似无害的scrollTop赋值可能反而触发整页的同步 layout。如果你是边开发边调试强烈建议在状态栏持续输出这几个指标单次渲染耗时renderTime当前可视块数量块高度缓存命中率主线程长任务数量内存变化通过performance.memory有了这些数字你能非常清晰地知道每次改动是变快了还是变慢了而不是靠“感觉”。5. 重构过程中沉淀的几个关键经验5.1 性能优化优先做减法而不是做加法这次重构最大的认知升级是旧版卡顿的根因是架构问题不是某个函数写得不够快。即使我把正则表达式优化到极致把 innerHTML 换成 createContextualFragment全量渲染带来的卡顿依然会存在。真正解决问题的是减少“要做的事”而不是让每件事变得更快。所以开动之前先想清楚哪些是可以不做的往往比寻找更快的实现更重要。5.2 状态唯一来源single source of truth编辑器是一个非常容易出现“状态不同步”的场景textarea 里的文本、预览区的 DOM、大纲的数据、光标的偏移、滚动的位置这些东西如果各自为政稍微改一处另外几处就乱套。这次重构我立了一条死规矩textarea 的 value 是唯一权威数据源一切渲染和状态计算都由它派生。预览区的 DOM 是 source 的“投影”大纲是“索引”滚动位置是“游标当前观察点”。任何地方想改状态都必须通过“修改 value → 触发更新”的方式完成。这条规矩在最开始会增加一点开发成本不能图方便直接操作 DOM但越到后期越能感受到它的价值——编辑器几乎不会出现“内存态和界面态不一致”的诡异 bug。5.3 给自动化测试留后路重构这种核心模块不做测试就是在走钢丝。我给三个核心模块blockManager、tokenizer、viewport都写了独立的单元测试。尤其是 tokenizer我准备了一批特殊用例空行开头、Windows 换行符、混合缩进、嵌套引用、表格中的竖线、代码块内的 Markdown 语法、HTML 标签等等确保状态机在各种情况下都不会“跑飞”。在真实输入事件和滚动事件上做自动化测试比较麻烦但业务逻辑不依赖 DOM 事件所以单元测试的价值依然立得住。后续如果还有人动这块代码回归测试直接跑一遍比靠“点两下看看”要可靠得多。5.4 时间预算与迭代节奏两个月的时间我在排期上做了个三七开前两周做技术调研和架构验证中间五周做核心功能开发最后一周专门做性能调优和 bug 修复。实际执行下来性能调优永远不够一周——所以如果你也要做类似的重构建议至少给性能打磨留出两周。特别是虚拟滚动这类对交互细节要求极高的模块真正的坑往往是在“好像已经能用了”之后才暴露出来的。另外有个建议不要第一天就冲进代码里。先用一天时间把你手头最大的那个 Markdown 文件、最慢的操作、最难受的交互全部列出来量化好“现在慢到什么程度”。后面的每一次优化都对照这个基线去衡量。这样既不会做无用功也能在团队 review 时拿出清晰的数据说服别人。6. 后续扩展方向和踩坑预告重构完成之后我又花了一点时间把预览区升级成了“可交互预览”。做法很简单预览区不再是一块纯只读区域而是叠加了一层透明的 canvas 事件层用于捕获光标位置和点击坐标这样用户点击预览区的标题时编辑区的光标会自动跳到对应的 Markdown 源文本位置。这个功能实现成本不高但对“看文档写文档”的使用习惯是有明显改善的。具体实现是先遍历 Token 树为每个标题 Token 记录它在预览区 DOM 中的 offsetTop然后在点击事件里二分查找最近的标题 Token再调用编辑器的光标定位方法。还有一个我觉得值得做的方向是“代码块折叠”。Markdown 文档里如果包含大量很长的代码块预览时用户体验非常糟糕。可以考虑给代码块 Token 增加一个 collapsed 状态折叠时只显示第一行和展开按钮高度只有一行。这个功能在技术上没有任何难度只是需要在块高度缓存上多做一层失效逻辑——折叠操作之后所有后续块的前缀和都要全部重算。如果文档本身就是长文档这个重算可以做得很克制因为一次折叠只影响折叠块自身的高度后续块高度都没变完全可以做成增量更新。再往后如果团队有富文本导出或 PDF 导出的需求Token 树这套架构也能直接复用你只需要写一个新的 renderer 输出 HTML 片段再交给打印引擎即可完全不需要动解析器。这也是当初把 Token 化作为独立一层带来的最大红利。根据我个人的体会编辑器这类工具是最值得做性能投入的——它不是给用户看一眼就走的页面而是很多人每天要盯上好几个小时的生产力工具。一次输入延迟半秒一天下来累积的无谓等待和心理摩擦是非常惊人的。所以如果你手头也有一个越用越卡的 Markdown 编辑器或者任何类似的“大文档 实时预览”类工具别犹豫尽早做一次架构层面的重构。先把“全量更新”改成“局部更新”再把“全部解析”改成“懒解析”性能问题的雪崩大概率就能直接止住。这次重构里的两个小细节最后再分享一下吧一个是在 textarea 上设置spellcheckfalse和autocorrectoff能显著减少输入过程中的潜在重排另一个是预览区尽量使用contain: content的 CSS 属性把渲染边界锁死从根上避免编辑器内部改动引发外层页面布局抖动。这俩都是几行代码的事但对体感提升非常明显。如果你正打算动手建议开场就先加上。
返回列表