Skip to content

WebGL context create/destroy on every tab switch — expensive GPU round-trips #290

@Axelj00

Description

@Axelj00

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:

  1. Allocating a GPU context (`canvas.getContext('webgl2')`)
  2. Compiling shaders
  3. Uploading glyph atlas texture
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions