Details
- Browser and browser version: Chromium (reproduced on Electron 30+/Chromium 124+ and stock Chrome); driver-dependent
- OS version: Windows 11 (also seen on macOS)
- xterm.js version: 5.5 / current
master, @xterm/addon-webgl
Steps to reproduce
-
Create a Terminal, open() it, and load a WebglAddon.
-
Dispose the addon (or the terminal), then repeat: create + dispose many WebGL-backed terminals over the app's lifetime (tab/split churn, session restore, etc.).
-
Once the number of contexts created this way passes Chromium's per-document live-context cap (~16, driver/build dependent), the console logs:
WARNING: Too many active WebGL contexts. Oldest context will be lost.
and a still-live terminal loses its renderer (webglcontextlost) and renders blank / as an "X box" / garbled.
A self-contained repro is attached (repro.html): it keeps one live terminal and then creates and disposes 30 more WebGL terminals.
Root cause
WebglRenderer's dispose (registered in the constructor) tears down the render layers and removes the <canvas> from the DOM, but never releases the WebGL2 context:
https://github.com/xtermjs/xterm.js/blob/master/addons/addon-webgl/src/WebglRenderer.ts#L160-L168
this._register(toDisposable(() => {
clearTimeout(this._contextRestorationTimeout);
this._contextRestorationTimeout = undefined;
for (const l of this._renderLayers) {
l.dispose();
}
this._canvas.parentElement?.removeChild(this._canvas);
removeTerminalFromCache(this._terminal);
}));
Detaching the canvas does not free the GPU-side context — it lingers until the canvas is garbage collected, which is non-deterministic and can be arbitrarily delayed. Every disposed addon leaves a "zombie" context that still counts toward Chromium's cap. When the cap is exceeded the browser force-evicts the oldest context, which may belong to a terminal that is still on screen, so a live terminal goes blank rather than the disposed one being cleaned up.
Impact
Any embedder that creates and disposes many terminals in one document hits this — VS Code-style multi-terminal UIs, terminal multiplexers, IDEs with per-tab terminals. The failure is confusing because the terminal that breaks is not the one that was disposed, and it only appears after enough churn to cross the (driver-dependent) cap.
We currently work around it downstream by reaching into private addon internals (addon._renderer._gl) after dispose() and calling WEBGL_lose_context.loseContext() ourselves — which is exactly the kind of thing that should live in the addon.
Suggested fix
Call this._gl.getExtension('WEBGL_lose_context')?.loseContext() as part of the renderer's dispose so the context is released immediately and the live count stays bounded. PR to follow.
Details
master,@xterm/addon-webglSteps to reproduce
Create a
Terminal,open()it, and load aWebglAddon.Dispose the addon (or the terminal), then repeat: create + dispose many WebGL-backed terminals over the app's lifetime (tab/split churn, session restore, etc.).
Once the number of contexts created this way passes Chromium's per-document live-context cap (~16, driver/build dependent), the console logs:
and a still-live terminal loses its renderer (
webglcontextlost) and renders blank / as an "X box" / garbled.A self-contained repro is attached (
repro.html): it keeps one live terminal and then creates and disposes 30 more WebGL terminals.Root cause
WebglRenderer's dispose (registered in the constructor) tears down the render layers and removes the<canvas>from the DOM, but never releases the WebGL2 context:https://github.com/xtermjs/xterm.js/blob/master/addons/addon-webgl/src/WebglRenderer.ts#L160-L168
Detaching the canvas does not free the GPU-side context — it lingers until the canvas is garbage collected, which is non-deterministic and can be arbitrarily delayed. Every disposed addon leaves a "zombie" context that still counts toward Chromium's cap. When the cap is exceeded the browser force-evicts the oldest context, which may belong to a terminal that is still on screen, so a live terminal goes blank rather than the disposed one being cleaned up.
Impact
Any embedder that creates and disposes many terminals in one document hits this — VS Code-style multi-terminal UIs, terminal multiplexers, IDEs with per-tab terminals. The failure is confusing because the terminal that breaks is not the one that was disposed, and it only appears after enough churn to cross the (driver-dependent) cap.
We currently work around it downstream by reaching into private addon internals (
addon._renderer._gl) afterdispose()and callingWEBGL_lose_context.loseContext()ourselves — which is exactly the kind of thing that should live in the addon.Suggested fix
Call
this._gl.getExtension('WEBGL_lose_context')?.loseContext()as part of the renderer's dispose so the context is released immediately and the live count stays bounded. PR to follow.