跪拜 Guibai
← All articles
Three.js · React.js

Three.js First-Frame Jank: Why compileAsync Alone Doesn't Fix the Pause

By 鸿蒙开发 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

First-frame jank in WebGL often gets misdiagnosed as a shader compilation problem. This walkthrough shows that terrain math, texture upload, and render-target-specific shader variants are equally likely bottlenecks, and it provides a concrete staging sequence plus the caveats that keep it honest.

Summary

When a Three.js scene stutters on the first frame, shader compilation is the usual suspect, and `renderer.compileAsync` seems like the cure. But in a fishing game with reflections and shadows, the pause persisted even after precompiling. The real work that still crowded the first draw was terrain geometry computation, texture upload to the GPU, and shader programs for the offscreen reflection path. `compileAsync` only prepares programs for the active render target; it does nothing for CPU math or texture initialization.

The fix was to stage everything before the normal render loop starts. Terrain generation got a soft budget: check elapsed time every 256 vertices and yield to the next animation frame if over ~8ms. Textures are deduplicated and initialized with `renderer.initTexture`, one per frame. After precompiling the main scene, the code switches to a 1×1 offscreen render target and precompiles again, because the output color space and program cache key depend on the current target. A `preparing` flag blocks the normal loop from drawing during this warm-up.

The author is explicit about limits: `await nextFrame` only spreads JavaScript scheduling, not GPU work; large textures can still block; and the `KHR_parallel_shader_compile` extension only provides non-blocking status queries, not free compilation. Warm-up on a dev machine doesn't guarantee smoothness on integrated GPUs.

Takeaways
— `compileAsync` precompiles shader programs but does not cover CPU terrain computation, texture upload, or programs for different render targets.
— Break terrain generation into chunks across animation frames using a soft budget — check elapsed time every 256 vertices and yield if it exceeds ~8ms.
— Collect textures into a `Set` and call `renderer.initTexture` on each one, awaiting next frame between calls, to spread uploads.
— `await requestAnimationFrame` only defers JavaScript execution to the next frame; it does not wait for GPU upload or compilation to finish.
— Precompile the scene once with the main render target, then switch to a 1×1 offscreen target and precompile again, because the render target affects the program cache key.
— Shadow depth materials require separate handling for alpha maps and alpha test settings; create temporary proxies and keep them until the first real shadow draw.
— During warm-up, use a `preparing` flag to stop the normal render loop from drawing the unfinished scene.
— Warm-up cannot guarantee zero jank on all devices; GPU buffer uploads and model processing can still create long tasks.
Conclusions

Render target selection influences shader program caching, so a single `compileAsync` call can miss offscreen paths entirely — the 1×1 target trick is a cheap way to force those programs.

The soft budget pattern for terrain generation is a practical compromise: it smooths the main thread but doesn't guarantee a hard frame limit because synchronous operations remain.

Using `await nextFrame` between texture uploads is a JavaScript-level yield; it prevents one giant synchronous block, but the GPU work can still accumulate and cause a later stall.

Concepts & terms
compileAsync
Three.js method that precompiles shader programs for a scene and camera, typically before the first render, and can run asynchronously using the KHR_parallel_shader_compile extension.
initTexture
Renderer method that initializes a texture on the GPU ahead of time, avoiding a burst of upload work during the first render.
KHR_parallel_shader_compile
WebGL extension that makes shader compilation status queries non-blocking, letting applications poll instead of blocking the main thread.
offscreen render target
A framebuffer that isn't displayed directly; used here to compile shader programs specific to an offscreen reflection pass.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗