Problem
Every tab switch triggers WebGL addon deactivation on hidden panes and reactivation on shown panes:
```ts
// Tab.show() activates WebGL on all panes:
for (const pane of this.panes) {
pane.activateWebGL(); // Creates new WebGL context
}
// Tab.hide() deactivates WebGL on all panes:
for (const pane of this.panes) {
pane.deactivateWebGL(); // Destroys WebGL context
}
```
WebGL context creation involves:
- Allocating a GPU context (`canvas.getContext('webgl2')`)
- Compiling shaders
- Uploading glyph atlas texture
- Setting up framebuffers
This costs 20-50ms per pane and is the primary source of latency when switching tabs.
Code reference
`pane-webgl.ts:31-69` — `activate()` creates a fresh WebglAddon + ImageAddon
`pane-webgl.ts:77-101` — `deactivate()` disposes both addons entirely
The pattern was introduced to solve WebGL context exhaustion (#135) — browsers limit total WebGL contexts (typically 8-16). The current approach is correct for preventing exhaustion but has a high per-switch cost.
Proposed solution
1. LRU context pool with lazy eviction
Instead of destroy-on-hide / create-on-show, maintain a pool of WebGL contexts with LRU eviction:
```ts
class WebGLPool {
private contexts = new Map<string, WebglAddon>();
private lru: string[] = [];
private maxContexts = 6; // Leave headroom below browser limit
acquire(paneId: string, terminal: Terminal): WebglAddon {
if (this.contexts.has(paneId)) {
// Move to front of LRU
this.touch(paneId);
return this.contexts.get(paneId)!;
}
// Evict oldest if at capacity
while (this.contexts.size >= this.maxContexts) {
this.evictOldest();
}
const addon = new WebglAddon();
terminal.loadAddon(addon);
this.contexts.set(paneId, addon);
this.lru.push(paneId);
return addon;
}
}
```
This means switching between 2-3 recent tabs has zero WebGL overhead — the contexts stay alive.
2. Defer WebGL activation
Don't activate WebGL immediately on tab show. Wait until the first frame renders, then activate asynchronously:
```ts
show() {
// Show immediately with canvas renderer
// ...
// Activate WebGL after first frame
requestIdleCallback(() => this.activateWebGL());
}
```
This makes tab switches feel instant even if WebGL activation is slow.
3. Canvas fallback for rapid switching
If the user is rapidly switching tabs (e.g., Cmd+1, Cmd+2, Cmd+3), don't activate WebGL at all — use the canvas renderer. Only activate WebGL after the tab has been visible for >500ms.
Impact
- Tab switch latency: 20-50ms × panes per tab = 40-100ms per switch with 2 panes
- GPU memory churn: Constant allocation/deallocation of GPU resources
- Visual glitch: Brief flash of canvas-rendered content before WebGL takes over
- User perception: Tab switching feels sluggish compared to native terminals
Metrics to validate
- Measure `show()` duration with/without WebGL activation
- Profile GPU memory allocation during rapid tab switching
- Compare tab switch time: WebGL pool vs current destroy/create
Problem
Every tab switch triggers WebGL addon deactivation on hidden panes and reactivation on shown panes:
```ts
// Tab.show() activates WebGL on all panes:
for (const pane of this.panes) {
pane.activateWebGL(); // Creates new WebGL context
}
// Tab.hide() deactivates WebGL on all panes:
for (const pane of this.panes) {
pane.deactivateWebGL(); // Destroys WebGL context
}
```
WebGL context creation involves:
This costs 20-50ms per pane and is the primary source of latency when switching tabs.
Code reference
`pane-webgl.ts:31-69` — `activate()` creates a fresh WebglAddon + ImageAddon
`pane-webgl.ts:77-101` — `deactivate()` disposes both addons entirely
The pattern was introduced to solve WebGL context exhaustion (#135) — browsers limit total WebGL contexts (typically 8-16). The current approach is correct for preventing exhaustion but has a high per-switch cost.
Proposed solution
1. LRU context pool with lazy eviction
Instead of destroy-on-hide / create-on-show, maintain a pool of WebGL contexts with LRU eviction:
```ts
class WebGLPool {
private contexts = new Map<string, WebglAddon>();
private lru: string[] = [];
private maxContexts = 6; // Leave headroom below browser limit
acquire(paneId: string, terminal: Terminal): WebglAddon {
if (this.contexts.has(paneId)) {
// Move to front of LRU
this.touch(paneId);
return this.contexts.get(paneId)!;
}
// Evict oldest if at capacity
while (this.contexts.size >= this.maxContexts) {
this.evictOldest();
}
const addon = new WebglAddon();
terminal.loadAddon(addon);
this.contexts.set(paneId, addon);
this.lru.push(paneId);
return addon;
}
}
```
This means switching between 2-3 recent tabs has zero WebGL overhead — the contexts stay alive.
2. Defer WebGL activation
Don't activate WebGL immediately on tab show. Wait until the first frame renders, then activate asynchronously:
```ts
show() {
// Show immediately with canvas renderer
// ...
// Activate WebGL after first frame
requestIdleCallback(() => this.activateWebGL());
}
```
This makes tab switches feel instant even if WebGL activation is slow.
3. Canvas fallback for rapid switching
If the user is rapidly switching tabs (e.g., Cmd+1, Cmd+2, Cmd+3), don't activate WebGL at all — use the canvas renderer. Only activate WebGL after the tab has been visible for >500ms.
Impact
Metrics to validate