How Lazygit Manages Memory When Handling Large Diffs: PTY Streaming and Incremental Reading
Lazygit streams large diffs through a pseudo-terminal (PTY) and reads only the lines visible in the UI viewport, keeping memory usage proportional to the screen size rather than the total diff size.
When working with massive repositories in jesseduffield/lazygit, users often encounter diffs spanning thousands of lines. Rather than loading entire patches into RAM, lazygit implements a sophisticated memory management strategy that processes diffs incrementally. This approach leverages pseudo-terminal streaming and viewport-aware reading to maintain consistent performance regardless of diff size.
The PTY-Based Streaming Architecture
Lazygit avoids buffering complete diff outputs by executing git commands inside a pseudo-terminal and consuming the output as a stream.
Building Diff Commands with DiffHelper
The process begins in pkg/gui/controllers/helpers/diff_helper.go, where the DiffArgs method constructs the git arguments (e.g., --stat -p <ref>). The resulting command object is created via GitCommands.DiffCmdObj in pkg/commands/git_commands/diff.go, which prepares the executable task without immediately running it.
// Build diff arguments and create command object
args := diffHelper.DiffArgs() // -> ["--stat","-p","HEAD"]
cmdObj := gui.Git().Diff.DiffCmdObj(args) // git diff command object
Executing in a Pseudo-Terminal
When rendering is required, Gui.newPtyTask in pkg/gui/pty.go determines whether the command needs a PTY based on custom pager configuration or external diff settings. If needed, it invokes pty.StartWithSize to spawn the process inside a pseudo-terminal and registers the PTY file descriptor in gui.viewPtmxMap.
// Run the diff in a PTY - lazygit decides automatically if PTY is required
gui.RenderToMainViews(gui.types.RefreshMainOpts{
Main: &gui.types.ViewUpdateOpts{
Title: "Diff",
Task: gui.types.NewRunPtyTaskWithPrefix(cmdObj.GetCmd(),
style.FgMagenta.Sprintf("git diff %s\n\n", strings.Join(args, " "))),
},
})
This PTY-based execution allows lazygit to treat the git output as a stream rather than a static buffer, enabling line-by-line consumption while the command runs.
Viewport-Aware Line Reading
The core memory optimization comes from reading only what the user can actually see, plus a small buffer for scrollbar calculations.
Calculating Visible Lines
In pkg/gui/view_helpers.go, the linesToReadFromCmdTask function determines exactly how many lines are needed to fill the current view and render an accurate scrollbar. This calculation returns a LinesToRead struct that instructs the UI precisely how much data to fetch from the command task.
// LinesToRead struct controls how many lines are fetched
type LinesToRead struct {
TotalLines int
LinesToRead int
ReadFromStart bool
}
Task-Based Incremental Consumption
The actual consumption happens in pkg/gui/tasks_adapter.go, where PTY output is wrapped in a task that reads incrementally via manager.ReadLines or ReadToEnd. As the user scrolls, the task fetches additional chunks on-demand rather than slurping the entire diff into memory. This ensures the memory footprint remains bounded by the viewport size plus scrollbar metadata, not the total diff size.
Minimal State and Line Number Adjustment
Lazygit maintains almost no persistent diff data in memory. The Diffing struct in pkg/gui/modes/diffing/diffing.go stores only the reference and direction flags (Ref, Reverse), containing zero diff content.
When the UI must map a line number from the diff view back to the working tree file, the adjustLineNumber method in diff_helper.go runs a minimal on-the-fly diff with --unified=0 and parses only the necessary patch data via patch.Parse. This targeted approach avoids loading the full file or complete diff history into memory.
// Adjust line number using minimal diff calculation
adjusted := diffHelper.adjustLineNumber(
42, // line number shown in diff view
"--", // diff against working copy
"path/to/file.go", // file path
)
Summary
- Lazygit manages memory for large diffs by streaming git output through a pseudo-terminal (PTY) rather than buffering the entire output.
- Viewport-limited reading via
LinesToReadcalculations inpkg/gui/view_helpers.goensures only visible lines plus a small scrollbar buffer reside in memory. - Task-based incremental consumption in
pkg/gui/tasks_adapter.goreads diff output line-by-line as the user scrolls, preventing memory spikes. - Minimal state storage means the
Diffingstruct holds only metadata (reference and direction), not diff content. - Efficient line mapping uses targeted
--unified=0diffs viaadjustLineNumberto calculate positions without parsing full patches.
Frequently Asked Questions
Does lazygit load the entire diff into memory when viewing large patches?
No. According to the jesseduffield/lazygit source code, the program explicitly avoids loading complete diffs. Instead, it streams the git diff output through a pseudo-terminal and reads only the amount of data needed to fill the visible viewport plus a small buffer for scrollbar calculations.
How does lazygit handle custom pagers with large diffs?
Lazygit supports custom pagers through PagerConfig in pkg/config/pager_config.go. When a custom pager is configured, lazygit still wraps the pager command inside the same PTY infrastructure used for standard diffs. The pager may handle its own buffering, but lazygit itself never stores the full diff before handing it to the pager.
What happens to memory usage when scrolling through a massive diff?
Memory usage remains stable and proportional to the terminal height. As implemented in pkg/gui/tasks_adapter.go, scrolling triggers incremental reads via the task manager (ReadLines), fetching additional chunks only when the viewport moves. Previously read lines may be discarded or cached based on the specific view implementation, but the total resident memory does not scale with the diff size.
How does lazygit calculate line numbers without parsing the full diff?
The adjustLineNumber function in pkg/gui/controllers/helpers/diff_helper.go executes a minimal diff with --unified=0 on-demand, parsing only the specific hunk headers required to map the displayed line number back to the working tree file. This targeted approach uses patch.Parse on a tiny subset of data rather than the complete diff output.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →