返回文章列表

为什么我们用 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 playgroundMonaco
代码格式化工具(输入/输出框)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(EditorViewdocument 访问)需要在浏览器环境运行。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。

总结

我们的最终决策:

维度MonacoCodeMirror 6选谁
Bundle 体积2.5MB+150KBCM6 ✅
移动端体验较差良好CM6 ✅
TS 智能感知一流Monaco(但我们不需要)
多工具共享重复打包dedup 友好CM6 ✅
Static export复杂零配置CM6 ✅
大型代码项目优秀一般Monaco(但我们不需要)

总结成一句话:Monaco 是「网页版 VS Code」,CodeMirror 6 是「可嵌入的文本编辑器」。前者解决"我要写完整代码项目",后者解决"我要在表单里编辑一段代码"。

我们做的是工具站,每个编辑框是"表单里的代码片段"——CodeMirror 6 是更精准的工具。如果哪天做了一个完整的在线 IDE,那时候再换回 Monaco 也是一行 npm install 的事。

延伸阅读