WebGL 2 is OpenGL ES 3.0, a 2012 standard descended from 1992's OpenGL: one global state machine that every call edits and every draw re-validates. The native APIs moved on to explicit designs (Metal in 2014, Direct3D 12 in 2015, Vulkan in 2016) in which pipelines are built once and commands are recorded in buffers. Rather than bolt that onto WebGL, the browser vendors started a new API at the W3C, and Khronos stopped extending WebGL: its WebGL 2.0 Compute draft, led by Intel, ends with the note that "the decision was made to halt further expansion of the WebGL API" and that "WebGPU will be the path forward for compute shaders on the web."

WebGL is not being retired: every browser still ships and maintains it, it reaches 96.44% of users against WebGPU's 87.35% (caniuse.com 3,038 , September 2026), and it runs in software where WebGPU could not on this machine (Migration Paths). Why WebGPU Exists tells the same story from WebGPU's side.
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
.wrap { position: relative; max-width: 600px; }
canvas { display: block; width: 100%; }
.lbl { position: absolute; transform: translate(-50%, 0); font: 10px monospace; text-align: center; white-space: nowrap; line-height: 1.25; }
p { max-width: 600px; margin: 4px 6px; font: 11px monospace; }
</style>
<div class="wrap"><canvas id="c" width="600" height="300"></canvas></div>
<p id="out"></p>
<script>
const gl = document.getElementById('c').getContext('webgl2');
const wrap = document.querySelector('.wrap');
// [year, label, row: 0 = OpenGL family (above the line), 1 = explicit APIs (below), label level]
const events = [[1992, 'OpenGL 1.0', 0, 0], [2007, 'OpenGL ES 2.0', 0, 1], [2011, 'WebGL 1', 0, 0], [2012, 'OpenGL ES 3.0', 0, 1],
[2014, 'Metal', 1, 0], [2015, 'Direct3D 12', 1, 1], [2016, 'Vulkan', 1, 0], [2017, 'WebGL 2 ships', 0, 0], [2019, 'WebGL 2.0 Compute\nhalted', 0, 1],
[2023, 'WebGPU in Chrome', 1, 0], [2026, 'WebGPU in all\nmajor engines', 1, 1]];
const X = (year) => 30 + (Math.max(0, year - 2005) * 26 + Math.min(year, 2005) - 1990) / (21 * 26 + 15) * 540; // squeeze the quiet 1990s
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec2 aPixel; layout(location = 1) in vec3 aColor; layout(location = 2) in float aSize;
out vec3 vColor;
void main() { vColor = aColor; gl_PointSize = aSize; gl_Position = vec4(aPixel / vec2(300.0, 150.0) - 1.0, 0.0, 1.0); }`],
[gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec3 vColor; out vec4 c;
void main() { float r = length(gl_PointCoord - 0.5); if (r > 0.5) discard;
c = vec4(mix(vColor, vec3(1.0), step(r, 0.18) * 0.6), 1.0); }`]]) {
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
const blue = [0.12, 0.37, 0.55], gold = [0.88, 0.6, 0.06], red = [0.71, 0.27, 0.18];
const points = []; // pixel x, y (from bottom), colour, size
for (let year = 1990; year <= 2027; year++) points.push(X(year), 170, 0.6, 0.57, 0.52, year % 5 ? 3 : 6); // the axis as dots
events.forEach(([year, label, row, level]) => {
const colour = label.includes('halted') ? red : row ? gold : blue;
points.push(X(year), 170, ...colour, 16);
const top = row ? 300 - 170 + 16 + (level * 30) : 300 - 170 - 44 - (level * 30);
wrap.insertAdjacentHTML('beforeend', `<div class="lbl" style="left:${X(year) / 6}%;top:${top / 3}%;color:rgb(${colour.map(c => c * 200)})">` +
`<b>${year}</b><br>${label.replace('\n', '<br>')}</div>`);
});
[2010, 2020].forEach((year) => wrap.insertAdjacentHTML('beforeend', `<div class="lbl" style="left:${X(year) / 6}%;top:${(300 - 166) / 3}%;color:#999">${year}</div>`));
// Reach bars at the bottom, as rows of dots (caniuse.com, September 2026).
[['WebGL 2', 96.44, blue, 40], ['WebGPU', 87.35, gold, 18]].forEach(([name, share, colour, y]) => {
for (let i = 0; i < share / 2; i++) points.push(110 + i * 9, y, ...colour, 8);
wrap.insertAdjacentHTML('beforeend', `<div class="lbl" style="left:9%;top:${(300 - y - 7) / 3}%">${name}</div>` +
`<div class="lbl" style="left:${(110 + share / 2 * 9 + 26) / 6}%;top:${(300 - y - 7) / 3}%">${share}%</div>`);
});
gl.bindVertexArray(gl.createVertexArray());
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 24, 0); gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 8); gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 24, 20); gl.enableVertexAttribArray(2);
gl.clearColor(0.97, 0.96, 0.93, 1); gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, points.length / 6);
document.getElementById('out').textContent = 'This browser: WebGL 2 ' + (gl ? 'yes' : 'no') + ', navigator.gpu ' + ('gpu' in navigator ? 'present' : 'absent');
if (navigator.gpu) navigator.gpu.requestAdapter().then((adapter) => {
document.getElementById('out').textContent += adapter ? ', WebGPU adapter available' : ', but no WebGPU adapter';
});
</script>