With a slow command, a rough baseline, and one or two trace files, you can now get to the problem and fix things.
Start With the Symptom, Not the Theory
Let's put it all together. We need to know which command is slow, and what kind of work it is doing. As we have said many times, each requires a different fix. Consider these four:
time git status
time git log -- path/to/file
time git switch main # actually switches — substitute your target branch
time git fetch --dry-run
Those four commands touch four different layers: local filesystem and index work, graph and tree history work, local materialization, and network synchronization.
Separate Local Time From Remote Time
When a command crosses the network, break the delay apart before guessing at fixes. For clone, fetch, and push, the time may include client-side preparation, server-side negotiation, hook or policy checks, transfer time, and local unpack or checkout. Start by timing the split:
time git fetch --dry-run
time git fetch
--dry-run skips some real costs, but it is often enough to separate negotiation and server work from later local unpack and checkout behavior.
What To Run First When You Do Not Know Yet
If the problem is still vague, start with one small instrumentation pass instead of jumping straight to fixes:
mkdir -p perf
git count-objects -vH >perf/count-objects.txt
git config --show-origin --get-regexp '^(core\.fsmonitor|core\.untrackedCache|index\.sparse|fetch\.|maintenance\.|commitGraph\.)' >perf/config.txt || true
time git status >/dev/null
GIT_TRACE2_PERF=perf/status.perf git status >/dev/null
That already answers several practical questions:
- is the repository layout unhealthy?
- are key local accelerators enabled?
- is the command slow in a repeatable way?
- does Trace2 show the time staying local rather than disappearing into the network?
If the slow command is fetch or push, swap in the network-oriented version:
time git fetch --dry-run >/dev/null
GIT_TRACE_PACKET=perf/fetch.packet GIT_TRACE2_PERF=perf/fetch.perf git fetch origin >/dev/null
git status Problems Usually Mean Local Filesystem and Index Cost
If the complaint is status, the likely cost layers are tracked-file validation, untracked-file discovery, ignore matching, index read and write cost, and working tree size. That points you toward the index, sparse-index, untracked cache, fsmonitor, split index, and worktree reduction, not toward graph or transport tuning. The questions to ask:
- is the working tree too large?
- is untracked discovery what's slow?
- would sparse-checkout or sparse-index reduce the local workload?
- is the repository missing index-side accelerators?
The first evidence pass can stay simple:
time git status
git config --show-origin --get-regexp '^(core\.fsmonitor|core\.untrackedCache|index\.sparse)'
git ls-files --debug | sed -n '1,20p'
Path-Limited History Usually Means Graph and Tree Cost
If the complaint is git log -- path, the likely cost layers are commit walk cost, path-limited history reasoning, changed-path Bloom filters, and rename heuristics. A path-limited history query is a graph walk with path reasoning layered on top, and sometimes rename heuristics on top of that, which is why the fix lives in commit-graph, changed-path Bloom filters, and the history-traversal chapter rather than anywhere near transport. The right questions are:
- is commit-graph present and current?
- were changed-path Bloom filters written?
- is the query path-limited, rename-heavy, or both?
The corresponding probe:
time git log -- path/to/file
git commit-graph verify
git config --show-origin --get-regexp '^commitGraph\.'
Checkout and Branch-Switch Cost Usually Means Materialization
If the complaint is switch, checkout, or branch movement generally, the likely cost layers are working-tree rewrite volume, index rewrite volume, sparse versus dense checkout layout, and stash churn from repeated context teardown. Slow checkout almost always means too much local materialization rather than bad history storage, which is why the fix lives in worktrees, sparse-checkout, sparse-index, and the earlier command-semantics material. The right questions are:
- should this really be one mutable checkout?
- would a second worktree remove the churn?
- is the checkout denser than the task requires?
Start here:
time git switch main
git worktree list
git sparse-checkout list
Clone and Fetch Problems Usually Mean Transport
If the complaint is clone or fetch, the likely cost layers are transfer volume, negotiation cost, server-side pack generation, missing bitmaps, absent bundle seeding, and the lack of a partial-clone strategy. Network diagnosis needs its own layer because many local optimizations do not help a bad clone path; the fix more often lives in protocol v2, bundle URIs, bitmaps, and partial clone. The right questions are:
- is the initial transfer too large?
- is negotiation too expensive?
- would bundle seeding reduce repeated bootstrap cost?
- would blobless partial clone or prefetch change the experience?
The first transport pass:
time git fetch --dry-run
git count-objects -vH
git config --show-origin --get-regexp '^(fetch\.|remote\..*promisor|maintenance\.)'
Typical Fixes Map to Typical Layers
Once the issue is known, return to the options we've learned.
Working-tree and index fixes:
- sparse-checkout
- sparse-index
- worktrees
- untracked cache
- fsmonitor
- split index
History and graph fixes:
- commit-graph
- changed-path Bloom filters
Storage fixes:
- maintenance
- incremental-repack or geometric repack
- MIDX
- bitmaps
Transport fixes:
- partial clone
git backfill- bundle URI
- Scalar prefetch
You can compress that mapping into a quick field guide:
- slow
git statusfirst instrumentation:time git status,git ls-files --debug, Trace2 perf likely fix area: sparse-checkout, sparse-index, fsmonitor, untracked cache - slow
git log -- pathfirst instrumentation:time git log -- path,git commit-graph verifylikely fix area: commit-graph, changed-path Bloom filters, query form - slow
git switchfirst instrumentation:time git switch,git worktree list, sparse state likely fix area: worktrees, sparse-checkout, checkout layout - slow
git fetchfirst instrumentation:time git fetch --dry-run, packet trace, Trace2 perf likely fix area: bitmaps, maintenance, bundle seeding, partial clone, prefetch - huge local disk use first instrumentation:
git count-objects -vH, the externalgit-sizertool likely fix area: maintenance, history cleanup, asset policy
A Practical Diagnostic Sequence
When a repository feels slow, a practical sequence looks like this:
- identify the command and workflow that hurt
- decide which Git layer that command primarily touches
- inspect repository layout and current accelerators
- trace the command if the cause is still unclear
- choose one targeted change
- rerun and compare
Worked Example: Slow git status In a Large Checkout
Suppose the complaint is:
"git status takes several seconds in this checkout."
Do not start with repack or clone flags. Start with the local evidence:
time git status >/dev/null
git config --show-origin --get-regexp '^(core\.fsmonitor|core\.untrackedCache|index\.sparse)' || true
git ls-files --debug | sed -n '1,20p'
GIT_TRACE2_PERF=/tmp/status.perf git status >/dev/null
Now read the result in order:
- If repeated
statusruns stay slow, the problem is probably real and local. - If fsmonitor and untracked cache are both absent, missing local accelerators are an obvious first suspect.
- If the checkout is much wider than the task needs, sparse-checkout and sparse-index become the first real candidates.
- If Trace2 shows most of the time inside local index and working-tree regions, stop looking at packfiles and transport.
That leads to much narrower fixes: enable fsmonitor if the platform and repo support it, enable untracked cache if directory scanning is what's eating time, reduce checkout width with sparse-checkout, and consider sparse-index if the repository is large enough for index layout itself to matter. The order matters more than the menu of features, because the goal is to learn which local layer is slow and what the evidence actually says about it.
Worked Example: Slow git fetch
Now suppose the complaint is:
"Fetch is slow every time, even when the change set is small."
The first pass is different:
time git fetch --dry-run >/dev/null
git count-objects -vH
git config --show-origin --get-regexp '^(fetch\.|remote\..*promisor|maintenance\.)' || true
GIT_TRACE_PACKET=/tmp/fetch.packet GIT_TRACE2_PERF=/tmp/fetch.perf git fetch origin >/dev/null
Now the likely readings are:
- If packet tracing shows a long negotiation or a lot of round trips, transport and reachability are the first place to look.
- If Trace2 shows most local time after transfer, unpack or checkout may be part of the cost.
- If maintenance and bitmap-related preparation are weak on the server side, the client may be suffering from poor repository layout upstream.
- If the repository is large and repeatedly fetched, prefetch, bundle seeding, or partial-clone policy may change the user experience more than local checkout tuning.
That usually narrows the next step to one of: server-side maintenance and bitmap freshness, prefetch strategy, partial clone, bundle seeding for first-clone pain, or separating transfer cost from later local materialization cost.
If traces point to stale server bitmaps, pack layout, or hook cost, stop piling on client config and fix the server-side layer instead.