JavaScript WebGL Table
| Piece | What it does | Field note |
|---|---|---|
getContext('webgl2') | The entry | Returns null when unsupported - branch to fallback; WebGL2 = GLSL ES 3.0, VAOs, instancing built in |
shaders | The program | Vertex + fragment in GLSL, compiled and linked once; the pipeline is fixed, your shaders are its custom stages |
buffers + attributes | The geometry | Float32Array into ARRAY_BUFFER; vertexAttribPointer maps buffer to attribute - interleaved layouts save binds |
VAOs | The state bundle | One bind restores the whole attribute layout - WebGL2 makes them native instead of an extension |
uniforms | The per-draw constants | Cache getUniformLocation once; matrices flow here, per-vertex data through attributes |
drawArrays/Elements | The call | The only call that rasterizes; ELEMENT_ARRAY_BUFFER indexes past the 65k Uint16 ceiling |
context loss | The silent kill | GPU reset voids every resource - webglcontextlost/restored events, full re-init; init belongs in a function |
preserveDrawingBuffer | The screenshot trap | Default false = toDataURL reads blank after compositing - read synchronously or pay the flag's cost |
WebGL is the GPU escape hatch of the web platform: a canvas whose drawing commands become GPU work, speaking a C-like shader language (GLSL) instead of a 2D API. WebGL2 - the version every modern browser ships - brings GLSL ES 3.0, vertex array objects, instanced drawing, and 3D textures as baseline. The mental model that makes it clickable: the pipeline is FIXED (vertices in, triangles rasterized, pixels out), and your vertex and fragment shaders are the two customizable stages bolted into that fixed pipeline.
Bottom line: WebGL programming is state management with a rendering habit. Buffers hold numbers, attributes map buffers to shader inputs, uniforms carry per-draw constants, and a single drawArrays or drawElements call rasterizes. The wins come from bookkeeping: cache uniform locations once (getUniformLocation is a lookup, not a getter), record your attribute layout in a VAO so restoring geometry is one bind, and prefer interleaved buffers so one binding serves position, normal and UV.
The honest part: the context can vanish underneath you. GPU resets, driver upgrades, and mobile webview memory pressure all trigger webglcontextlost - every buffer, texture and program you compiled is gone, and nothing re-runs until webglcontextrestored fires and you rebuild the entire scene. Production WebGL is an architecture that can re-initialize from scratch, not a script that runs once. And the screenshot trap bites everyone once: by default the drawing buffer is cleared after compositing, so toDataURL returns blank unless you render synchronously right before reading or set preserveDrawingBuffer: true and pay for it.
How to use
- Get the context defensively: const gl = canvas.getContext('webgl2') || canvas.getContext('webgl'); - null means no WebGL (blocked, out of GPU memory, or software-rendering disabled), so branch to a static fallback instead of throwing. WebGL2 is a superset: write GLSL ES 3.00 shaders (#version 300 es first line) and VAOs come for free.
- Build the pipeline once: compile vertex and fragment shaders, check COMPILE_STATUS, link into a program, check LINK_STATUS, then gl.useProgram. Shader compile errors log to the console via getShaderInfoLog - read them, they carry line numbers.
- Feed geometry through buffers: bindBuffer + bufferData with a Float32Array, then vertexAttribPointer maps it to an attribute location (enableVertexAttribArray first). Record the whole layout in a VAO (createVertexArray/bindVertexArray) so switching models later is one bind call instead of re-walking every attribute.
- Move per-frame data through uniforms: cache locations with getUniformLocation at init, then uniformMatrix4fv your transform each frame. The split that keeps code sane: attributes are per-vertex data, uniforms are per-draw state, textures are per-draw images.
- Plan for context loss from day one: listen for webglcontextlost (call preventDefault to allow restoration) and webglcontextrestored (re-create programs, buffers, textures, VAOs - which is why init belongs in a function, not in top-level script). Test it in the browser's GPU-reset tooling or by opening dozens of WebGL tabs; the first real device reset should not be the first time your code meets the code path.
Frequently asked questions
Why is my canvas screenshot blank (toDataURL after render)?
The drawing buffer - the canvas's backing store - is cleared after each frame is composited to the page (preserveDrawingBuffer defaults to false), so reading it later reads an empty buffer. Fixes in cost order: call toDataURL (or drawImage into a 2D canvas) synchronously in the same task as your draw call, before yielding; or create the context with preserveDrawingBuffer: true, which keeps the buffer readable but costs performance on every frame because the browser cannot reuse or discard it. The synchronous-read pattern is free and works for occasional captures; preserveDrawingBuffer is for apps that screenshot constantly and accept the frame budget.
Do I need a framework like three.js?
For anything product-shaped, yes - three.js (or Babylon) buys you matrices, loaders, lights, and a scene graph that hides exactly the bookkeeping this page describes. Raw WebGL earns its keep for shader-heavy 2D effects (particle systems, image processing, full-screen post effects) where a scene graph is dead weight, for tiny bundles, and for understanding what the framework is actually doing when it breaks. The honest learning path: raw WebGL once, deliberately - then never debug a model matrix by hand again. Framework-free does not mean abstraction-free: you will rebuild a matrix library either way, and gl-matrix exists for a reason.
What actually happens on context loss?
The GPU (or the browser's watchdog for it) resets: the WEBGL_lose_context extension aside, every resource - programs, buffers, textures, framebuffers, VAOs - becomes invalid, the canvas blanks, and webglcontextlost fires. If you preventDefault() that event, the browser later fires webglcontextrestored on the SAME context object; you rebuild everything (init function, remember) and re-render. If you do not preventDefault, the context is gone for good and a new one needs a new canvas. Mobile browsers are aggressive here - backgrounded tabs lose contexts routinely - so the loss-restore path is not an edge case on phones, it is Tuesday.
WebGL2 or WebGL 1 - what do I actually get upgrading?
Baseline features that WebGL1 bolted on through extensions: vertex array objects (native, not OES_vertex_array_object), instanced drawing (angle_instanced_arrays built in), 3D textures and texture arrays, unsigned integer indices (no more 65k-vertex ceilings from Uint16), uniform blocks, transform feedback (GPU-side output of vertex processing), and GLSL ES 3.00 with real loops and integer math. Coverage is effectively universal in 2026 - the fallback branch matters mainly for ancient devices and locked-down webviews. Write GLSL 300 es, keep the WebGL1 context fallback only if your analytics say someone still needs it, and skip the extension dance entirely.