Pi feels instant when it reaches its prompt quickly. That makes extension startup cost unusually visible: one synchronous session_start handler can turn a lightweight terminal UI into a multi-second wait.
This is a field report from tuning a real Pi setup. The goal was not to remove useful extensions, but to move work off the critical path and stop initializing tools that were not needed for every session.
The baseline
The first useful benchmark recorded a 6,049 ms TUI startup with 373 resources loaded. A later run reached 8,353 ms, which exposed an important rule: one startup measurement is not a performance conclusion. Cold-start work and process scheduling introduce substantial noise, so comparisons need repeated runs and medians.
The resource timings still identified two clear suspects:
| Extension | Import cost | Startup behavior |
|---|---|---|
pi-hermes-memory |
~700–786 ms | Memory/database lifecycle work during startup |
@mrclrchtr/supi-code-intelligence |
~230–334 ms | Tree-sitter and LSP provider startup on session_start |
The cache extension was not the problem. Its import took about 2 ms.
First fix: keep the cache hot path cheap
The Ollama Cloud model cache was changed in two ways:
- Its TTL increased from one hour to one week.
- A fresh cache is loaded synchronously from
~/.pi/agent/extensions/.ollama-cloud-cache.json.
Network refresh moved to session_start_complete, after the interactive startup path. That preserves a fast local read while keeping the cache refreshed in the background.
The extension tests passed 20/20, and the installed package matched the source. This is a useful pattern for startup work: make the critical path a local read, then refresh asynchronously after the UI is usable.
Hermes: defer memory initialization
Hermes had two costs:
- a large static module graph containing SQLite, migration, recovery, indexing, memory tools, and skill handlers;
- synchronous memory initialization because
lazyInitializationdefaulted tofalse.
The configuration became:
{
"lazyInitialization": true,
"memoryMode": "policy-only"
}
This moved database and memory initialization to first use while retaining policy-only behavior. A fresh run measured 4,914.82 ms, about 1.13 seconds faster than the earlier 6,049.58 ms run. The import cost remained roughly 709 ms, so lazy initialization solved lifecycle work—not module loading.
SuPi: the hidden multi-second wait
SuPi looked modest in the resource table because its module import was only a few hundred milliseconds. The larger cost was hidden in its awaited lifecycle handler:
session_start
└─ acquire workspace provider host
├─ start Tree-sitter
├─ load LSP settings
├─ scan the workspace
├─ detect language servers
└─ start servers and wait for readiness
Disabling SuPi temporarily reduced startup from 4,914.82 ms to 1,415.92 ms. That roughly 3.5-second difference made it the dominant bottleneck.
Disable language servers you do not use
SuPi supports per-language server configuration in ~/.pi/agent/supi/config.json. Ruby and R were not needed in this workspace, so they were disabled without affecting the other servers:
{
"lsp": {
"servers": {
"ruby": { "enabled": false },
"r": { "enabled": false }
}
}
}
lsp.exclude is for workspace path patterns; it is not the right mechanism for disabling a language. Use lsp.servers.<language>.enabled: false for that.
Make SuPi startup lazy
The installed SuPi release already had lazy server routing after the controller was ready, but no configuration switch for lazy controller initialization. The lifecycle handler itself still awaited the entire provider acquisition during session_start.
The lifecycle was changed so acquisition runs in the background:
pi.on("session_start", (_event, ctx) => {
void acquireProvidersInBackground(ctx);
});
The real implementation also includes a generation token. If the session is restarted or shut down while acquisition is running, a stale lease is released instead of being attached to the new session. That guard matters: making startup asynchronous without lifecycle cancellation can leak providers or attach an old workspace controller to a new session.
Result
After the lazy SuPi change and Ruby/R exclusions, a fresh benchmark recorded:
3,215.73 ms TUI startup
373 resources
Compared with the 4,914.82 ms run immediately before the SuPi patch, that is approximately 1.70 seconds faster, or 34.5% in that run.
The result is encouraging, but it is not a clean scientific comparison yet. Earlier runs ranged from 4.15 seconds to 8.35 seconds, with one extreme outlier around 70 seconds. The next step should be five or more runs per configuration and a median, ideally with the same terminal, working directory, cache state, and extension set.
Practical tuning checklist
- Measure startup with the real runtime, not only a fixed dashboard fixture.
- Record resource-level import timings.
- Separate module import cost from lifecycle cost.
- Move network refreshes after startup completion.
- Enable Hermes lazy initialization when startup memory work is not required immediately.
- Disable language servers you do not use.
- Do not block
session_starton LSP readiness unless the first interaction truly requires it. - Add generation and shutdown guards before making lifecycle work asynchronous.
- Keep benchmark logging opt-in and remove diagnostic instrumentation after investigation.
- Use repeated samples; treat a single timing as evidence, not proof.
The main lesson is that startup bottlenecks are often not where the import table points. A 230 ms extension import can still cost several seconds if its startup hook eagerly scans a workspace and waits for external processes.