Engineering

Designing for 60fps Native Performance in Flutter

Arjav JainMay 6, 20268 min read

How we optimize layout passes, minimize widget rebuilds, and structure state machines to deliver fluid interfaces — the specific techniques, not just the theory.

Flutter's rendering engine is capable of a consistent 60fps (and 120fps on capable hardware) out of the box, but 'capable' isn't the same as 'automatic.' Most jank we've debugged in production Flutter apps traces back to a small set of repeated mistakes — not a framework limitation. This is the specific checklist we run through when performance-tuning a screen.

Widget rebuilds are the first thing to profile

Flutter's rebuild model is efficient when scoped correctly and expensive when it isn't. The most common mistake is putting state high up the widget tree — a single setState() call on a parent widget can trigger a rebuild of an entire subtree, including expensive children that didn't actually need to change. Using const constructors wherever a widget's inputs are genuinely static, and scoping state management (Riverpod, Bloc, or even plain ValueNotifier) to the smallest widget subtree that actually needs to react to a given piece of state, eliminates most unnecessary rebuild cascades.

  • Use const constructors for any widget subtree that doesn't depend on changing state — Flutter skips rebuilding these entirely.
  • Scope Provider/Riverpod/Bloc listeners to the narrowest widget that needs them, not a top-level wrapper around the whole screen.
  • Use RepaintBoundary around expensive custom-painted widgets (charts, custom animations) to isolate their repaint cost from sibling widgets.
  • Profile with Flutter DevTools' widget rebuild counter before optimizing anything — intuition about what's expensive is wrong more often than not.

Impeller changed the shader-jank conversation

Historically, Flutter's Skia-based renderer caused visible jank on an animation or transition's first run, because shaders were compiled just-in-time on first use. Impeller, now Flutter's default rendering engine, pre-compiles shaders at build time instead, which removes this entire class of first-run stutter. If you're profiling an older Flutter app and seeing first-run animation jank that disappears on subsequent runs, that's the classic Skia shader-compilation signature — upgrading to a recent Flutter version with Impeller enabled is usually the fix, not a code change.

Layout passes: measure once, not per frame

Complex layouts — nested Flex widgets, deeply nested Column/Row combinations, or custom layout delegates — can trigger expensive layout recalculation every frame if the layout depends on animated values. Where possible, we push animation into the paint phase (via Transform, Opacity, or AnimatedBuilder scoped tightly around only the animating widget) rather than the layout phase, since paint-phase changes are significantly cheaper than triggering a full layout recalculation.

The fastest animation is one that never triggers a layout pass. Transform and Opacity are paint-phase; changing a widget's size or position via layout properties is not.

State machines for complex screens

For screens with genuinely complex state (multi-step forms, screens with several async loading states, anything with more than three or four distinct UI states) we model state explicitly as a finite state machine rather than a scattered collection of boolean flags. This isn't primarily a performance optimization — it's a correctness one — but it has a performance side effect: explicit state machines make it obvious exactly which widget subtree needs to rebuild on a given transition, which naturally leads to tighter rebuild scoping instead of the 'just wrap it in setState and see what happens' pattern that causes most of the rebuild cascades described above.

None of these techniques are exotic — they're the standard toolkit for any team that's shipped Flutter apps at scale. The consistent theme is scoping: scope your state, scope your rebuilds, scope your repaints, and profile before optimizing rather than guessing. That discipline is what separates a Flutter app that feels native from one that technically runs at 60fps in a benchmark but visibly stutters under real usage.

Have a project in mind?

Let's build it.

Start a project