rAF与提交
一帧的真相是提交,不是「又调了一次 drawArrays」。顺序写死,热路径才可测。
1. 一帧顺序
requestAnimationFrame(timestamp)
1. 更新 CPU 场景(积分、剔除、脏标记)
2. 只上传脏 buffer / 脏纹理 / 脏 uniform
3. bind 状态 → draw* / encoder.finish() → queue.submit()
4. (可选)读回:1px 拾取或 FBO blit;默认跳过
合成器取走 drawing buffer(preserveDrawingBuffer: false 时可被清空)
export interface GpuTick {
now: number;
dt: number;
}
export interface GpuFrameLoop {
maxDt: number;
update: (tick: GpuTick) => void;
uploadDirty: () => void;
draw: () => void;
readback?: () => void;
}
export function startGpuLoop(loop: GpuFrameLoop): () => void {
let id = 0;
let last: number | undefined;
let running = true;
const frame = (now: number): void => {
if (!running) return;
if (document.visibilityState === "hidden") {
last = undefined;
id = requestAnimationFrame(frame);
return;
}
const raw = last === undefined ? 0 : (now - last) / 1000;
last = now;
const dt = Math.min(Math.max(raw, 0), loop.maxDt);
loop.update({ now, dt });
loop.uploadDirty();
loop.draw();
loop.readback?.();
id = requestAnimationFrame(frame);
};
const onVis = (): void => {
if (document.visibilityState === "visible") last = undefined;
};
document.addEventListener("visibilitychange", onVis);
id = requestAnimationFrame(frame);
return () => {
running = false;
cancelAnimationFrame(id);
document.removeEventListener("visibilitychange", onVis);
};
}
maxDt 取 1 / 20~1 / 30。后台恢复的大间隙不夹紧,相机会瞬移。hidden 时不要继续 bufferSubData——白烧带宽,恢复时丢 last 重新积分。时间基准细则与 Canvas rAF 相同:插值用 rAF timestamp,测本帧耗时用 performance.now(),禁用 Date.now()。
2. 上传只碰脏区
CPU 场景图是 source of truth。GPU buffer 是投影。物体挪了,改矩阵或 instance 数组再 bufferSubData;没脏的 mesh 不要每帧 bufferData 整包。
export function uploadF32(
gl: WebGL2RenderingContext,
buffer: WebGLBuffer,
data: Float32Array,
dirty: boolean,
): void {
if (!dirty) return;
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, data);
}
适用:每帧变的 instance transform、粒子位置。
不适用:静态索引、静态 UV——创建时 bufferData 一次,丢失恢复时再走资源表。
WebGPU 对应 queue.writeBuffer 或 mapping 双缓冲。不要每帧 createBuffer。
3. 不要在 rAF 里 compile
compileShader、linkProgram、getShaderInfoLog、WebGPU createRenderPipeline 都是 CPU 尖刺,还可能隐式 flush。变体在启动或热更新时编完,按 key 缓存 WebGLProgram / GPURenderPipeline。运行时缺变体:本帧降级已有 pipeline,下一帧空闲再编,不要卡在 draw 前面。
WebGPU 用 createRenderPipelineAsync,完成前走 WebGL2 或上一个 pipeline。
gl.getError() 同样会同步。调试开,生产关。
4. 读回放在 draw 之后,且要克制
默认 preserveDrawingBuffer: false:rAF 返回、合成器拿走之后,默认 framebuffer 内容可以没了。readPixels 必须:
- 对着 当前帧刚画完的 默认缓冲,仍在本次 rAF 内;或
- 对着 FBO(自己的纹理,不受合成清空约束)。
热路径禁止 toDataURL / 全屏 readPixels。拾取最多 1px,见 读回与同步陷阱。不需要读回就不要设 readback。
5. 失败形态
| 症状 | 原因 |
|---|---|
| 切回标签页相机飞走 | dt 未夹紧 |
| 前几秒掉帧、之后稳 | shader / pipeline 在第一帧 rAF 里编译 |
| 截图或拾取全黑 / 全 0 | 跨帧读默认缓冲,且未 preserve |
| 越跑越卡 | 每帧 bufferData 整包或每帧 createTexture |
| 后台耗电 | hidden 仍上传 + draw |
权威资料
- HTML — Animation frames
- WebGL — Drawing Buffer / preserveDrawingBuffer
- Khronos — WebGL 2.0
- W3C — WebGPU submit
核对日期:2026-08-26