Buffer Objects

Creating and Filling Buffer Objects

createBuffer() makes a buffer, bindBuffer(target, buffer) attaches it to ARRAY_BUFFER (vertices) or ELEMENT_ARRAY_BUFFER (indices), and bufferData(target, data, usage) allocates and fills it; later calls name the target, not the buffer. The usage hint is STATIC_DRAW (set once), DYNAMIC_DRAW (changed often) or STREAM_DRAW (used a few times), plus WebGL 2's _READ and _COPY variants. Rather than creating a buffer per change, allocate once and patch with bufferSubData(); WebGL 2's getBufferSubData() reads bytes back:

Allocating once, patching in place and reading backJavaScript
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, 6 * 4, gl.DYNAMIC_DRAW);   // room for 3 points, zero-filled
gl.bufferSubData(gl.ARRAY_BUFFER, 8, new Float32Array([0.5, -0.25]));   // the second point
const back = new Float32Array(6);
gl.getBufferSubData(gl.ARRAY_BUFFER, 0, back);
console.log(back.join(', '), '| size', gl.getBufferParameter(gl.ARRAY_BUFFER, gl.BUFFER_SIZE));
gl.bufferSubData(gl.ARRAY_BUFFER, 20, new Float32Array([1, 2]));   // 8 bytes at 20 overflow 24
console.log('error', gl.getError());
Output
0, 0, 0.5, -0.25, 0, 0 | size 24
error 1281

Writing past the end is INVALID_VALUE (1281), so grow a buffer by calling bufferData() again. getBufferSubData() stalls until the GPU catches up; Pixel Buffer Objects reads back asynchronously.

A buffer allocated once and patched every frame with bufferSubData, read back with getBufferSubDataHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  pre { max-width: 600px; margin: 4px 0 0; padding: 6px 8px; background: #fff; border: 1px solid #ddd; }
</style>
<canvas id="view" width="600" height="220"></canvas>
<pre id="out"></pre>
<script>
const gl = document.getElementById('view').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec2 aPos; out vec3 vColor;
void main() {
  vColor = vec3[3](vec3(0.12, 0.37, 0.55), vec3(0.88, 0.6, 0.06), vec3(0.71, 0.27, 0.18))[gl_VertexID % 3];
  gl_Position = vec4(aPos, 0.0, 1.0); gl_PointSize = 9.0;
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec3 vColor; out vec4 c; void main() { c = vec4(vColor, 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 buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, 6 * 4, gl.DYNAMIC_DRAW);   // room for 3 points, zero-filled
gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array([-0.8, -0.8]));   // the first point
gl.bufferSubData(gl.ARRAY_BUFFER, 16, new Float32Array([0.8, -0.8]));   // the third point
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0);

// Writing 8 bytes at offset 20 overflows the 24-byte buffer: INVALID_VALUE, nothing written.
gl.bufferSubData(gl.ARRAY_BUFFER, 20, new Float32Array([1, 2]));
const overflowError = gl.getError();

const back = new Float32Array(6);
const point = new Float32Array(2);
const out = document.getElementById('out');
function frame(ms) {
  const t = ms * 0.0015;
  point[0] = Math.sin(t) * 0.7;
  point[1] = 0.3 + Math.cos(t * 1.3) * 0.5;
  gl.bufferSubData(gl.ARRAY_BUFFER, 8, point);        // patch only the second point, in place
  gl.clearColor(0.97, 0.96, 0.93, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  gl.drawArrays(gl.POINTS, 0, 3);
  if (Math.floor(ms / 250) !== frame.last) {           // read back 4 times a second: it stalls the GPU
    frame.last = Math.floor(ms / 250);
    gl.getBufferSubData(gl.ARRAY_BUFFER, 0, back);
    out.textContent = `getBufferSubData: ${[...back].map(v => v.toFixed(2)).join(', ')}\n` +
      `BUFFER_SIZE ${gl.getBufferParameter(gl.ARRAY_BUFFER, gl.BUFFER_SIZE)} bytes, ` +
      `BUFFER_USAGE DYNAMIC_DRAW: ${gl.getBufferParameter(gl.ARRAY_BUFFER, gl.BUFFER_USAGE) === gl.DYNAMIC_DRAW}\n` +
      `bufferSubData past the end -> getError() = ${overflowError} (INVALID_VALUE)`;
  }
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>