有人花了四个月做了一个 Rust 的替代 LSP,叫 Rust Glancer,八月底挂上 Hacker News。它的卖点很集中:内存占用目标 100MB 以内,而且编辑器重启后不需要重新索引。

我平时不太关注"又一个 LSP",但这条值得看,因为它把一个被普遍接受的坏现象——语言服务器吃几个 GB——当成必须解决的问题来设计,而不是当既成事实。

现状:rust-analyzer 的内存账

rust-analyzer 是个好项目,功能覆盖比大多数语言服务器都全,但代价是内存。大一点的 Rust 工作区(几十个 crate、几个工作区成员)跑起来,索引和类型推导的中间结果常驻在几个 GB 的量级。16GB 的机器上同时开编辑器、浏览器、编译任务,往往就不够用了。

这个内存去哪了?主要几块:语法树(每个文件一份,全量常驻)、类型信息和推导结果(跨 crate 的解析、trait 解的候选集)、宏展开产物、以及各种缓存。这些数据的生命周期管理很粗糙——一旦算出来就留着,因为"下次可能还会用到"。

问题在于"下次可能还会用到"这个假设在很多场景下是错的。你在一个文件里改两行,rust-analyzer 为了保持响应,会把相关的类型检查结果全部重算并保留。久而久之,常驻集只增不减。

Glancer 的两个设计取舍

作者明确说了目标是低内存,并且给出了两个具体特性:

第一,内存目标 <100mb。他在自己的老 MacBook Pro M1(2020 款,8GB 内存)上测过,能跑。这个约束不是"尽量省内存",而是硬指标——设计之初就拿它来否决方案。

第二,重启后无需重建索引。如果你的项目已经索引过,重启编辑器不需要从头再来。这一条看起来是体验优化,其实是内存设计的一部分:要实现它,索引必须能被序列化到磁盘,并且能以增量方式加载回来,而不是在内存里重新长一遍。

为了实现这两点,能用的手段基本是固定的:字符串驻留(把重复的标识符、路径、类型名映射到整数 ID)、arena 分配(把大量小对象放在连续内存里,减少分配器开销和指针跳转)、惰性解析(只解析当前需要的文件,其余保持未展开状态)、以及缓存驱逐策略(该扔的扔掉,别抱着不放)。

跟 rust-analyzer 的取舍差异

如果只做到"省内存",代价一定是功能覆盖。作者很诚实地列了 caveat:宏展开、泛型推导这些复杂场景的支持不完整,某些代码的推断结果可能跟 rust-analyzer 不一致。

这不奇怪。rust-analyzer 之所以吃内存,很大一部分是在为完整性买单——它要把宏展开、trait 解析、类型推导都算到底,才能保证"跳转、补全、错误提示"在所有场景下都对。Glancer 选择了"在常见场景下够用,在复杂场景下可能失灵",把省下来的内存换成了覆盖度的损失。

作者说大概一个半月前开始把它当日常主力,替代 rust-analyzer。这个信号比任何 benchmark 都有说服力——如果你自己天天在真实项目上用,说明"常见场景够用"这个判断是站得住的。

LSP 内存问题的通用解法

抛开 Rust 这一具体场景,语言服务器的内存问题有通用解法,Glancer 的选择基本都落在这些套路里:

  • 惰性化。不要一启动就全量解析。按需解析能省掉大量"打开了但没碰过"的文件的开销。
  • 驻留化。任何重复出现的字符串都应该只存一份,其余存 ID。路径名、类型名、导入名,重复率极高,驻留的收益立竿见影。
  • 序列化缓存。把索引结构持久化到磁盘,重启时 mmap 回来,而不是重算。这一条对"重启要等 30 秒索引"的痛点特别有效。
  • 有界缓存。缓存必须有容量上限和淘汰策略。无界缓存看起来省事,实际是内存泄漏的温床。
  • 分代收集。区分"刚算出来的、很可能还会用"和"算了很久没人碰"的数据,后者优先扔。

这几条不是 Rust 独有的,也不只适用于 LSP。任何做长期驻留的开发者工具(索引器、代码分析器、IDE 后端)都会遇到同一个问题:怎么在"算得快"和"占得少"之间划一条线。

我的做法

如果你在写一个会长期驻留的开发者工具,我的建议是先定内存上限,再设计数据结构:

  • 一开始就写死一个上限(比如"整个进程不超过 200MB"),拿它当验收标准。没有上限,数据结构会自然膨胀到吃满机器。
  • 所有跨文件共享的字符串一律驻留。这一条通常能砍掉 20% 以上的内存。
  • 缓存一定带上限和淘汰。写缓存的时候顺手写淘汰,别等到内存爆了再补。
  • 把"重建索引"当成可以优化掉的成本,而不是必然接受的启动开销。能持久化就持久化。

Glancer 的意义不在于它比 rust-analyzer 好——它在功能上明确不如。它的意义是证明了一件事:"语言服务器必须吃几个 GB"不是天道,是设计选择。有人选了另一条路,并且在真实项目上跑得下去。