Why is my shader slow? A practical guide to shader performance

Updated 2026-10-12

A fragment shader runs once for every pixel. On a 1920 by 1080 screen that is more than two million runs per frame, and a game draws sixty or more frames a second. Anything you add to a shader is multiplied by all of that, which is why a line that looks harmless can cost real frames. This guide covers what usually makes a shader slow, the simple fixes that matter most, and how to find out what is actually happening.

First: measure, do not guess

Performance advice is full of folklore, and the real cost of an instruction changes from one GPU to the next. The reliable approach is to measure on the hardware you care about. On ShaderBattle, the results screen reports your shader's GPU time against the target's. In Unity, the Profiler and the Frame Debugger show where frame time goes; on other platforms RenderDoc and the vendor tools (such as Xcode's GPU capture and NVIDIA or AMD profilers) do the same. Change one thing, measure again, and keep the change only if it helped.

Cost one: how many pixels

The most effective optimisation is to run the shader on fewer pixels. Rendering at a lower resolution, restricting the effect to the part of the screen that needs it, or skipping pixels you know will be covered all cut the cost directly. A shader that is twice as expensive but runs on a quarter of the pixels is a big win. This is also why full-screen effects (blur, fog, post-processing) are the usual suspects when a game suddenly slows down.

Cost two: loops

Every loop iteration costs a full set of operations, for every pixel. A procedural noise function with five octaves does five times the work of one octave. A ray-marching loop with 100 steps does up to 100 distance evaluations per pixel. When a shader is slow, loops are the first place to look:

  • Lower the iteration count and see if the picture still looks fine. Often 4 octaves look the same as 6.
  • Stop early. In a ray march, leave the loop the moment a surface is hit or the ray has gone too far.
  • Use loops with small, fixed bounds so the compiler can unroll them.

Cost three: expensive operations

Some operations cost more than others, and the gap depends on the GPU. As a rule of thumb, the ones to be careful with are pow, exp, log, sin, cos, atan, normalize, sqrt and divisions. Cheap, common rewrites include:

float a = x * x;                  // instead of pow(x, 2.0)
float b = x * x * x;              // instead of pow(x, 3.0)
float inv = 1.0 / w;              // work out a division once...
float c = value * inv;            // ...then multiply by it many times
float len2 = dot(v, v);           // compare squared lengths instead of using length() when you only need to compare

Do not do this everywhere and mangle readable code; do it inside loops and in the hot paths that you have measured.

Cost four: texture reads

Sampling a texture costs memory bandwidth, and bandwidth is often the real limit on a GPU rather than arithmetic. Reading several textures per pixel, or reading in an order that jumps around memory, adds up. Use smaller or compressed textures, avoid repeated reads of the same texel (store it in a variable), and use mipmaps so distant surfaces read from smaller versions.

Cost five: branches

A GPU runs pixels in lockstep in small groups (often 32 or 64 at a time, called warps or waves). If one pixel in a group takes the if branch and its neighbour takes the else, the hardware generally has to run both paths for the whole group. A branch is cheap when whole groups of neighbouring pixels agree, and expensive when they split pixel by pixel. Sometimes it is faster to compute both sides and blend them with lerp and step, which has no divergence at all:

float3 result = lerp(colorB, colorA, step(0.5, mask));    // no branch

That only wins when both sides are cheap. If one side is a big expensive calculation, skipping it with a real if that most pixels agree on is still the better choice.

Cost six: precision, especially on phones

Mobile GPUs can often do half-precision maths much faster and with less power. In Unity's HLSL you can declare half, half3 and half4 for values that do not need full precision, such as colors in the 0 to 1 range. Keep full precision (float) for positions, UVs and anything with a large range, or you will see banding and swimming artifacts.

Move work out of the pixel shader

If a value is the same for every pixel, do not compute it in the pixel shader. Work it out once on the CPU and pass it in, or compute it per vertex. A vertex shader runs per vertex, not per pixel, and a big triangle covers thousands of pixels, so anything that varies slowly across a surface (a simple gradient, a light direction) is far cheaper to compute there and let the hardware interpolate it.

A tuning order that works

  1. Measure, and find out what the shader is limited by.
  2. Reduce the number of pixels it runs on.
  3. Cut loop counts and add early exits.
  4. Replace expensive functions in the hot path with cheap equivalents.
  5. Reduce texture reads.
  6. Consider lower precision on mobile.
  7. Re-measure after every change.

See the cost of loops for yourself

The fractal noise lesson adds octave after octave, and you can watch how the GPU time rises with each one. The ray-marching lessons, starting with sphere tracing, show why smart step sizes matter: taking big steps when you are far from a surface is the difference between a smooth frame rate and a slideshow.

Keep reading

Start the interactive lessons