鸿蒙 PC Markdown 编辑器性能工程:中文输入与 10MiB 文档
本文讨论中文 IME 正确性、固定语料、加载时间、完整进程组 PSS 和大文件功能降级。完整示例代码:https://gitcode.com/VON-/codex_md_oh。
编辑器性能要同时看时间、内存与正确性
鸿蒙 PC Markdown 编辑器的性能不能只用"打开很快"来判断。大文档可能在加载时很快,但在撤销、预览或 IME 组合输入时消耗大量内存。因此当前测试同时覆盖:
- 1MiB 与 10MiB UTF-8 Markdown 文档的读取和编辑器加载时间。
- 主进程、ArkWeb 活跃进程和 ArkWeb 辅助进程的完整进程组 PSS。
- 中文输入、撤销、重做、保存快照与修改状态。
- 大文档连续撤销时是否出现异常退出或 LowMemoryKill。
5MiB 阈值后主动降级
当文档字符数达到 5×1024×1024,编辑器切换到大文件模式:
ts
const LARGE_DOCUMENT_CHARACTER_THRESHOLD = 5 * 1024 * 1024;
function updateLargeDocumentMode(documentLength: number): void {
largeDocumentMode = documentLength >= LARGE_DOCUMENT_CHARACTER_THRESHOLD;
if (largeDocumentMode && currentMode !== 'source') {
currentMode = 'source';
workspace.dataset.mode = currentMode;
previewDirty = true;
}
}
function createEditorState(content: string): EditorState {
const useLargeDocumentSetup = content.length >= LARGE_DOCUMENT_CHARACTER_THRESHOLD;
const lineSeparator = content.includes('\r\n') ? '\r\n' : '\n';
return EditorState.create({
doc: content,
extensions: [
useLargeDocumentSetup ? minimalSetup : basicSetup,
useLargeDocumentSetup ? [] : markdown({ base: markdownLanguage }),
EditorState.lineSeparator.of(lineSeparator),
placeholder('Start writing Markdown...'),
useLargeDocumentSetup ? [] : EditorView.lineWrapping,
EditorView.updateListener.of((update) => {
if (!update.docChanged) {
return;
}
documentRevision += 1;
pendingDirty = forcedDirty || !update.state.doc.eq(baselineDocument);
updateLargeDocumentMode(update.state.doc.length);
if (currentMode === 'source') {
previewDirty = true;
} else {
renderPreview(update.state.sliceDoc());
}
notifyNative(update.state.doc.length);
scheduleRecoverySnapshot();
}),
EditorView.theme({
'&': { height: '100%' },
'.cm-scroller': { overflow: 'auto' },
'.cm-content': { minHeight: '100%' }
})
]
});
}
代码来源:web-editor/src/main.ts
降级后使用 minimalSetup,关闭 Markdown 语法扩展、换行和预览模式。这是有意的产品取舍:大文档首先保证能打开、能编辑和能保存,而不是在极限场景中继续运行所有高成本功能。
Release 模拟器实测数据
| 指标 | 目标 | 实测 | 结论 |
|---|---|---|---|
| 1MiB 读取 | 计入 1 秒总预算 | 48ms | 通过 |
| 1MiB 编辑器加载 | 计入 1 秒总预算 | 35ms | 通过 |
| 1MiB 总加载 | ≤ 1 秒 | 83ms | 通过 |
| 10MiB 读取 | 计入 3 秒总预算 | 158ms | 通过 |
| 10MiB 编辑器加载 | 计入 3 秒总预算 | 187ms | 通过 |
| 10MiB 总加载 | ≤ 3 秒 | 345ms | 通过 |
| 1MiB 完整进程组 PSS | < 250MiB | 324.7MiB | 距目标高 29.9% |
| 10MiB 完整进程组 PSS | 建立技术基线 | 396.4MiB | 已记录 |
内存数据不应被描述为"全部通过"。1MiB 的 324.7MiB 高于 250MiB 正式目标,只是满足预先定义的"距目标不超过 30% 且有优化路径"技术验证容差。
鸿蒙 PC 大文件模式截图
下图为 10MiB Markdown 文档在鸿蒙 PC / 2in1 模拟器中加载后的画面。右上角 Split 和 Preview 不可用,状态栏显示 Large file mode。

当前结论
模拟器中,"鸿蒙输入测试"可以输入,撤销后为空,重做后恢复,字数计数为 6。修复前,10MiB 文档连续撤销曾触发一次 LowMemoryKill;修复后相同路径未产生新的异常退出。
这些是可以支持架构继续前进的模拟器证据,但不替代鸿蒙 PC 真机上的多次 Release P95、物理键盘长按、系统快捷键冲突和触控板滚动测试。
中文输入首先是正确性问题
英文键盘事件通常是一次按键对应一次可见字符,中文输入法不是。用户输入拼音时,浏览器会经历 composition start、update、end,候选文本可能多次变化,最终才提交到文档。若编辑器或快捷键处理器把组合过程当成普通按键,就会出现重复字符、候选窗提前关闭、撤销粒度异常或拼音字母被直接写入正文。
因此 IME 验证不能只看最后是否出现汉字,还要检查完整状态:
- 组合期间候选文本不触发保存、命令面板等快捷键。
- 候选提交后正文只出现一次目标字符。
- 一次撤销的粒度符合用户预期,不把组合过程拆成多个残片。
- 重做能够恢复完全相同的 Unicode 文本。
- 修改标记、字数和恢复快照以提交后的文档为准。
- 中文后继续输入标点、英文和换行,光标位置保持正确。
CodeMirror 已经封装了浏览器编辑事件的大量细节,但 ArkWeb 版本、鸿蒙系统 IME 和物理键盘仍会影响实际行为。浏览器自动化可以验证插入与撤销状态,不能完全模拟系统候选窗;真机需要覆盖全拼、双拼、中文标点、长句候选、退格修改和中英文切换。
正确性指标和性能指标不能互相替代
编辑器在 30ms 内显示了错误字符,性能仍然是不合格;输入完全正确但每次按键卡顿 500ms,同样不可用。测试报告应把两类结果分开。
正确性关注内容、选区、撤销、保存和格式。性能关注加载时间、输入延迟、滚动、内存和长时间稳定性。二者的交叉点是降级策略:关闭语法高亮可以改善性能,但不能破坏正文;暂停实时预览可以减少工作量,但必须明确显示当前处于 Source 模式。
性能优化必须守住文档正确性回归。任何改变事务分组、Bridge 节流或大文档扩展的提交,都应重新执行中文输入、撤销重做和保存快照用例,而不能只比较启动时间。
固定语料才能比较版本
"打开一个大文件"无法复现。测试语料至少要固定字节大小、字符结构、最长行、标题密度、代码块比例、行尾和编码。两个同为 10MiB 的文件,性能可能完全不同:一份有十万行短文本,另一份只有一个超长行;软换行、语法解析和 DOM 节点数量会产生不同瓶颈。
建议至少维护以下夹具:
- 1MiB 常规 Markdown:中英文段落、标题、列表、链接和代码块混合。
- 10MiB 多行文档:用于大文件加载、滚动、撤销和内存基线。
- 10MiB 超长行文档:专门观察换行布局和横向滚动。
- 大量标题文档:观察 Markdown 语法树和未来大纲索引成本。
- 中文密集文档:比较字节数、UTF-16 长度、字数统计和 IME。
- CRLF/BOM 文档:确认性能路径没有绕过格式兼容。
夹具生成规则、SHA-256 和实际大小应进入报告。每轮测试使用同一文件,才能判断 345ms 到 420ms 是代码回归还是语料变化。
加载时间要拆分阶段
总加载时间可以直接描述用户等待,但只记录总数很难定位瓶颈。当前测量把文件读取和编辑器加载分开:ArkTS 从授权 URI 分块读取并严格解码,随后 Bridge 把正文设置到 CodeMirror,页面创建 EditorState 和 EditorView。
进一步可以拆成:
text
用户确认文件
→ URI open/stat
→ 分块 read + UTF-8 decode
→ 格式检测
→ Bridge 传输
→ EditorState 创建
→ 首次可编辑帧
→ 首次预览完成
"首次可编辑"和"预览完成"不必是同一个时间点。对于较大文档,可以先呈现 Source 并接受输入,再延迟生成预览。测量点必须使用单调时钟,避免系统时间调整;多次运行要区分冷启动、热启动和文件缓存。
正式门槛应采用 P95,而不是挑最快一次。至少记录样本数、中位数、P95、最大值和是否存在首次运行异常。模拟器宿主机负载会显著影响数据,所以模拟器适合发现数量级问题,正式结论仍应来自目标鸿蒙 PC。
内存要看完整进程组
混合应用会同时存在 ArkTS 主进程、ArkWeb 活跃渲染进程和辅助进程。如果只读取主进程 PSS,CodeMirror 文档、语法树、预览 DOM 和 JavaScript 堆都可能被遗漏。完整进程组 PSS 更接近应用实际占用。
PSS 仍不是唯一指标。测试时还要关注:
- 打开文档前的稳定基线。
- 打开 1MiB、10MiB 后的稳定值和峰值。
- 连续撤销、重做、切换模式后的增量。
- 关闭文档或重载后内存能否回落。
- 是否出现 LowMemoryKill、ArkWeb 重启或明显交换。
- 多次打开文档是否持续增长,提示监听器或旧 EditorView 未释放。
当前 1MiB 完整进程组 PSS 为 324.7MiB,高于 250MiB 目标。正确的工程结论是"加载性能通过,内存仍需优化",而不是用快速加载掩盖内存。优化时应先测各组件增量:关闭预览、语法高亮、行换行或重建历史后分别比较,找到真正的大项。
5MiB 阈值是保护机制而不是魔法数字
达到阈值后切换 minimalSetup,一次性减少多个高成本能力。这种做法稳定,但阈值本身需要真实设备数据校准。字符长度与文件字节不完全对应,结构复杂度也会影响解析和布局,因此未来可以从单阈值演进为基于多信号的策略:
- 文件字节和字符长度控制读取与 Bridge 风险。
- 行数、最大行长控制换行和滚动风险。
- 标题、代码块等结构密度控制语法解析与预览风险。
- 当前设备可用内存和 ArkWeb 状态决定是否延迟高成本功能。
策略必须可解释。界面显示 Large file mode,Split 和 Preview 保持位置但不可用,用户仍可以编辑、保存和关闭。不要自动截断文档,也不要只渲染一部分却让保存覆盖完整文件。
输入延迟如何采样
加载快不代表编辑快。大文档输入测试可以在固定位置插入单字符、中文短句和大段粘贴,测量从事务发出到下一次绘制完成的时间。需要分别观察普通输入、语法解析、预览刷新、字数计算、Bridge 消息和恢复快照。
优化优先级通常是先移除与每次按键成全文复杂度的工作:
- 不在每次变更跨 Bridge 传全文。
- 大文档不扫描全文计算字数。
- Preview 只在可见时更新,Source 模式标记为待刷新。
- 恢复快照节流,并对超大文档使用不同策略。
- 语法解析和软换行在保护模式关闭。
然后再处理更细的渲染和样式。若一个状态栏动画很顺滑,却仍在每次输入复制 10MiB 字符串,优化方向就是错误的。
撤销重做是典型压力路径
大文档连续撤销可能触发比普通输入更高的峰值,因为历史中保存了变化结构,视图还要重新布局。测试不能只按一次 Ctrl+Z,应构造多次编辑、批量粘贴、连续撤销到基线、再全部重做,并观察内容、脏状态和内存。
修复异常退出时,不能简单关闭撤销来通过性能测试。撤销是桌面编辑器核心能力,合理方案是控制历史规模、减少重复全文快照、避免文档切换历史串联,并对极端场景给出可预测限制。任何限制都要在产品状态或文档中明确,而不是静默丢弃用户历史。
真机验证矩阵
鸿蒙 PC 真机至少应覆盖以下组合:
| 维度 | 代表场景 |
|---|---|
| 包类型 | Release 候选包,记录 SHA-256 |
| 窗口 | 最大化、左右分屏、连续缩放 |
| 输入 | 系统中文 IME、英文、emoji、长句候选 |
| 键盘 | 内置键盘、外接键盘、长按与快捷键 |
| 指针 | 触控板滚动、选择、拖拽、触控点击 |
| 文档 | 1MiB、10MiB、多行、超长行、CRLF |
| 操作 | 打开、输入、粘贴、撤销重做、保存、重开 |
| 稳定性 | 30 分钟编辑、多次切换、前后台与低内存 |
每项都要记录设备型号、系统版本、HAP 哈希和样本数。真机测试不需要把每个组合无限展开,但必须覆盖最可能暴露 ArkWeb、IME 和内存差异的代表场景。
性能验收清单
- 指标同时覆盖正确性、时间、内存和稳定性。
- 测试语料固定并记录结构、大小和摘要。
- 加载时间拆分读取、Bridge、EditorState 和首次可编辑帧。
- 使用 Release 包多次采样,报告 P50/P95 而不是最优值。
- 内存统计包含完整 ArkWeb 进程组,并观察峰值和回落。
- 中文 IME 覆盖组合、候选、撤销、重做和保存。
- 大文件降级保留正文完整性与基本编辑能力。
- 任何性能优化都重新运行内容无损和保存回归。
- 模拟器用于开发诊断,正式门槛由目标鸿蒙 PC 真机复核。
性能工程的最终目标不是追求一个漂亮数字,而是确保用户在不同大小文档、输入方式和窗口状态下都能预测应用行为。对混合架构而言,主动减少全文工作、明确降级边界、分层测量和完整进程组数据,比单独优化某个动画更能决定产品上限。
先建立性能预算再谈优化
性能预算把总体目标分配给各阶段,避免每个模块都认为自己的耗时"只有一点"。例如 1MiB 文档首次可编辑目标为一秒,可以为选择器返回后的读取解码、Bridge 传输、EditorState 创建和首帧分别设置观测值;不必机械平均,但任何阶段持续占据大部分预算都应被定位。
内存也需要预算。基线 ArkUI/ArkWeb 进程、空文档、1MiB 文档、语法树、预览 DOM、撤销历史和恢复快照分别测增量。多标签设计时,用单会话增量估算上限,超过目标就先选择冻结或复用,不能等做完十个标签后才发现架构不可承受。
预算不是一次定死。目标设备、ArkWeb 版本和功能变化后可以调整,但调整要记录原因与用户影响,不能为了让当前数字变绿而移动门槛。
用实验矩阵定位真正成本
大文档模式一次关闭多个功能,适合产品保护,却不利于定位。性能分析版本可以逐项对比:basicSetup 与 minimalSetup、Markdown 语法扩展开关、软换行开关、预览开关、字数统计开关、恢复快照开关。每次只改变一个变量,用同一 HAP 配置、语料和操作重复测量。
结果应分别记录加载、输入、内存和正确性。某项扩展可能增加 30ms 加载却明显改善编辑体验,不一定应删除;另一项每次输入都扫描全文,即使首屏影响小也应优先改造。实验目的不是证明某个库"慢",而是找到成本随文档规模增长的函数。
分析完成后,内部开关不应散落在正式产品中。保留清晰的大文件策略,调试实验通过构建配置或测试入口管理。
最长行是容易遗漏的性能变量
编辑器虚拟化通常按可视行工作,但一个数百 KB 的单行文本会让语法解析、选区、换行和水平滚动承受不同压力。日志、压缩 JSON、Base64 或生成代码都可能形成超长行,即使总文件只有 1MiB。
开启软换行时,浏览器需要计算大量视觉行;关闭时,超宽内容又可能影响滚动与绘制。测试应在行首、中间和末尾点击、选择、输入、撤销,并观察滚动条与光标。大文件策略可以在最大行长超过阈值时单独关闭换行,不必等总字符达到 5MiB。
Markdown 代码块中的超长行也应保持原文,不能为了性能自动插入换行字符。视觉换行和文档换行必须严格区分。
粘贴和替换属于峰值操作
单字符输入代表稳定延迟,大段粘贴和全局替换代表瞬时峰值。一次粘贴 5MiB 可能同时触发事务、语法更新、预览、字数、Bridge 和恢复任务,内存短时间出现多份字符串。
处理策略是先让 CodeMirror 完成一个原子事务,再根据新文档规模决定是否进入大文件模式;预览和统计读取最终状态,不对中间版本重复工作。Bridge 只报告最新摘要,恢复快照合并到下一周期。粘贴后应立即可撤销为一个合理步骤。
全局替换同样要限制匹配数量和预览结果,防止构造海量范围对象。替换前后内容正确性、撤销和保存必须进入压力测试。
滚动性能要区分编辑区和预览区
Source、Split 与 Preview 有不同滚动成本。编辑区由 CodeMirror 虚拟化,预览区可能包含完整 HTML DOM;Split 同时存在两者。测试滚动时记录窗口大小、是否软换行、预览 DOM 规模和触控板输入,不能只看鼠标滚轮一次移动。
同步滚动如果未来实现,会在两个区域间增加位置映射和事件回路。需要防止编辑区滚动触发预览、预览回调又反向触发编辑区。可以使用来源标识和动画帧合并,并在大文档关闭。同步精度不应以每次滚动重新解析全文为代价。
掉帧分析要结合主线程任务,确认是 Markdown 渲染、布局、Bridge 还是 ArkUI 外壳更新。肉眼感觉只是发现问题的入口。
长时间稳定性与内存泄漏
短测试可能看不出监听器、旧 EditorView 或预览节点残留。稳定性脚本可以循环打开小/大文档、切换模式、输入撤销、保存和新建,定期采集完整进程组 PSS。若每轮结束后稳定值持续上升,需要检查视图销毁、定时器、Bridge 代理和恢复队列。
测试还应包含前后台、锁屏恢复和 ArkWeb 进程重建。系统回收后能恢复编辑不代表没有泄漏,频繁回收本身可能由内存过高触发。记录系统退出原因和 ArkWeb 进程变化,避免把自动重建误认为正常稳定。
长测不要求用户文档参与,可使用固定无敏感夹具,并在每轮校验最终哈希与内容。
性能可观测性不能记录正文
埋点只需要阶段时间、文档字节/字符区间、行数区间、是否大文件模式、设备和错误码。不要记录文件名、URI、标题和 Markdown。对超长行可以记录长度区间,不记录内容。
本地开发日志提供细粒度时间点,正式版本只保留必要聚合并允许关闭。采样率和保留期限要明确。性能问题复现时,由用户主动选择专用测试文档,不自动上传真实资料。
这样既能观察版本趋势,又不会让性能优化成为隐私风险。
启动、首次可编辑与完全稳定是三个指标
应用窗口出现不代表用户可以输入,CodeMirror 可输入也不代表预览、字数和文件树已经稳定。测量时分别记录应用首帧、编辑器就绪、文档首次可编辑和后台派生任务完成。优化优先保证用户尽快进入可编辑状态,再把预览和索引延后;不能为了一个更短的"启动"数字显示不可操作的空壳。
冷启动包含进程和 ArkWeb 初始化,热启动可能复用系统缓存。两种数据分别报告,测试前明确是否强制停止应用。恢复会话启动还要单列,因为读取沙箱快照和显示决策对话框属于必要工作,不能与空文档混在一起。
系统负载与温度会影响结论
鸿蒙 PC 在电池、充电、温度和后台任务不同条件下可能调整频率与内存。正式性能采样前让设备达到稳定状态,关闭无关重任务,记录电源模式,同时不要为了数字人为关闭产品正常依赖的系统服务。连续压力测试要观察温升后的 P95,而不是只取冷机首轮。
模拟器更受宿主机 CPU、内存和图形负载影响。它适合比较明显回归,报告不把绝对值推广到真机。真机之间也按设备档位建立基线,不用最高配置结果代表全部目标设备。
性能回归阈值需要统计意识
单次从 345ms 变成 365ms 可能只是噪声。回归判断应基于足够样本和分布,设定绝对目标与相对变化双门槛。例如 P95 超过产品目标直接失败;仍在目标内但连续多个版本上升超过一定比例,则进入调查。内存也比较稳定区间和峰值,不用某一秒读数。
自动化环境保存历史趋势和置信范围,设备异常、测试夹具变化或系统升级在图表上标注。确认回归后用实验矩阵定位,不靠重复运行直到出现一次绿色结果。
优化完成必须回到用户任务
关闭所有扩展当然能降低内存,却可能让编辑器失去价值。每项优化要回到打开、输入、滚动、撤销、预览、保存和恢复这些任务,确认速度提高且功能语义不变。性能改动的验收同时包含基准数字和内容哈希、脏状态、格式、故障路径。
当目标冲突时,优先级是用户数据正确、安全、输入可用、可解释降级,最后才是视觉效果。明确优先级可以避免为追求局部帧率引入静默内容损坏。