Each device has one queue, device.queue (Encoders and Queues). Besides submit() and onSubmittedWorkDone(), it offers shortcuts that need no encoder: writeBuffer(), writeTexture() and copyExternalImageToTexture(). Its key guarantee is ordering: writes and submitted command buffers execute in the order you issue them, as this copy of one buffer before and after a price change shows:
<script type="module">
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
const { COPY_SRC, COPY_DST, MAP_READ } = GPUBufferUsage;
const price = device.createBuffer({ size: 4, usage: COPY_SRC | COPY_DST });
const before = device.createBuffer({ size: 4, usage: COPY_DST | MAP_READ });
const after = device.createBuffer({ size: 4, usage: COPY_DST | MAP_READ });
const copyTo = (dst) => { const e = device.createCommandEncoder();
e.copyBufferToBuffer(price, 0, dst, 0, 4); return e.finish(); };
device.queue.writeBuffer(price, 0, new Float32Array([14.99])); // The Quiet Harbor
device.queue.submit([copyTo(before)]);
device.queue.writeBuffer(price, 0, new Float32Array([12.74])); // 15% off
device.queue.submit([copyTo(after)]);
await Promise.all([before.mapAsync(GPUMapMode.READ), after.mapAsync(GPUMapMode.READ)]);
const read = (b) => new Float32Array(b.getMappedRange())[0].toFixed(2);
console.log(`first copy saw ${read(before)}, second copy saw ${read(after)}`);
</script>first copy saw 14.99, second copy saw 12.74
Batch a frame's command buffers into one submit([...]), since each submission has a fixed cost.