A lot of the WebGL questions that land here turn out to be performance questions wearing a syntax costume, so this is the profiling angle rather than the GLSL tutorial angle. If you have ever stripped a scene down to fewer triangles and watched the frame rate refuse to move, this is probably why.
Where The Frame Time Actually Goes
A 1920x1080 canvas is a little over two million pixels. Every triangle covering a pixel runs the fragment shader for that pixel, and with overdraw, meaning several triangles stacked over the same pixel, the fragment shader can execute tens of millions of times in a single frame. The vertex shader runs once per vertex, which for most web scenes is a few thousand invocations.
That ratio is the whole story. Cutting geometry shrinks the small number. Cutting per pixel work shrinks the large one.
Moving Work Up A Stage
Communication between the two programmable stages only flows one way. The vertex shader writes varyings, the rasterizer interpolates them across the triangle, and the fragment shader reads them as inputs. Anything that interpolates acceptably can be computed per vertex and handed down essentially for free, which is the difference between per vertex Gouraud lighting and per pixel Phong lighting: smoother results at real cost.
Procedural animation is the other easy win. Grass sway, ocean waves and cloth flutter are vertex displacement driven by sine waves, noise and a time uniform, and that beats rewriting vertex buffers from JavaScript every frame because the GPU does the math in parallel across every vertex at once. The full guide to game shaders and visual effects walks through the GLSL for both stages if you want the code rather than the summary.
Branching Costs More Than You Expect
GPUs execute fragments in groups, called warps or wavefronts depending on the vendor. If any fragment in a group takes a different branch, every fragment in that group executes both sides. So the if/else you wrote to skip expensive work often costs about the same as running it unconditionally. step(), mix() and clamp() usually beat the branch, and this is the single most common surprise for people coming from CPU code.
Draw calls matter for a related reason. Each one can force a shader program switch, texture rebinds and uniform updates, so a pile of unique ShaderMaterial instances quietly defeats the automatic batching that Three.js and Babylon.js do for you.
One Shader Source For WebGL And WebGPU
Three.js now ships TSL, a JavaScript node graph that compiles to GLSL for the WebGL backend and WGSL for WebGPU from a single source, so you are not maintaining two shader codebases while WebGPU support fills in across browsers. Classic ShaderMaterial still works if you would rather write GLSL by hand or you are porting existing code. Babylon.js goes the parallel route with its own node material system plus a built in inspector.
Takeaway
Profile first. Spector.js captures the WebGL calls and lets you inspect the shader program, textures and GPU state per draw call, and the Babylon.js inspector reports draw call counts and frame stats directly. Once you can see which stage is eating the frame, the fix is usually obvious, and it is usually not fewer triangles.