Rust Glancer:冻结索引把 Rust LSP 内存压到 100MB

rust-analyzer 的内存问题长期排在 Rust 工具链抱怨清单的前列。2026 年 8 月 19 日,一位有 7 年 Rust 经验、给 rustc / clippy / rust-analyzer 都提过贡献的开发者 popzxc 公开了自己做了 4 个月的替代方案 Rust Glancer:一个以低内存为第一目标的实验性 Rust LSP 实现,配套 VS Code 扩展已上架 Marketplace。项目上线后同时进入 Hacker News 与 Lobsters 热榜,在 Hacker News 拿到 70 分和 24 条讨论。

Rust Glancer 官方内存占用截图

两个核心承诺

官方博客给出的两个量化目标没有任何修饰:

  • 常规项目的内存占用目标控制在 100MB 以下(存在前提条件,后文展开);
  • 索引结果落盘,编辑器重启后无需重新索引,几乎立即恢复可用状态。

作者的动机来自自己的工作流:两台显示器各开一个相同的 IDE,workspace 里塞了一堆项目。这种配置下 rust-analyzer 的内存消耗约为单份的两倍,最后一组项目直接吃掉 16GB 内存,而且每次打开 VS Code 都会触发大量并行索引任务让风扇全速运转。官方博客附带的截图显示,Rust Glancer 两个引擎进程分别只占 79.7MB 和 12.5MB。

GitHub 仓库卡片

索引速度方面,官方在同一组机器上做了对比:

机器LSP基础索引(引擎可用)完整索引
MacBook Pro M4 Max 36GB(2025)Rust Glancer5 秒8 秒
MacBook Pro M4 Max 36GB(2025)rust-analyzer6 秒13 秒
MacBook Pro M1 8GB(2020)Rust Glancer6 秒9 秒
MacBook Pro M1 8GB(2020)rust-analyzer7 秒14 秒

作者特别提到在 8GB 内存的 M1 MacBook Pro 上实测体验良好,这也是该项目最直接的适用场景:老机器、低配机器,或者同时开多个 IDE 的重度工作流。

rust-analyzer 为什么吃内存

官方博客把 rust-analyzer 的内存开销归为三层原因:

  1. Rust workspace 本身信息量巨大:成千上万的函数、结构体、trait 及其引用关系、函数体和语句,想要支持「查找全部引用」这类功能就没有捷径,所有信息都得分析并记住。
  2. rust-analyzer 使用 salsa 作为数据库。salsa 是增量式查询驱动的设计,惰性计算所有需要的数据,无需显式记录一切。这个设计很优雅,但它天然与内存绑定,很难把数据挪到别处。
  3. rust-analyzer 使用 rowan 表示语法树,支持部分失效(文件只改了一部分时只重 parse 相关片段)。代价是树状结构容易产生严重的内存碎片:从操作系统拿走的内存远高于「实际使用」的内存。

前两点是架构选择的结果。rust-analyzer 用这些设计换取了更快的响应速度,而且确实做到了。Rust Glancer 的切入点是反着来。

冻结式索引:整个架构的赌注

Rust Glancer 的核心设计可以概括为一句话:放弃增量式分析,改用「冻结快照 + 保存时失效」的模型。

它对 workspace 做一次完整索引,把结果写入文件系统。之后的每次查询按需加载相关数据,查询结束后立即释放。文件保存(或检测到外部文件变更)会生成新一代项目快照,触发局部重建。输入过程中的每次按键都不会产生新的项目状态。

这个选择带来两个直接收益:

  • 分析结果可以卸载(offload)到文件系统,只在查询期间加载进内存;
  • 已落盘的分析结果在编辑器重启后直接复用,这就是「秒级恢复」的来源。

代价同样明确。冻结式分析在定义上就比惰性增量慢,因为从文件系统加载并反序列化数据慢于内存读取。缓解手段是打字时对当前函数体做浅层分析,同时复用上一次的完整索引。这带来了最需要注意的使用边界:新 import、新结构体、新 trait 要到保存文档之后才会进入索引。作者表示这个习惯几天就能适应,愿意接受的人可以试试。

针对 agent 工作流(AI 编码助手直接改文件)还有一个专门优化:Rust Glancer 实现了自定义文件监视器,并对编辑器外的变更降低处理优先级,避免 agent 改代码引发高频重索引或 inlay hint 错位。作者在 rust-analyzer 上观察到的 inlay hint 错位问题,在 Rust Glancer 中已通过这条路径解决。

工程细节:内存是怎么省下来的

项目文档的 Memory approach 一章详细记录了优化手段,这套方法论对任何内存敏感的 Rust 项目都有参考价值。

按分配生命周期分层。 常规做法是对每个 package 依次执行「item tree → def-map → semantic → body」全流程。问题在于不同生命周期的分配会交错:临时数据释放后留下空洞,后续 package 的分配填进这些空洞,最终产生高度碎片化的内存。Rust Glancer 反过来,对所有 package 先执行同一阶段,再收缩压缩,进入下一阶段。解析语法树时避免并行 item tree 降级,让语法驱逐释放出连续大块内存。

压缩分配(compacting)。 把一组中间 Builder 对象冻结成最终对象时,逐个「缩小旧的 + 分配新的」会让新对象钻进刚释放的缝隙。正确做法是先统一缩小全部旧对象,在旧对象仍存活时一次性分配整块新向量,然后再丢弃旧对象,最后配合 purge 把完整的大块内存还给操作系统。

jemalloc 与主动 purge。 项目使用 jemalloc 分配器,借助 jemalloc-ctl 在每轮大批量分配结束后主动归还内存。LSP 是人驱动的负载,查询时间线以秒计,多几个 syscall 换取低 RSS 完全划算。

引擎子进程隔离。 每个 engine 是独立子进程,通过 tarpc 与 LSP 服务器通信(服务器可向 engine 发查询和通知,engine 只能回发通知)。隔离的动机依旧是防碎片:如果多个 engine 共处一个进程,新 engine 启动时另一个 engine 可能正在索引,随机交织的分配会让内存布局失控。

基准数据。 项目用 rust-analyzer 自身代码库做测试 fixture。文档给出的 checkpoint 剖析表显示,完整索引过程中 jemalloc active 内存峰值约 613MB,package 缓存写入阶段 resident 达到约 911MB,但 package 数据卸载完成后 active 降到 36MB,整个 project 阶段结束时 allocated 仅剩 7.3MB。也就是说,峰值出现在索引过程中,索引完成落盘后引擎可以以极小的常驻内存继续服务查询。剖析通过 just analyze 命令产出,支持 --profile--package-residency all-offloadable-m 等参数,CI 中持续运行基准防止回归。

能力边界

作者对项目定位非常克制。README 明确写着「目标是 90% 完成,足以支撑日常工作,但没有打算做 rust-analyzer 2.0」。当前状态:

  • 已支持:完整索引管线、类型推断、trait 求解(集成 Chalk)、声明式宏展开(复用 rust-analyzer 基础设施)、goto definition、hover、inlay hints、补全等常规 LSP 操作;
  • 边界一:跨文件操作(查找引用、重命名)更严格,引擎会检查所有打开的 Rust 文档,只要有一份与已保存项目不一致,就会提示先保存,而不是返回可能已经失效的位置;
  • 边界二:需要执行不可信代码的能力(build script、proc macro 实际调用)不会支持,作者有一个不依赖真实执行的替代设想,但距离落地还远;
  • 边界三:nightly 特性支持被推迟到项目在 stable Rust 上成熟之后。

作者的预期是两者长期共存:追求完整性和逐键准确性的项目继续用 rust-analyzer,机器较弱或愿意牺牲部分功能换内存的用户选 Rust Glancer。

关于开发方式

项目重度使用 LLM 辅助开发,作者专门用一节说明这不是 vibe coding:每个 pull request 都经过人工审查,git 历史里有一次 10k+ 行的 diff,但这类大 PR 之间间隔数天,与「每天都在开发」的实际节奏对得上。作者在 rustc、clippy、rust-analyzer 都有贡献记录,写这个项目前已经花了几十小时读 rust-analyzer 源码。

作者对开发过程的描述也很坦率:LLM 提出的方案看起来合理,跑起来总觉得哪里不对,回头重新审视设计发现大缺陷,再花时间(有时是一周)修正。这个循环重复了很多次。用他自己的话说,LLM 是工具,代码是他的;如果有人认为这是 slop,那也是「他的 slop」。

上手方式

VS Code 用户直接在 Marketplace 安装 rust-glancer 扩展即可,也可以从仓库自行构建 vsix。项目采用 Apache 2.0 / MIT 双许可,当前 3 位贡献者。后续路线图包括性能与内存进一步优化(重点是索引期碎片问题)、更多 code action(补全 trait 成员、自动 import)以及不依赖真实执行的 proc macro 支持设想。

对 8GB 内存机器的用户、多 IDE 并行的开发者,以及正在被 agent 高频改文件时的索引抖动困扰的团队,这个项目值得跟踪。它在架构层面证明了 LSP 的内存开销存在数量级的优化空间,代价只是大多数人实际用不到的那部分完整性。

来源: