At 60 frames per second, a phone gives you 16.7 ms per frame for everything. In our Flutter 3D measurements, two post effects alone cost 188 ms and 360 ms, and 200 separate objects cost 95 ms. Fix effects first, then object count, then shadows, then lights.

This is for anyone building a 3D game or viewer in Flutter who sees it stutter. The numbers come from a reference runner game we built and timed on flutter_scene 0.20.0 in Flutter web, in September 2026. Your scene will differ, but the order of costs held up every time.

See it in actionTwo effects cost more than the scene
At 60 fps a phone has 16.7 ms per frame. Our runner’s vignette took about 188 ms, colour grading 360 ms and 200 separate objects 95 ms. Illustrative example. Use the playback controls or the thin timeline at the top to explore.

In short:

  • Vignette and colour grading passes cost more than the whole scene.
  • Instancing cut our separate objects from 202 to 6 groups.
  • Shadow cascades each add a full extra render pass.
  • First-second frames are shader compiles. Ignore them when measuring.

Post effects cost more than the scene itself

Each full-screen effect is another pass over every pixel. We timed each one alone, in the same scene, on the web backend. One sentence sums it up: bloom was affordable, vignette and colour grading were not.

Setting Time per frame Verdict
All effects off 3.3 ms Baseline
Bloom about 8.6 ms Keep, high threshold
Vignette about 188 ms Replace with a widget
Colour grading about 360 ms Use tone mapping instead

Those two slow passes were the main cost in a build that ran at single-digit frames per second. The fixes are cheap. A vignette is a Flutter RadialGradient in a DecoratedBox stacked over the scene, which composites cheaply. Colour grading is mostly covered by ACES tone mapping plus an exposure value, which the engine applies anyway.

Object count, not pixels, slowed our scene down

Every separate object costs CPU time each frame, because the engine sorts, culls and submits it. Our first runner had about 200 separate props, each its own node. That cost about 95 ms per frame of CPU before any effects.

The test that proved it was resolution. Halving the render resolution changed the frame cost by only 2%. If pixels were the problem, the cost would have dropped sharply. So the cost was in the number of objects, not their size on screen.

The fix was instancing: one mesh handled as a single item, with a list of positions for its copies. In flutter_scene that is an InstancedMesh. We kept fixed pools of each prop and parked unused copies far below the floor instead of deleting them. 202 separate objects became 6 instanced groups.

Shadows and lights have a price per pass

A shadow map is the scene rendered again from the light's point of view. Cascades are several shadow maps at different distances, and each one is another full pass. Our settings for a phone-sized runner:

  • One sun casting shadows, one cascade.
  • A 768 pixel shadow map.
  • Shadows drawn only within 30 units of the camera.
  • Fog to hide the far end, where props pop in.

Point lights are cheaper than shadows, but not free. We lit only the four lamps nearest the player and one key light that travels with the character, rather than a light on every lamp.

Measure the right number at the right size

Frame rate on the web lies in two ways, so we put two numbers in the corner of the screen. The first is milliseconds per render, a stopwatch around the scene's render call:

final sw = Stopwatch()..start();
              scene.render(camera, canvas, viewport: Offset.zero & size);
              sw.stop();
              final ms = sw.elapsedMicroseconds / 1000.0;
              

That number is the time spent in the render call, and it does not depend on how the browser throttles timers. The second is frames per second, which is honest only in a focused, visible tab. Browsers slow requestAnimationFrame in background tabs, so a screenshot tool can read 1 fps while the real window runs at 58.

Two more rules from our runs:

  • Skip the first frames. The first render took 263 ms because shaders were compiling. A running average that included it still read 126 ms half a minute later. We skip the first 12 frames.
  • Test at the target size. The same build ran about 20 fps filling a desktop browser and 40+ fps at phone size.

Fix slow frames in this order

When the number in the corner is too high, work down this list. Re-measure after each step, because the bottleneck moves.

  1. Turn off post effects except bloom, and replace the vignette with a widget.
  2. Merge repeated props into instanced meshes.
  3. Drop to one shadow cascade and shrink the shadow distance.
  4. Cut point lights to the few near the player.
  5. Halve the render resolution as a probe, to see if pixels matter yet.

Put each setting behind a URL flag while tuning, such as ?shadows=0&post=0, read with Uri.base.queryParameters. Every variant is then one reload instead of one rebuild.

Keep the widget tree out of the game loop

Calling setState sixty times a second rebuilds widgets you do not need to rebuild. Instead, a Ticker advances the game. A ValueNotifier, passed as the painter's repaint, triggers only the repaint. The HUD listens to the same notifier through a ValueListenableBuilder. Flutter's performance best practices explain why fewer rebuilds matter.

What the GrowApps 3D starter does by default

The 3D starter scene in the GrowApps builder ships with these choices already made. It renders with bloom only, a widget vignette, one shadow cascade at 768 pixels, instanced coins and a stopwatch readout in the corner that skips warm-up frames.

The builder agent is told to judge speed at the size the app is for, after shaders have loaded. You can start a 3D game on GrowApps and watch the ms/render number as you add props.

Your ten-minute check

Add the stopwatch above to your painter and print the number after ten seconds. Then turn off every post effect and read it again. If it drops a lot, you have found your first fix. Next, read the structure of a 3D endless runner to see these settings inside a full game.

Frequently asked questions

What frame rate should a mobile 3D game target?

60 fps feels smooth on most phones, which is 16.7 ms per frame. 30 fps is acceptable for slower-paced games if it is steady. A steady 30 beats a jumpy 45.

Are generated 3D models too heavy for phones?

Often the textures are. In our first generated model, textures were 90% of an 11 MB file. Resizing them to 1024 pixels cut the file to under a fifth.

Sources and further reading