It is impossible to get an accurate FPS measurement without using await renderer.waitForGPU(). Why doesn't renderer.renderAsync() already do this internally?

Here I have a simple FPS counter that displays the current frame and the FPS counted from the last second:

let globalFrames = 0;
let intervalFrames = 0;
let lastTimestamp = performance.now();
requestAnimationFrame(stepAsync);

async function stepAsync(now) {
    if ((now - lastTimestamp) >= 1000) {
        const averageDt = (now - lastTimestamp) / intervalFrames;
        const fps = 1000 / averageDt;
        document.getElementById("fpsDiv").innerHTML = fps.toString();
        intervalFrames = 0;
        lastTimestamp = now;
    } else {
        intervalFrames++;
    }
    globalFrames++;
    document.getElementById("frameDiv").innerHTML = globalFrames.toString();
    await renderer.renderAsync(scene, camera);
    requestAnimationFrame(stepAsync);
}

Although my application is much more complex than this in reality, this is the core of how I animate my three.js scene.

Recently I was noticing a general lag with my three.js scene, even though the FPS that I measured was 100+. I knew something wasn’t right.

While debugging, I noticed that renderer.renderAsync() was returning before the image in the <canvas> had updated for that frame. This was causing stepAsync() to be called again before the last call had finished, resulting in frames “backing up” and being dropped. This surprised me, as I thought renderer.renderAsync() was already supposed to be blocking until the <canvas> had updated.

After much research, I discovered the method renderer.waitForGPU(), which I added right after renderer.renderAsync(). This addition ensured that stepAsync was only called when the previous frame had truly been rendered, and fixed my FPS counter. My FPS counter finally showed the accurate FPS for the scene, which was around 30.

tldr; the fix was simply to add renderer.waitForGPU() after renderer.renderAsync():

await renderer.renderAsync(scene, camera);
await renderer.waitForGPU();

Why doesn’t await renderer.renderAsync() already wait for await renderer.waitForGPU() internally? If a developer truly wanted to send something off for rendering and not block at that line, they can just omit the await keyword when calling renderer.renderAsync().