WebGL 2 Migration

A WebGL 1 to WebGL 2 Migration Checklist

A WebGL 2 context still compiles GLSL ES 1.00 (NPOT Textures's probe ran the same shader in both versions), so switch the context first, then port shaders one at a time:

Porting WebGL 1 code to WebGL 2
WebGL 1 WebGL 2
getContext('webgl') or 'experimental-webgl' getContext('webgl2')
No version line (GLSL ES 1.00) #version 300 es as the very first line
attribute, varying in in the vertex shader, out to in between stages
gl_FragColor, texture2D() Your own out vec4; texture() for every sampler
VAO, instancing, draw-buffer extensions createVertexArray(), drawArraysInstanced(), drawBuffers()
OES_texture_float, WEBGL_depth_texture Sized formats (RGBA32F, DEPTH_COMPONENT24)
Power-of-two rules, 16-bit indices Gone: NPOT mipmaps, Uint32Array indices

Do not port extension checks blindly: extensions promoted to the core disappear from WebGL 2's list. Chrome 154 1 offered 16 extensions to WebGL 1 that its WebGL 2 context did not, on both renderers here: ANGLE_instanced_arrays, EXT_blend_minmax, EXT_disjoint_timer_query, EXT_frag_depth, EXT_shader_texture_lod, EXT_sRGB, OES_element_index_uint, OES_fbo_render_mipmap, OES_standard_derivatives, OES_texture_float, OES_texture_half_float, OES_texture_half_float_linear, OES_vertex_array_object, WEBGL_color_buffer_float, WEBGL_depth_texture and WEBGL_draw_buffers. Rendering to float textures still needs EXT_color_buffer_float (Deferred Rendering).

The same cover drawn by a GLSL ES 1.00 program in WebGL 1 and its ported 3.00 version in WebGL 2, and the extensions that became coreHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  .row { display: flex; gap: 10px; max-width: 600px; }
  .cell { flex: 1; text-align: center; font-family: monospace; font-size: 10px; }
  canvas { display: block; width: 100%; }
  pre { max-width: 600px; margin: 6px 0 0; font: 10px monospace; white-space: pre-wrap; }
</style>
<div class="row">
  <div class="cell"><canvas id="a" width="290" height="190"></canvas>getContext('webgl') + GLSL ES 1.00</div>
  <div class="cell"><canvas id="b" width="290" height="190"></canvas>getContext('webgl2') + #version 300 es</div>
</div>
<pre id="out"></pre>
<script>
// WebGL 1 style: attribute, varying, gl_FragColor, texture2D.
const glsl100 = [`attribute vec2 aPos;
varying vec2 vUV;
void main() { vUV = aPos; gl_Position = vec4(aPos * 2.0 - 1.0, 0.0, 1.0); }`, `precision mediump float;
varying vec2 vUV; uniform sampler2D uCover;
void main() { gl_FragColor = texture2D(uCover, vUV) * (0.8 + 0.2 * vUV.y); }`];
// Ported: #version first, in/out, your own output, texture().
const glsl300 = [`#version 300 es
in vec2 aPos;
out vec2 vUV;
void main() { vUV = aPos; gl_Position = vec4(aPos * 2.0 - 1.0, 0.0, 1.0); }`, `#version 300 es
precision mediump float;
in vec2 vUV; uniform sampler2D uCover; out vec4 outColor;
void main() { outColor = texture(uCover, vUV) * (0.8 + 0.2 * vUV.y); }`];
const art = Object.assign(document.createElement('canvas'), { width: 128, height: 192 }), g = art.getContext('2d');
g.fillStyle = '#3f7d3a'; g.fillRect(0, 0, 128, 192);
g.fillStyle = '#f2ead8'; g.beginPath(); g.moveTo(10, 150); g.lineTo(64, 50); g.lineTo(118, 150); g.fill();
g.fillStyle = '#fff'; g.font = 'bold 15px Georgia'; g.textAlign = 'center'; g.fillText('Small Steps', 64, 172);
function draw(id, version, [vs, fs]) {
  const gl = document.getElementById(id).getContext(version);
  const p = gl.createProgram();
  for (const [type, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fs]]) {
    const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
    gl.attachShader(p, s);
  }
  gl.bindAttribLocation(p, 0, 'aPos');
  gl.linkProgram(p); gl.useProgram(p);
  gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW);
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0);
  gl.bindTexture(gl.TEXTURE_2D, gl.createTexture());
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, art);   // 128 x 192: NPOT
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);          // WebGL 1 NPOT: no mipmaps,
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);       // clamped on both axes
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.clearColor(0.97, 0.96, 0.93, 1); gl.clear(gl.COLOR_BUFFER_BIT);
  gl.viewport(95, 5, 100, 180);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
  return gl.getSupportedExtensions();
}
const ext1 = draw('a', 'webgl', glsl100), ext2 = draw('b', 'webgl2', glsl300);
const promoted = ext1.filter((e) => !ext2.includes(e));          // offered to WebGL 1 but not WebGL 2
document.getElementById('out').textContent =
  `${promoted.length} extensions of WebGL 1 are missing from WebGL 2 here (promoted to core or replaced):\n` + promoted.join(', ');
</script>