2026/10/14 3:28:54

Markdown 导出 HTML 实战:2.6MB 编辑器快照如何瘦身 71%(DOM 净化 + CSSOM 运行时裁剪)

Markdown 导出 HTML 实战:2.6MB 编辑器快照如何瘦身 71%(DOM 净化 + CSSOM 运行时裁剪) Markdown 导出 HTML 实战2.6MB 编辑器快照如何瘦身 71%DOM 净化 CSSOM 运行时裁剪摘要本文记录 MarkPin 编辑器导出 HTML 的完整瘦身实战——从 2.6MB 降到 753KB-71%。核心思路是先把导出文档而非编辑器的产品语义拍板再做五项 DOM 净化去光标回显/选区/活动行/双栏/行号 全量 script 剥离最后用 CSSOM 运行时按选择器命中内容区裁剪样式实现零双份维护。文中还复盘了一个 Chrome 正常、ArkWeb 导出零样式的装机大坑克隆文档 CSSOM 不可读并沉淀出三条心得语义先定、运行时事实优于静态清单、跨内核必须装机验证。导出 HTML 是编辑器的高频功能MarkPin 的实现翻过两座山先是导出结果和渲染结果不一致导出文件里带着光标行的源码回显、双栏结构、选区高亮、行号栏再是导出文件本身就不对——2.6MB 的文档里面 1.85MB 是编辑器自己的 JS bundle双击打开还会执行。这篇讲完整改造五项 DOM 净化 全量 script 剥离 CSSOM 运行时裁剪最终 2.6MB→753KB-71%以及一个 Chrome 上完全正常、装机后导出零样式的 ArkWeb 大坑。一、问题定义导出的到底应该是文档还是编辑器MarkPin 的导出机制是渲染快照getRenderedHTML对整个 document 做cloneNode(true)内容级格式表格/代码块/KaTeX 公式/引用/图片随快照天然一致——这个架构决定了所见即所得的导出基本免费。但快照也把整个编辑器搬了进去编辑态残留五项光标行的源码回显mk-marker-reveal导出后那一行变成源码形态、双栏结构.mode-pane-right/.mode-divider甚至内容×2、选区高亮层、活动行高亮、行号 gutter体积与行为问题内联构建流程把 JS bundle 和字体全部塞进单文件——1.85MB 脚本 772KB 样式打开导出文件时 bundle 会执行二次初始化不可控。用户拍板方案 A导出只保留预览渲染内容编辑器的一切都不要。二、分析方法先拆体积构成再拆一致性差异体积问题先量化JS 移除后剩余大头是 KaTeX 字体的 base6439 个字体约 748KB——这决定了再怎么裁 CSS体积下限就在字体目标定为 800KB 是有依据的。一致性差异逐项审计五项编辑态残留全部来自快照包含编辑器 DOM 结构这一事实逐项列清单类名级再逐项设计剥离动作比整体重写导出稳得多——后者意味着双份维护。三、解决代码一快照净化 script 全剥离// editor-build/src/main.ts getRenderedHTML真实代码节选constclonedDoc:Documentdocument.cloneNode(true)asDocument;// 1) 活动行回显去 reveal 类恢复渲染形态与其余行统一clonedDoc.querySelectorAll(.mk-marker-reveal).forEach((el:Element){el.classList.remove(mk-marker-reveal);});// 2) 选区层 / 2.1) 光标元素内容区内会命中 CSS 裁剪保留样式静态文档画出竖线clonedDoc.querySelectorAll(.cm-selectionLayer, .cm-cursor).forEach((el:Element)el.remove());// 3) 活动行高亮类clonedDoc.querySelectorAll(.cm-activeLine).forEach((el:Element)el.classList.remove(cm-activeLine));// 4) 双栏结构残留移除镜像栏与分隔条解除 is-dual左栏恢复全宽clonedDoc.querySelectorAll(.mode-pane-right, .mode-divider).forEach((el:Element)el.remove());clonedDoc.querySelectorAll(.mode-dual).forEach((el:Element)el.classList.remove(is-dual));// 5) 行号 gutter编辑器 chrome导出文档无需行号栏clonedDoc.querySelectorAll(.cm-gutters).forEach((el:Element)el.remove());// 6) 移除全部 script纯内容导出快照里的编辑器 bundle 打开文件时会执行// 二次初始化/报错不可控导出文档必须零可执行代码clonedDoc.querySelectorAll(script).forEach((el:Element)el.remove());配套还有导出流程切渲染形态导出前统一setViewMode(preview) 等待渲染稳定finally 恢复原模式成功/取消/失败所有出口——解决源码模式导出变纯源码全文、双栏模式导出变双栏的模式级差异。四、解决代码二CSS 运行时裁剪——按命中内容区保留删了 script、删了编辑器 DOM但完整 CSS772KB还在。手工维护一份导出专用 CSS 清单必然腐烂我们的方案是运行时按选择器命中过滤// editor-build/src/main.ts getRenderedHTML真实代码节选// 【装机实证修正】遍历基于【原始 document.styleSheets】——ArkWeb 上克隆文档//cloneNode 产物的 styleSheets 为空/不可读首版遍历 clonedDoc.styleSheets// 导致裁剪结果为空、导出 HTML 无任何样式装机导出没有渲染效果根因。// 原文档 CSSOM 必然可用编辑器自身渲染正常。选择器命中判断用克隆内容区。constcontentEl:Element|nullclonedDoc.querySelector(.cm-content);constcollect(ruleList:CSSRuleList):string[]{constout:string[][];for(letri0;riruleList.length;ri){construle:CSSRuleruleList[ri];if(rule.typeCSSRule.STYLE_RULE){constsr:CSSStyleRuleruleasCSSStyleRule;letkeepfalse;if(sr.selectorText.indexOf(:root)0||sr.selectorText.indexOf(body)0||sr.selectorText.indexOf(*)0){keeptrue;// 变量定义/全局排版/通配基础保留}else{// 剥离伪元素/伪类::before、:hover 等后按选择器命中内容区判断constplain:stringsr.selectorText.replace(/::?[\w-](?:\([^)]*\))?/g,).trim();if(plain.length0){keepcontentEl.matches(plain)||contentEl.querySelector(plain)!null;}}if(keep){out.push(sr.cssText);}}elseif(rule.typeCSSRule.MEDIA_RULE){// media 递归裁剪print 等媒体块内规则同样按内容区过滤constinner:string[]collect((ruleasCSSMediaRule).cssRules);if(inner.length0){out.push(media (ruleasCSSMediaRule).conditionText {inner.join( )});}}// font-face/keyframes 整类保留动画与字体是渲染刚需}returnout;};效果mk-*渲染类、排版、KaTeX 样式因为命中.cm-content自动保留gutter/tooltip/cursor/mode 布局这些编辑器 chrome 类不命中自动剔除。零双份维护——源码样式演化时导出子集自动同步因为裁剪依据是命中这个事实不是手工清单。防御性兜底裁剪结果为空时降级保留原样式全文渲染优先宁可大不可裸。五、装机翻车Chrome 正常ArkWeb 导出零样式首版上线装机就翻车Chrome 里导出渲染完美模拟器里导出 HTML 打开完全无样式。环境差异定位到样式表来源——首版遍历clonedDoc.styleSheets克隆文档的 CSSOMArkWeb 上 cloneNode 产物的 styleSheets 为空/不可读裁剪结果自然为空。修复就是上面代码注释里那句改遍历原始document.styleSheets选择器命中判断仍用克隆内容区结构一致 空结果降级兜底。这是Chrome 能跑 ≠ 装机能跑的典型案例我们后续把这类环境差异沉淀成了固定的取证方法论后篇展开。同批还补了 PDF 链路导出 PDF 前同样切 preview 模式print 样式补行号隐藏。六、验证与效果Chrome导出 753KB2.6MB→-71%余量大头为 KaTeX 字体 base64独立打开 DOM 断言 14 行内容全渲染标题/任务框/引用/代码卡片/表格/KaTeX/加粗无光标/选区/行号/双栏结构像素验证复选框 16×16、引用竖线、代码表格背景。装机模拟器三项验证导出 HTML 渲染/PDF 纯净/导出后视图恢复全部通过用户确认闭环。七、能力边界表事项AI 表现我的结论导出渲染快照架构内容级一致性天然免费快照机制的代价是什么都带走净化要做减法而非重写CSSOM 运行时裁剪命中驱动方案一次成型零双份维护用选择器命中这种事实依据代替手工清单是防腐烂的正解ArkWeb 克隆文档 CSSOM 不可读首版未预判装机暴露环境差异无法穷举预判只能装机验证快速修正script 全剥离一刀切干净零可执行代码导出的语义是文档携带执行逻辑是语义错误不只是体积问题降级兜底空结果保留原文渲染优先宁可大不可裸裁剪类功能的失败模式要设计成退回原点而非输出空八、三条心得导出的产品语义先定技术方案跟着清晰导出文档而非编辑器这一句拍板script 剥离、chrome 剔除、模式切换全部顺理成章运行时事实优于静态清单CSS 裁剪靠选择器命中而不是维护导出样式表——清单必腐事实不会跨内核功能必须装机验证cloneNode 的 CSSOM 行为在 Chrome 与 ArkWeb 上不同这类差异没有文档只有装机能告诉你。如果你也在试 AI 开发鸿蒙或者想看后续关注专栏所有踩坑都会持续更新。