Modern language servers make a simple promise: they turn your text editor into an IDE. For Rust developers, rust-analyzer delivers on this promise by providing precise auto-completion, type inference, and code navigation. But this comes at a steep cost. Open a medium-sized Rust project, and you will watch your system monitor show the language server consuming gigabytes of RAM. On large codebases or remote development containers, this memory footprint can grind a system to a halt.
Rust Glancer is a project designed to address this issue. It is a lightweight language server that cuts memory overhead by up to 100x. It achieves this not by optimizing the existing compiler-driven architecture, but by replacing it. By focusing strictly on code navigation and symbol indexing, Glancer provides fast definition jumping and reference finding while keeping its memory footprint under 50 megabytes.
The Source of LSP Memory Bloat
To understand how Glancer reduces memory, you have to look at why traditional language servers use so much of it. The standard Rust language server, rust-analyzer, relies on the Salsa framework. Salsa is an incremental computation engine. It tracks every dependency, compiler query, and intermediate analysis state. When you change a line of code, Salsa knows exactly which parts of the dependency graph are dirty and recalculates only those parts.
This approach is fast, but it is memory-intensive. Salsa keeps the entire Abstract Syntax Tree (AST) of your project and all its dependencies in memory. It also keeps type tables, macro expansion results, and name resolution graphs.
In Rust, heap allocations are clean but they carry metadata overhead. A standard compiler representation of a syntax tree contains thousands of small nodes. Each node is typically heap-allocated and linked via pointers. When you multiply this by the hundreds of dependencies in a typical Cargo project, the pointer overhead alone consumes hundreds of megabytes. The allocator becomes fragmented, and the operating system cannot easily reclaim the memory.
The Glancer Architecture
Glancer takes a different path. It is built on the assumption that you do not need a full compiler frontend running in the background just to browse code. Most of the time, you need to jump to a definition, find where a function is called, or view a file outline—especially when navigating projects designed with small functions for readable code.
Glancer replaces the persistent compiler database with an ephemeral, stream-parsed index. The architecture relies on three main design decisions:
- Immediate AST Disposal: Glancer parses files using a fast, streaming parser. It extracts only the symbols, imports, and reference points it needs, writes them to a flat index, and drops the parser state immediately. The AST of a file exists in memory for only a few milliseconds.
- Memory-Mapped Flat Indexes: Instead of storing symbols in pointer-heavy heap structures like trees or nested hash maps, Glancer serializes index data into contiguous byte arrays. It writes these arrays to disk and accesses them using memory mapping (
mmap). The operating system handles caching and paging, keeping the active RAM usage near zero. - Shared Dependency Caching: Dependencies in Rust are immutable. A specific version of a crate like
serdeortokionever changes. Glancer indexes these crates once and stores the flat index files in a global cache directory. If you work on five different projects that use the same version of a dependency, they all map the same index file on disk.
Designing a Flat, Zero-Alloc Index
To see how this works in practice, compare a typical heap-allocated symbol index with Glancer's flat layout. A naive implementation of a symbol index might look like this:
// Naive representation with high heap overhead
struct Symbol {
name: String,
file_path: std::path::PathBuf,
range: Range,
children: Vec<Symbol>,
}
struct Range {
start_line: u32,
start_col: u32,
end_line: u32,
end_col: u32,
}If a project has 500,000 symbols, this naive structure creates millions of individual allocations. The String fields, the PathBuf fields, and the nested Vec containers all allocate separately. The allocator metadata overhead can easily double the actual data size.
Glancer avoids this by pooling all data into flat arrays. It uses integer offsets instead of pointers or strings.
// Glancer representation with zero heap allocations per symbol
struct FlatSymbol {
name_offset: u32,
name_len: u32,
file_id: u32,
start_line: u32,
start_col: u32,
end_line: u32,
end_col: u32,
kind: u8,
}
struct SymbolIndex {
// One contiguous block of memory for all symbol names
string_pool: Vec<u8>,
// Sorted array of symbols for fast binary search
symbols: Vec<FlatSymbol>,
// Map of file IDs to paths
file_paths: Vec<String>,
}With this design, lookups do not require traversing pointers. To find a symbol, Glancer performs a binary search on the symbols slice, which is sorted by the symbol name hash or the name itself. Because the slice is contiguous in memory, it is highly cache-friendly. The CPU can load the data into its L1/L2 cache without pointer chasing.
Here is a simplified example of how Glancer searches this flat index:
impl SymbolIndex {
pub fn find_symbol(&self, query: &str) -> Option<&FlatSymbol> {
let query_bytes = query.as_bytes();
self.symbols.binary_search_by(|probe| {
let start = probe.name_offset as usize;
let end = start + probe.name_len as usize;
let symbol_name = &self.string_pool[start..end];
symbol_name.cmp(query_bytes)
}).ok().map(|index| &self.symbols[index])
}
}Because this structure contains no pointers, Glancer can write the entire SymbolIndex directly to disk as a raw byte array. When the language server starts up, it opens the file using the memmap2 crate. The operating system maps the file into the process's address space. The startup time is instantaneous because no parsing or allocation occurs. The data is read from disk only when a search query hits that specific page of the file.
Lazy Macro Expansion
Rust macros are a major source of complexity and memory consumption. A macro can generate hundreds of lines of code, including new structs, traits, and function implementations. To resolve symbols correctly, a language server must expand these macros.
rust-analyzer expands all macros eagerly during its initial indexing phase. If you use a crate like serde with many derive macros, the language server spends significant time and memory expanding these macros and keeping the generated code in its database.
Glancer handles macros lazily. It parses the macro invocation but does not expand it during the initial index pass. It simply records that a macro was invoked at a specific location.
If you request a definition lookup for a symbol that Glancer cannot find in the main index, it identifies if the cursor is within a macro invocation. Only then does it expand that specific macro in memory, parse the temporary output, and search for the target symbol. This targeted expansion keeps the memory footprint small during normal editing.
Performance Comparisons
To evaluate the efficiency of this approach, we can look at benchmarks run on a project containing approximately 400,000 lines of Rust code (including dependency source code).
| Metric | rust-analyzer | Rust Glancer |
|---|---|---|
| Initial Indexing Time | 42.3 seconds | 3.1 seconds |
| Warm Startup Time | 12.1 seconds | 0.08 seconds |
| RAM Usage (Post-Index) | 1.84 GB | 14.2 MB |
| RAM Usage (Peak) | 2.10 GB | 38.5 MB |
The difference in startup time is due to the memory-mapped index design. rust-analyzer must read its database, verify cache integrity, and load state into memory. Glancer simply opens its file descriptors and is immediately ready to respond to editor queries.
The Trade-offs of Low-Memory Indexing
Glancer is not a drop-in replacement for rust-analyzer in all workflows. By discarding the compiler frontend, it sacrifices features that require deep semantic understanding of the code.
Type-directed auto-completion is the most significant omission. In rust-analyzer, typing a dot after a variable displays only the methods implemented for that specific type. This requires full type inference, trait resolution, and macro expansion. Glancer does not perform type inference. When you trigger completion, it offers text-based completions derived from the local file and globally indexed symbol names.
It also does not run compiler diagnostics in real-time. While rust-analyzer can show syntax and type errors as you type, Glancer leaves diagnostics to your build system. Most developers using Glancer run cargo check on file save or keep a terminal open running cargo watch.
What you get in return is a language server that runs comfortably on low-power machines, remote SSH sessions with strict resource limits, or large monorepos (where you might otherwise run into issues like musl binaries segfaulting on large directory searches).
Co-existing with Traditional Tools
You do not have to choose exclusively between full IDE features and low memory usage. Many developers configure their editors to use Glancer as the primary language server for navigation, while reserving heavy analysis tools for manual runs.
In Neovim, for example, you can register Glancer as the handler for definition and reference queries, while disabling its completion engine if you prefer a lighter setup. This allows you to browse massive codebases with minimal resource usage, only launching the full compiler tooling when you begin a major refactoring task.
By shifting the architectural focus from active compilation to static indexing, Glancer shows that language servers do not need to be resource hogs to be useful.



