为什么我们用 CodeMirror 6 而不是 Monaco
为什么我们用 CodeMirror 6 而不是 Monaco
在 代码格式化工具 和 Markdown 转 HTML 里,我们需要一个浏览器内的代码编辑器:带行号、语法高亮、Cmd-F 查找、撤销栈。摆在面前的就两个主流选项:
- Monaco:VS Code 的同款编辑器,微软维护
- CodeMirror 6:Marijn Haverbeke 重写的第三代版本,2021 年稳定发布
直觉上 Monaco 是"显然正确"的选择——它驱动着全世界最流行的代码编辑器。但深入对比后我们选了 CodeMirror 6。本文记录这次选型的真实考量。
大小:差距比想象大
bundle 体积是这次选型最直接的差距:
| 编辑器 | 最小 bundle(含 1 种语言) | 含 5 种语言 |
|---|---|---|
| Monaco | ~2.5MB (gzipped ~700KB) | ~3MB (gzipped ~850KB) |
| CodeMirror 6 | ~150KB (gzipped ~45KB) | ~280KB (gzipped ~80KB) |
10 倍以上的差距。对工具站来说,这不只是性能问题——是首屏可用性问题:
- Monaco 默认要 ~2.5MB 初始下载。3G 网络(中国三四线、东南亚常见)实测 8-15 秒
- CodeMirror 6 的 150KB 在同样网络下 < 1 秒
我们的工具页平均 PV 停留时间是 90 秒。如果 12 秒花在加载编辑器上,用户大概率已经关闭页面。CF Web Analytics 的 bounce rate 数据印证了这一点——首屏 LCP 每超 1 秒,跳出率上升 7-12%。
架构哲学:单体 vs 可组合
Monaco 是 大教堂——它把语法高亮、autocomplete、minimap、folding、search、diff view 等全部打包。你引入 Monaco 等于引入了一个完整的 IDE 内核。
CodeMirror 6 是 集市——核心包 @codemirror/state 只管文档模型(< 30KB),其它一切都是独立包:
import { EditorState } from "@codemirror/state";
import { EditorView, keymap, lineNumbers } from "@codemirror/view";
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import { javascript } from "@codemirror/lang-javascript";
import { oneDark } from "@codemirror/theme-one-dark";
const view = new EditorView({
state: EditorState.create({
doc: "console.log('hello')",
extensions: [
lineNumbers(),
history(),
keymap.of([...defaultKeymap, ...historyKeymap]),
javascript(),
oneDark,
],
}),
parent: document.body,
});
每个 extension 都是独立 npm 包,按需引入。不需要 minimap?不装。不需要 folding?不装。最终 bundle 只包含你真正用的功能。
这种设计的最大好处不是体积——是心智清晰。当 Monaco 的某个行为不符合需求,你要去翻几万行 TypeScript 找钩子;CodeMirror 的某个 extension 不喜欢,直接换一个或自己写——extension API 是稳定的公共接口。
移动端
Monaco 在 2019 年明确表态 不打算良好支持移动浏览器——VS Code Web 在移动端也是"能用但不舒服"。具体问题:
- iOS Safari 的虚拟键盘弹起后 viewport 改变,Monaco 的鼠标坐标计算错位
- 触摸滚动惯性在 Monaco 的 overflow 容器里行为不一致
- 移动端选中文字时的放大镜与 Monaco 内置选区高亮冲突
CodeMirror 6 从设计之初就考虑移动端。我们的工具页移动端流量占 35%(CF Web Analytics 数据),这个差距不可忽视。
语言支持的取舍
Monaco 的杀手锏是 TypeScript / JavaScript 的智能感知——它内嵌了完整的 TypeScript 语言服务,能给出和 VS Code 几乎一样的 hover、jump-to-definition、自动补全。如果你的产品是 TS playground / 在线 IDE,Monaco 几乎没有竞争对手。
CodeMirror 6 的语法高亮基于 Lezer parser——比 Monaco 用的 TextMate grammar 更快、更精确,但没有语义分析。它能告诉你"这是个 identifier",但不能告诉你"这个 identifier 是 string 类型"。
实战取舍:
| 需求 | 推荐 |
|---|---|
| 在线 IDE / TS playground | Monaco |
| 代码格式化工具(输入/输出框) | CodeMirror 6 |
| Markdown 编辑器 | CodeMirror 6 |
| 笔记应用 / 富文本编辑器 | CodeMirror 6(或 ProseMirror) |
| Jupyter-like 单元格 | CodeMirror 6(JupyterLab 就用它) |
| Repl / 评估器 | 看是否需要 hover 类型提示 |
共享代码的优势
我们的工具站里多个工具都用到代码编辑器:
- json-formatter — JSON 编辑 + 树形视图(用 vanilla-jsoneditor,内部基于 CodeMirror)
- code-formatter — 多语言 prettier
- markdown-to-html — Markdown 输入
- yaml-json / yaml-toml — YAML / TOML 编辑
- mermaid-editor — Mermaid 源码
- text-diff — 两侧文本对比
- curl-converter — cURL 命令 + 目标代码
Next.js 的 webpack/turbopack 会自动 dedupe @codemirror/* 子依赖——8 个工具页共享同一份 CodeMirror chunk(约 200KB),平均到每个工具页边际成本只有 30-50KB(各语言扩展)。
如果换成 Monaco,每个用到编辑器的工具页都被钉上 2.5MB 的 Monaco baseline。整站打包能涨 5-10MB。
集成成本:Next.js / SSR 友好度
CodeMirror 6 的关键 API(EditorView、document 访问)需要在浏览器环境运行。Next.js App Router 下我们用 next/dynamic({ ssr: false }) 隔离:
const CodeArea = dynamic(() => import("@/components/tools/CodeArea"), {
ssr: false,
loading: () => <div>正在加载编辑器…</div>,
});
Monaco 也支持 SSR-disabled 加载,但它依赖 Web Worker 做语言服务。worker 需要单独的 chunk URL 路径配置,且 worker 内部还会动态加载更多语言文件——在静态导出(output: 'export')下这是个噩梦:
// 静态导出场景下你要这样手动配 worker URL
self.MonacoEnvironment = {
getWorkerUrl(_, label) {
if (label === "typescript") return "/_next/static/chunks/ts.worker.js";
if (label === "json") return "/_next/static/chunks/json.worker.js";
return "/_next/static/chunks/editor.worker.js";
},
};
而且这些 worker 文件路径在每次 next build 后会变(hash 后缀),需要构建后脚本注入。CodeMirror 6 没有 worker 依赖,零额外配置。
什么时候 Monaco 仍然是更好的选择
不是说 Monaco 永远输——它有 CodeMirror 6 难以追平的场景:
- 完整 IDE 体验:StackBlitz、CodeSandbox、GitHub Codespaces 都用 Monaco。多文件、跳转定义、refactor 是核心功能
- VS Code 风格统一:如果产品定位是"网页版 VS Code"或想给用户 VS Code 熟悉感
- TypeScript 重度场景:实时类型检查 + 错误高亮 + 类型补全
- 企业内部工具:bundle 大小不敏感,要快速上线一个像样的代码编辑器
判断分水岭:用户会不会在这个编辑器里写超过 100 行代码? 是→ Monaco;否→ CodeMirror 6。
总结
我们的最终决策:
| 维度 | Monaco | CodeMirror 6 | 选谁 |
|---|---|---|---|
| Bundle 体积 | 2.5MB+ | 150KB | CM6 ✅ |
| 移动端体验 | 较差 | 良好 | CM6 ✅ |
| TS 智能感知 | 一流 | 无 | Monaco(但我们不需要) |
| 多工具共享 | 重复打包 | dedup 友好 | CM6 ✅ |
| Static export | 复杂 | 零配置 | CM6 ✅ |
| 大型代码项目 | 优秀 | 一般 | Monaco(但我们不需要) |
总结成一句话:Monaco 是「网页版 VS Code」,CodeMirror 6 是「可嵌入的文本编辑器」。前者解决"我要写完整代码项目",后者解决"我要在表单里编辑一段代码"。
我们做的是工具站,每个编辑框是"表单里的代码片段"——CodeMirror 6 是更精准的工具。如果哪天做了一个完整的在线 IDE,那时候再换回 Monaco 也是一行 npm install 的事。
延伸阅读
- 代码格式化工具:8 种语言的 Prettier 实战,编辑器底层就是 CodeMirror 6
- CodeMirror 6 官方文档:extension 设计模式与 transaction 系统
- Monaco vs CodeMirror benchmark(实测对比):性能与功能矩阵