Search posts, tags, users, and pages
Your 3D scene runs smoothly on your machine. Sixty frames, no jank. You drop the page into PageSpeed Insights and get a 48. Total Blocking Time is red, and the main-thread breakdown shows one long tas
grodev.hashnode.dev7 min read
Markdown supported · ⌘↵ to post
Yeah, the way you’re testing it is basically what I landed on too. Cold cache, treat the first render and the first interaction as two separate things, and watch if anything new compiles when you poke it. That warm initial scene is exactly what fools you.
The thing that unlocked it for me was realizing I was thinking about it wrong. I was trying to cover configurations and there are thousands of those. But the GPU doesn’t care about configurations, it cares about programs, and there’s only a handful. Twenty wood finishes that are all the same MeshStandardMaterial with a different texture are one program, not twenty. So I stopped enumerating configs and started enumerating the stuff that actually changes the shader: the material types I switch between, the lighting presets that change light count or the env map, and whether the post stack with bloom and tone mapping is on or off.
That list is short. During the loading screen I just step through it, set each combo, call compileAsync. The one that bit me was the EffectComposer passes. renderer.compile doesn’t touch them, you have to actually render a frame through the composer with the passes enabled, otherwise they compile on the first interaction and you get exactly the hitch you’re describing.
And to your real question, how do you know you covered them without brute forcing every config, I just watch renderer.info.programs.length. Warm up, then click around like an annoying user, switch finishes, flip lighting. If that number goes up, you missed one, add it. When it stops moving no matter what you click, you’re done. Never had to render the whole matrix.
One more that caught me once, if you use onBeforeCompile or a custom cache key anywhere those are their own programs too, so they need to be in the warm-up or they’ll surprise you on first interaction.
That’s a useful way to narrow the warm-up down. One thing I’d be careful with, though: a stable renderer.info.programs.length tells you that the program count stopped growing, not necessarily that every interaction is hitch-free.
For example, switching a configurator from a standard wood finish to a transmission-heavy glass or coated-metal material can introduce texture uploads, render-target work, or other GPU resource initialization even if the shader program is already cached. Those costs can still show up in the first interaction without increasing the program count.
I’d keep your program-count check, but pair it with frame-time profiling around the first material switch and the first composer render, ideally on a cold device cache. That helps distinguish a missed shader variant from a resource-upload or post-processing bottleneck.
Have you seen cases where the program count stays flat but the first interaction still spikes?
The tricky part with shader warm-up is making sure you’re actually covering the programs the scene will use, not just the ones needed for the initial render.
Imagine a product configurator with
EffectComposer, bloom, tone mapping, and several material variants that only become active when the user changes the finish or switches to a different lighting preset. The initial scene might warm up perfectly, but the first interaction could still trigger a new shader compilation and introduce a noticeable hitch.I’d test this by recording the first render and the first interaction separately, with a cold cache, then checking whether any new programs are compiled after the interaction. Otherwise, it’s easy to improve the initial frame while leaving the actual interaction problem untouched.
Have you found a reliable way to enumerate and warm up those less obvious material and post-processing variants without rendering every possible configuration upfront?