After preventDefault(), the browser builds a new context behind the same gl object and fires webglcontextrestored. Keep all GPU setup in one init() that runs at startup and on every restore, and keep CPU-side sources (vertex arrays, decoded images) so nothing is fetched twice. This page loses the context at frame 10 and restores it 300 ms later:
<div style="display:flex;gap:14px">
<canvas id="c" width="700" height="260" style="width:350px;height:130px"></canvas>
<pre id="log" style="font-size:13px;margin:0"></pre></div>
<script src="webgl-helper.js"></script>
<script>
const canvas = document.getElementById('c'), wall = GLH.coverWall(); // CPU-side data kept
const log = (text) => { document.getElementById('log').textContent +=
`${performance.now().toFixed(0).padStart(5)} ms ${text}\n`; };
let gl, vao, frame = 0, request;
function init() { // every GPU object is created here
gl = canvas.getContext('webgl2'); // the same object after a restore
const { attributes, uniforms } = GLH.load(gl, `#version 300 es
in vec2 aPosition; in float aBook; uniform vec3 uColors[6]; out vec3 vColor;
void main() { vColor = uColors[int(aBook)]; gl_Position = vec4(aPosition, 0, 1); }`,
`#version 300 es
precision mediump float; in vec3 vColor; out vec4 c;
void main() { c = vec4(vColor, 1); }`);
uniforms.uColors(GLH.covers.flatMap(GLH.rgb));
vao = GLH.vao(gl, attributes, wall.vertices, [['aPosition', 2], ['aUV', 2], ['aBook', 1]],
wall.indices);
}
function draw() {
gl.clearColor(0.93, 0.91, 0.87, 1); gl.clear(gl.COLOR_BUFFER_BIT);
gl.bindVertexArray(vao); gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);
if (++frame === 10) { log('frame 10: loseContext()'); lose.loseContext(); }
if (frame < 16) request = requestAnimationFrame(draw);
else { log(`frame 16: getError() ${gl.getError()}`); window.__done = true; }
}
canvas.addEventListener('webglcontextlost', (event) => {
event.preventDefault(); cancelAnimationFrame(request); log('webglcontextlost');
setTimeout(() => { log('restoreContext()'); lose.restoreContext(); }, 300);
});
canvas.addEventListener('webglcontextrestored', () => {
log('webglcontextrestored: init()'); init(); request = requestAnimationFrame(draw);
});
init();
const lose = gl.getExtension('WEBGL_lose_context'); // kept: it survives the restore
log('init(), drawing'); request = requestAnimationFrame(draw);
</script>
Old objects stay dead: useProgram(old) gave INVALID_OPERATION, "object does not belong to this context". Without preventDefault(), restoreContext() failed with "context restoration not allowed". Three.js 30,815 and Babylon.js 89,603 restore their own resources; raw WebGL code needs its own init().
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
.row { display: flex; gap: 12px; max-width: 600px; align-items: flex-start; }
canvas { width: 330px; height: 123px; flex: none; }
pre { margin: 0; font: 11px monospace; }
</style>
<div class="row">
<canvas id="c" width="660" height="246"></canvas>
<pre id="log"></pre>
</div>
<script>
const canvas = document.getElementById('c');
const log = (text) => { document.getElementById('log').textContent += `${performance.now().toFixed(0).padStart(5)} ms ${text}\n`; };
// CPU-side data survives a loss: keep it, so nothing has to be rebuilt from scratch.
const covers = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
const vertices = [], indices = [];
covers.forEach((hex, i) => {
for (const [u, v] of [[0, 0], [1, 0], [1, 1], [0, 1]]) vertices.push(-0.94 + i * 0.32 + u * 0.26, -0.82 + v * 1.64, i);
indices.push(i * 4, i * 4 + 1, i * 4 + 2, i * 4, i * 4 + 2, i * 4 + 3);
});
let gl, vao, frame = 0, request;
function init() { // every GPU object is created here
gl = canvas.getContext('webgl2'); // the same object after a restore
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec2 aPosition; layout(location = 1) in float aBook; uniform vec3 uColors[6]; out vec3 vColor;
void main() { vColor = uColors[int(aBook)]; gl_Position = vec4(aPosition, 0, 1); }`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec3 vColor; out vec4 c; void main() { c = vec4(vColor, 1); }`]]) {
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
gl.uniform3fv(gl.getUniformLocation(program, 'uColors'), covers.flatMap((hex) => [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16) / 255)));
vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 12, 0); gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(1, 1, gl.FLOAT, false, 12, 8); gl.enableVertexAttribArray(1);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
}
function draw() {
gl.clearColor(0.93, 0.91, 0.87, 1); gl.clear(gl.COLOR_BUFFER_BIT);
gl.bindVertexArray(vao); gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);
if (++frame === 10) { log('frame 10: loseContext()'); lose.loseContext(); }
if (frame < 16) request = requestAnimationFrame(draw);
else log(`frame 16: getError() ${gl.getError()}`);
}
canvas.addEventListener('webglcontextlost', (event) => {
event.preventDefault(); cancelAnimationFrame(request); log('webglcontextlost');
setTimeout(() => { log('restoreContext()'); lose.restoreContext(); }, 300);
});
canvas.addEventListener('webglcontextrestored', () => {
log('webglcontextrestored: init()'); init(); request = requestAnimationFrame(draw);
});
init();
const lose = gl.getExtension('WEBGL_lose_context'); // kept: it survives the restore
log('init(), drawing');
request = requestAnimationFrame(draw);
</script>