Chapter 100: Mapping Web APIs to Pipeline
Web 图形程序的难点在于,开发者看到的是 fetch()、createImageBitmap()、gl.texImage2D()、device.createBuffer()、device.queue.writeBuffer()、requestAnimationFrame() 这些 Web API 调用,实际成本却落在资源解码、CPU 到 GPU 上传、pipeline 创建、命令录制、队列提交、浏览器合成和显示刷新上。读完本章后,读者应能把一个 Web 页面中的图形调用追踪到渲染流水线阶段,并判断卡顿、首帧慢、贴图闪烁、提交频繁或资源泄漏分别发生在哪条路径上。
本章使用一个贯穿材料:网页加载一个带纹理的旋转模型,在 canvas 上持续渲染。这个页面需要从网络取得模型和图片,解码成 CPU 可读数据,创建 GPU buffer 和 texture,创建 shader 与 pipeline,进入每帧命令录制,最后提交到 GPU 并交给浏览器呈现。WebGL 与 WebGPU 的 API 形态不同,但这条路径中的阶段相同:数据先进入浏览器进程和 JavaScript 堆,再进入 GPU 资源,再由命令流驱动图形管线。
本文以 WebGPU 和 WebGL 的公开 Web API 文档为语境。WebGPU 的对象模型可参考 MDN WebGPU API,资源上传可参考 MDN GPUQueue.writeBuffer,WebGL 的资源释放与性能建议可参考 MDN WebGL best practices。这些资料提供 API 事实;正文把这些 API 调用整理成可迁移的 pipeline 判断顺序。
100.1 将 Web API 调用映射到渲染流水线阶段
Web API 调用需要先按阶段归位。fetch() 解决网络传输,Response.arrayBuffer() 解决字节落地,图片解码解决 CPU 侧像素可用性,GPU upload 解决资源进入 GPU 可访问内存,pipeline creation 解决 shader、绑定布局和固定功能状态组合,command encoding 解决本帧要执行的 draw 或 dispatch,queue submit 解决命令交给 GPU,present 解决渲染结果进入浏览器合成与显示。
这条路径可以用一个旋转模型帧来观察。页面首帧前,模型顶点和索引需要成为 GPUBuffer 或 WebGL buffer;图片需要成为 GPUTexture 或 WebGL texture;shader 需要编译成可被 pipeline 使用的程序;pipeline 或 WebGL program 需要和输入布局、深度状态、颜色目标格式匹配。每帧阶段,JavaScript 更新相机矩阵或模型矩阵,写入 uniform buffer 或 WebGL uniform,编码 draw 命令,提交给 GPU。画面出现在屏幕上时,浏览器还会把 canvas 结果纳入 compositor 合成。
下面的图只描述从资源加载到第一帧显示的主路径,省略缓存、错误重试和多 pass 渲染。图中的每个节点都对应一个可观察对象:网络请求、CPU 数据、GPU 资源、pipeline、命令缓冲、swapchain 或当前画布纹理。
这张图的核心判断是:Web API 的调用顺序与 GPU 执行顺序存在两个分界。第一个分界在 CPU 数据到 GPU 资源之间,上传动作会消耗带宽,并可能引入额外复制。第二个分界在命令录制到 queue submit 之间,JavaScript 调用只是构造和提交命令,GPU 按队列和依赖关系执行。排查性能时,应先定位卡在加载、上传、pipeline 创建、命令提交、GPU 执行还是呈现合成。
WebGPU 的对象命名让这条路径更显式。GPUDevice 创建长期对象,例如 buffer、texture、sampler、bind group、shader module 和 render pipeline。GPUCommandEncoder 录制一次或一批命令。GPURenderPassEncoder 表达当前 render pass 的 pipeline、绑定和 draw。GPUQueue 接收 submit(),也承担 writeBuffer() 这类上传入口。MDN 的 WebGPU 结构说明把创建 shader、配置 canvas、创建资源、创建 pipeline、运行 pass、提交 queue 放在同一条应用结构中,工程上也应按这个顺序组织初始化与每帧逻辑。
WebGL 的对象模型更隐式,但映射关系仍然成立。gl.createBuffer()、gl.bufferData() 和 gl.vertexAttribPointer() 对应 buffer 创建、数据上传与 vertex input 描述;gl.createTexture()、gl.texImage2D() 和 sampler 参数对应 texture 上传与采样状态;gl.createProgram()、gl.linkProgram() 和 gl.useProgram() 对应 shader program 与 pipeline 部分状态;gl.drawElements() 或 gl.drawArrays() 对应 draw command。WebGL 的状态机把大量状态保存在 context 中,WebGPU 把状态显式放进 pipeline、bind group 和 pass encoder 中。
一个可复用的映射顺序是:先找数据来源,再找 GPU 资源,再找绑定关系,再找 pipeline,再找 draw 或 dispatch,再找提交和呈现。只要某个 API 调用能回答其中一个问题,它就属于对应阶段。fetch() 回答“数据从哪里来”;createBuffer() 回答“数据进入哪类 GPU 资源”;createBindGroup() 回答“shader 通过哪个 binding 读取资源”;createRenderPipeline() 回答“vertex 与 fragment 阶段采用哪组状态”;drawIndexed() 回答“本帧消耗多少几何”;submit() 回答“命令何时交给 GPU”。
这套映射还可以解释常见画面问题。模型位置错误通常先查 uniform 或 vertex input,属于数据绑定和 shader 输入阶段。贴图黑块通常先查图片解码、texture 格式、sampler、bind group 或 WebGL texture 参数,属于资源上传和采样阶段。首帧等待时间长通常先查资源 fetch、decode、shader 编译、pipeline 创建和首次上传。帧率下降则继续分成 CPU 侧 JavaScript、API validation、command encoding、GPU shader、texture bandwidth、overdraw 和浏览器合成。
100.2 Web Graphics Buffer Texture Lifecycle and State Tracking
Web 图形资源的生命周期从 CPU 可见数据开始,到 GPU 对象释放或设备丢失结束。Buffer、texture、sampler 和 bind group 的生命周期应由资源所有权、用途标记、上传时机、引用关系和失效恢复共同决定。Web 页面长期运行时,资源生命周期管理直接影响内存峰值、GC 压力、GPU 内存占用和恢复能力。
Buffer 的工作定义是:一段可被 GPU 读取或写入的线性数据存储。顶点、索引、uniform、storage 数据都可以放入 buffer,但它们的 usage、更新频率和绑定位置不同。静态顶点 buffer 通常在资源加载后一次上传;每帧更新的 uniform buffer 需要环形分配或按帧分块;storage buffer 可能被 compute pass 写入,再被 render pass 读取。WebGPU 用 usage flags 表达用途,WebGL 通过 binding target 和调用路径间接表达用途。
Texture 的工作定义是:二维、三维、数组或 cube 形式的 GPU 图像资源。它既可以来自图片解码,也可以来自离屏 render target,还可以作为 storage texture 参与 compute。采样纹理需要 texture view 和 sampler;渲染目标纹理需要匹配 color attachment 或 depth attachment;当前画布纹理来自 GPUCanvasContext.getCurrentTexture() 或 WebGL 的默认 framebuffer。贴图问题的排查应同时检查像素来源、格式、尺寸、mipmap、采样器、绑定位置和 shader 读取坐标。
Sampler 的生命周期经常被低估。Sampler 表达过滤、寻址和比较采样状态。相同过滤策略和地址模式可以共享 sampler,频繁为每张贴图创建 sampler 会增加对象数量和绑定管理成本。Bind group 则把 buffer、texture view 和 sampler 组合成 shader 可访问的资源集合。WebGPU 中 bind group 必须和 pipeline layout 兼容;WebGL 中相同关系分散在 active texture、uniform sampler、buffer binding 和 vertex attribute 状态里。
下面的状态图把资源从 CPU 数据到可绘制对象的生命周期展开。它适合用来设计资源管理器,也适合用来排查首帧和设备丢失后的恢复路径。
这个状态图的关键边界在于“引用可见”和“资源可用”并非同一件事。JavaScript 变量仍然引用一个对象,只说明脚本还能访问该句柄;GPU 侧资源是否仍可用,取决于 context 或 device 状态、浏览器实现和对象生命周期。WebGL best practices 提到主动删除不再使用的对象,原因也在这里:API 层释放句柄可以让实现更早回收底层资源,实际释放时间仍受 GPU 使用状态约束。
资源跟踪表应按资源类型记录最小字段。Buffer 至少记录 size、usage、更新频率、绑定槽位、最后上传帧和所有者。Texture 至少记录 width、height、format、mipmap、usage、sampler key、最后使用帧和来源 URL 或生成 pass。Pipeline 至少记录 shader key、vertex layout、bind group layout、color format、depth format、blend state 和 sample count。Bind group 至少记录 layout、资源引用、版本号和失效条件。
资源失效来自四类来源。第一类是浏览器 context 或 WebGPU device lost,所有 GPU 对象需要重新创建。第二类是画布尺寸、device pixel ratio 或颜色格式变化,swapchain 或当前画布纹理需要重新配置。第三类是材质或 shader variant 改变,bind group layout 或 pipeline key 需要更新。第四类是资源容量变化,例如动态 mesh 顶点数超过原 buffer size,需要重新分配 buffer 并更新所有引用。
上传路径应和更新频率绑定。静态资源使用加载阶段批量上传;中频资源使用 staging 或 queue 写入;每帧小块 uniform 使用 ring buffer 或动态 offset;大纹理上传应切块并和渲染调度协调。GPUQueue.writeBuffer() 的文档把它描述为把数据源写入 GPUBuffer 的便利入口,并允许 user agent 选择合适复制方式。工程判断上,这个入口适合小到中等规模的数据更新;连续大块上传仍需考虑分帧、压缩、格式和缓存策略。
WebGL 与 WebGPU 的状态跟踪粒度不同。WebGL 需要记录当前绑定状态,因为后续调用依赖 context 的隐式状态;WebGPU 需要记录对象兼容性,因为 bind group、pipeline 和 pass 之间的布局必须匹配。工程层面可以把两者抽象成同一张资源依赖图:asset 产生 resource,resource 进入 binding,binding 进入 pipeline,pipeline 进入 pass,pass 产生 frame output。
100.3 构建从加载到渲染的完整流程
完整流程的目标是让资源、pipeline 和每帧调度形成稳定依赖。对贯穿材料中的旋转模型页面,可以把流程拆成六个阶段:能力查询、资源加载、CPU 解码、GPU 上传、pipeline 准备、帧循环调度。每个阶段都输出下一阶段需要的对象,并记录失败时的 fallback。
能力查询阶段回答“当前浏览器和设备能使用哪条渲染后端”。WebGPU 路径先检查 navigator.gpu,请求 adapter 和 device,再读取必要 feature、limit 和 canvas format。WebGL 路径先取得 webgl2 或 webgl context,再查询 extension、最大 texture 尺寸、uniform 限制和精度。能力查询结果应成为 renderer profile,后续材质、纹理格式、后处理规模和 fallback 都基于这个 profile。
资源加载阶段回答“需要哪些资产以及何时到达”。模型、贴图、shader 源码和配置可以并行请求,但进入 GPU 上传前需要满足各自依赖。模型 bytes 需要解析成顶点、索引、材质引用和包围盒;图片 bytes 需要解码成 ImageBitmap、HTMLImageElement 或可上传像素;shader 文本需要进入编译或模块创建。加载阶段应输出 CPUData,draw 阶段只消费已经满足依赖的资源。
GPU 上传阶段回答“CPUData 如何变成 GPU 可读资源”。顶点数组创建 vertex buffer,索引数组创建 index buffer,材质常量创建 uniform buffer,图片创建 texture 并写入 mip 0,必要时生成 mipmap 或预先加载多级纹理。WebGPU 中应让 usage flags 覆盖真实用途,例如顶点 buffer 需要 VERTEX 和上传目标;纹理若作为 shader 采样源,需要 texture binding 相关用途。WebGL 中应在上传后设置 filtering、wrap 和 mipmap 条件,使 texture 完成状态满足采样要求。
Pipeline 准备阶段回答“资源如何进入 shader 并完成固定功能配置”。WebGPU 的 render pipeline 需要 shader module、entry point、vertex buffer layout、primitive、depth/stencil、multisample、fragment target format 和 pipeline layout。WebGL 的 program 需要编译、链接、attribute 位置、uniform 位置和相关 GL state。pipeline key 应包含所有影响兼容性的字段,尤其是 shader variant、目标格式、深度状态、blend state、sample count 和 vertex layout。
帧循环阶段回答“每个刷新周期提交哪些变化”。requestAnimationFrame() 提供与浏览器刷新协作的入口,回调中通常更新时间、相机、动画、可见集、uniform,再录制本帧命令。WebGPU 需要每帧取得当前 canvas texture view,打开 render pass,设置 pipeline、bind group、vertex/index buffer,调用 draw,结束 pass,finish command buffer 并 submit。WebGL 则在当前 context 状态下设置 framebuffer、viewport、program、buffer、texture 和 uniform 后调用 draw。
下面的简化代码只展示 WebGPU 每帧映射关系。代码中的 scene、pipeline、frameBindGroup 和 mesh 来自初始化阶段;这里的重点是每帧 API 调用如何落到 render pass、资源绑定、draw 和 submit。
function renderFrame(time) {
updateCameraUniform(scene.camera, time);
device.queue.writeBuffer(scene.cameraBuffer, 0, scene.cameraBytes);
const encoder = device.createCommandEncoder();
const colorView = context.getCurrentTexture().createView();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: colorView,
loadOp: "clear",
clearValue: { r: 0.02, g: 0.02, b: 0.03, a: 1.0 },
storeOp: "store",
}],
depthStencilAttachment: scene.depthAttachment,
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, frameBindGroup);
pass.setVertexBuffer(0, mesh.vertexBuffer);
pass.setIndexBuffer(mesh.indexBuffer, "uint32");
pass.drawIndexed(mesh.indexCount);
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(renderFrame);
}
这段代码能映射出五个检查点。writeBuffer() 是 CPU 到 GPU 的本帧小数据上传;getCurrentTexture() 连接当前显示目标;beginRenderPass() 建立本帧颜色和深度附件;setPipeline()、setBindGroup() 和 buffer 设置建立 shader 输入;drawIndexed() 与 submit() 形成 GPU 工作。若这帧卡顿,排查顺序应先看 uniform 上传频率和大小,再看 pipeline 是否在帧内创建,再看 bind group 是否重复创建,再看 draw 数量和 shader 成本,再看 canvas 尺寸与浏览器合成。
Fallback 应在流程设计阶段写清楚。WebGPU 初始化失败时可以降级到 WebGL2;高精度纹理格式不可用时可以改用较低精度格式;大贴图超过限制时可以选择缩放版本;shader 编译失败时可以使用 debug material;资源请求失败时可以使用占位 mesh 或占位 texture。Fallback 的判断依据来自能力查询和错误对象,draw 阶段只执行已经确定的降级路径。
错误处理要与阶段绑定。网络失败属于 asset 阶段,输出缺失资源;解码失败属于 CPUData 阶段,输出格式错误;validation error 属于 API 调用约束,通常指向 usage、layout、format 或尺寸;pipeline error 属于 shader 和 pipeline 描述;device lost 或 context lost 属于全局 GPU 资源失效。错误日志只有放回阶段,才可以指导重试、降级或重新创建。
100.4 异步调度与事件循环机制影响
Web 图形流程运行在浏览器调度体系内。网络请求、Promise microtask、图片解码、JavaScript 主线程任务、worker 消息、requestAnimationFrame()、GPU 命令提交和浏览器合成都共享时间预算。图形程序的稳定性取决于是否把异步结果收敛到明确阶段,并把每帧主线程工作控制在刷新周期内。
事件循环对贯穿材料的影响可以从首帧开始观察。fetch() 完成后回调进入 JavaScript;图片解码可能异步完成;pipeline 创建在 WebGPU 中可以采用同步创建或 async 创建;资源上传会从 JS 数据进入浏览器内部队列;requestAnimationFrame() 回调在浏览器准备绘制下一帧前运行。若资源到达与帧循环交错,renderer 需要用显式状态表达“资源未就绪、资源上传中、资源可绘制”。
Promise 的完成顺序不等同于画面可绘制顺序。模型先到、图片后到时,材质 bind group 还缺 texture view 和 sampler;图片先到、pipeline 后到时,texture 已经 resident,但 draw 仍缺 pipeline;pipeline 已经创建、uniform buffer 容量不足时,帧循环仍需等待 buffer 重建。每个异步任务完成后,只应推进自己负责的状态,并由调度器检查整套依赖是否满足。
主线程预算需要按任务类型拆分。输入处理、DOM、布局、脚本、图形命令录制和浏览器合成都可能占用同一帧时间。Web 图形页面若在 rAF 回调中解析大型 JSON、同步创建大量纹理、编译多个 shader 或重建大量 pipeline,就会把本该用于命令录制的时间消耗在资源准备上。更稳的方式是把加载、解析、上传和绘制拆成阶段,并为每帧资源处理设置上限。
Worker 与 OffscreenCanvas 可以改变调度位置。把解析、物理、可见性计算或图形 context 移到 worker,可以降低主线程与 UI、输入、DOM 的竞争。这个设计的收益来自任务隔离和消息边界,代价是数据传递、所有权转移、调试复杂度和浏览器支持差异。适合放入 worker 的任务具有两个特征:输入输出边界清楚,和 DOM 交互少。
异步调度还影响 GPU 与 CPU 的重叠。CPU 录制命令时,GPU 可能仍在执行上一帧;GPU 执行本帧时,CPU 可能开始准备下一帧。这个重叠能隐藏一部分延迟,但 readback、同步查询、过早等待结果、频繁读取 GPU 状态会破坏重叠。WebGL best practices 中提到生产路径控制阻塞式调用,背后的原因正是同步等待会把浏览器、驱动和 GPU 队列串成一个长等待链。
一个实用的调度器可以使用三类队列。第一类是 asset queue,处理网络与解码;第二类是 upload queue,把已解码资源分帧上传;第三类是 frame queue,只保留当前帧需要的状态更新和 draw。三类队列共享一个资源表,但各自只修改自己负责的状态字段。这样做可以把“资源是否到达”和“本帧是否绘制”解耦。
下面的伪代码展示资源状态推进。它不依赖具体 WebGPU 或 WebGL 后端,重点是把异步完成收敛到状态表,再由帧循环消费可用资源。
const ResourceState = {
Requested: "requested",
Decoded: "decoded",
Uploaded: "uploaded",
Ready: "ready",
Failed: "failed",
};
async function loadTexture(record) {
try {
const response = await fetch(record.url);
const blob = await response.blob();
record.bitmap = await createImageBitmap(blob);
record.state = ResourceState.Decoded;
uploadQueue.push(record.id);
} catch (error) {
record.state = ResourceState.Failed;
record.error = error;
}
}
function processUploadBudget(maxUploads) {
for (let i = 0; i < maxUploads && uploadQueue.length > 0; i += 1) {
const record = resources.get(uploadQueue.shift());
uploadTextureToGPU(record);
record.state = ResourceState.Ready;
}
}
这个例子的结论是:异步任务完成只改变资源状态,渲染循环根据状态选择真实材质或 fallback 材质。这样可以让首帧先显示可用内容,再逐步替换高清贴图或复杂材质。它也让错误路径更清楚:失败资源不会进入上传队列,上传成功资源才进入 bind group 创建,ready 资源才参与 draw。
100.5 性能优化方法与最佳实践
Web 平台优化应先定位瓶颈类型,再选择策略。贯穿材料中的旋转模型页面可能受加载延迟、CPU 解析、上传带宽、shader 编译、pipeline 创建、API 调用次数、draw 数量、纹理带宽、fragment 过量绘制、GC pause、canvas 像素数或浏览器合成影响。相同帧率下降现象背后可能是完全不同的资源路径。
首帧优化的重点是缩短 critical path。critical path 包含首帧必须等待的请求、解码、上传、pipeline 和最小 draw。模型可以拆分为低模和高模,贴图可以先使用小尺寸版本,shader 可以预编译常用 variant,pipeline 可以在加载阶段创建,canvas 首帧可以先绘制 loading pass。关键判断是:首帧只保留能产生稳定画面的最小资源集合,其他资源进入后台加载和替换路径。
上传优化的重点是减少重复复制和峰值内存。静态资源集中上传后复用;动态 uniform 使用连续内存和按帧偏移;大纹理分块上传,并限制同一帧上传数量;图片优先使用浏览器可高效解码和上传的格式;顶点属性使用合理量化和交错布局。WebGPU 中 writeBuffer() 适合把 typed array 写入目标 buffer,WebGL 中 bufferSubData() 适合更新已有 buffer 的局部范围。频繁重新分配大 buffer 会带来内存和调度成本。
Pipeline 优化的重点是控制 variant 数量和帧内创建。WebGPU 的 pipeline 创建需要 shader、layout 和固定状态组合;WebGL 的 program link 和状态切换也有成本。材质系统应把影响 shader 编译的宏、影响 pipeline 的格式与 blend state、影响绑定的 bind group layout 分开记录。帧循环内应使用已创建 pipeline,材质切换应按 pipeline、bind group 和 texture 局部性排序。
命令提交优化的重点是控制 JavaScript 调用密度。大量小 draw、频繁状态切换、每帧重建 bind group、每对象单独上传 uniform,都会增加 CPU 侧提交成本。WebGPU 可以用更明确的 bind group、render bundle、indirect draw 或 compute culling 组织大批量绘制;WebGL 可以通过 batching、instancing、VAO 复用和 texture atlas 降低调用数量。策略选择取决于瓶颈位于 CPU 提交、GPU 顶点、GPU fragment 还是带宽。
Canvas 像素数需要纳入预算。CSS 尺寸、device pixel ratio 和实际 back buffer 尺寸共同决定 fragment 工作量和 render target 带宽。高 DPR 设备上,全分辨率后处理可能迅速放大成本。可复用的判断方式是先记录 canvas drawing buffer 尺寸,再估算颜色、深度、MSAA 和后处理 render target 的像素量。若瓶颈来自 fragment 或带宽,降低内部渲染分辨率通常比微调 JavaScript 更直接。
GC 与内存抖动也属于 Web 图形性能路径。每帧创建大量临时数组、临时对象、字符串 key、命令描述对象和材质包装对象,会增加 GC pause 风险。优化方向是复用 typed array、复用 descriptor 模板、用数字 id 或稳定 key 管理资源、把 per-frame 临时数据放入 frame allocator。这里的目标是降低主线程不可预测停顿,让 rAF 回调保持稳定。
浏览器合成阶段需要单独观察。canvas 可能与 DOM、CSS transform、视频、透明层和滚动区域一起参与 compositor。透明 canvas、频繁 resize、CSS filter、复杂 overlay 都可能把渲染结果带入额外合成成本。若 GPU pass 时间稳定但用户看到掉帧,应继续检查主线程长任务、compositor、layout、paint 和 layer 合成,draw call 只是其中一个观察点。
一个稳定的性能检查顺序如下。第一步记录 frame time,把 CPU rAF 时间、GPU pass 时间和 present 观察分开。第二步固定 canvas 分辨率,排除 DPR 与 resize 干扰。第三步关掉资源上传,判断是否为加载和上传成本。第四步使用简单 shader 和单色材质,判断 fragment、texture 或材质成本。第五步合并 draw 或禁用部分对象,判断 CPU 提交和几何规模。第六步观察内存曲线和 GC,判断对象创建与资源释放。第七步再回到真实画面,逐步恢复功能并记录哪一步引入成本。
这套顺序的价值在于它按数据路径排查。先分离 CPU、GPU 和 browser compositor,再逐步收窄到 upload、pipeline、draw、shader、texture 和 memory。Web 图形优化常见失败原因是直接修改 shader 或压缩贴图,却没有确认瓶颈所在阶段。稳定做法是先把 Web API 调用映射到 pipeline,再用最小改动验证瓶颈假设。
最小自检任务
给定一个 WebGPU 页面:它加载一个 glTF 模型和一张 4096×4096 贴图,首帧需要 2 秒,之后每隔几秒出现一次明显卡顿。代码中每帧都会调用 device.createRenderPipeline()、device.createBindGroup()、device.queue.writeBuffer(),并在 rAF 回调中检查资源是否加载完成。请把这些调用映射到渲染流水线阶段,并给出排查顺序。要求说明哪些问题属于加载路径,哪些属于 GPU 资源路径,哪些属于每帧提交路径。
答案要点
首帧 2 秒应先拆成网络 fetch、glTF 解析、图片解码、4096×4096 贴图上传、shader 编译和 pipeline 创建。模型和贴图属于 asset 与 CPUData 阶段;贴图上传和 buffer 创建属于 GPU resource 阶段;pipeline 创建属于 pipeline preparation 阶段;首帧 draw 与 submit 属于 frame 阶段。排查时先记录每个阶段耗时,并确认首帧是否等待了高分辨率贴图和完整 pipeline。
每隔几秒卡顿应优先检查帧循环内的对象创建和上传。每帧调用 device.createRenderPipeline() 表示 pipeline 创建进入了 frame path,应把 pipeline key 缓存到初始化或材质变体准备阶段。每帧调用 device.createBindGroup() 需要检查资源版本是否真的变化;若 texture、sampler 和 buffer binding 稳定,应缓存 bind group。每帧调用 writeBuffer() 可以合理用于小块 uniform 更新,但需要检查写入大小、频率和 buffer 分配方式。
rAF 回调中的资源检查应只读取资源状态,不应在同一帧无限制执行大量上传、解码后处理或 pipeline 创建。更稳的顺序是:加载任务把资源推进到 Decoded,upload queue 按预算推进到 Uploaded,pipeline cache 按 key 创建并复用,frame loop 只消费 Ready 资源并提交 draw。若卡顿仍存在,再检查 canvas 像素数、draw 数量、shader 采样、GC pause 和浏览器合成。
本章知识点总结
- 阶段映射:Web 图形 API 调用应归入加载、解码、上传、pipeline、命令录制、队列提交和呈现阶段。
- 贯穿路径:一个模型从 URL 到屏幕,需要先成为 CPU 数据,再成为 GPU 资源,最后由 draw 命令驱动显示。
- 资源边界:Buffer、texture、sampler 和 bind group 的生命周期由用途、上传、引用、释放和失效恢复共同决定。
- 状态跟踪:资源管理器应记录 size、format、usage、binding、版本号、最后使用帧和失效条件。
- 上传策略:静态资源适合加载阶段上传,动态小数据适合按帧写入,大纹理和大 buffer 需要预算控制。
- Pipeline 缓存:Shader、layout、目标格式、深度状态、blend state 和 vertex layout 应组成稳定 pipeline key。
- 帧循环职责:rAF 回调应承担状态更新、命令录制和提交,加载、解码和大量创建应进入独立阶段。
- 异步收敛:Promise、worker 和上传队列应通过资源状态表收敛到 Ready、Failed 或 Lost 等明确状态。
- 设备失效:Context 或 device lost 会使 GPU 对象失效,恢复路径需要从 CPUData 或 asset cache 重新创建资源。
- 提交成本:大量小 draw、频繁状态切换和帧内重建对象会增加 JavaScript 与 API validation 成本。
- 像素预算:Canvas 实际绘制尺寸、DPR、MSAA、深度和后处理目标共同决定 fragment 与带宽压力。
- 排查顺序:性能分析应先分离 CPU、GPU 和浏览器合成,再定位 upload、pipeline、draw、shader、texture 或 memory。