How does the Flutter rendering pipeline work?

Medium PriorityAsked in ~55% of Flutter interviews

4 min read

Flutter Internals

Theory — the 4 phases

PhaseWhat runsKey rule
BuildWidget.build() → produces / diffs the Widget & Element treesPure function — no side effects
LayoutRenderObject.performLayout() — parent passes constraints down, child returns size upSingle-pass, top-down constraints / bottom-up sizes
PaintRenderObject.paint() — records draw commands into the Layer treeNo layout work allowed here
CompositeLayer tree → Scene → GPU rasterises pixelsRuns on the raster thread

Frame budget: 16.67ms @ 60Hz, 8.33ms @ 120Hz. Anything longer = dropped frame.


Layout: constraints down, sizes up

Container(
  width: 200,
  height: 100,
  child: Center(                     // forwards loose constraints
    child: Text('Hello'),            // sizes to content, returns size
  ),
)

Mental model — every layout call follows three steps:

  1. Parent gives child constraints (min/max width & height).
  2. Child picks a size within those constraints.
  3. Parent positions the child.

The Flutter mantra: "Constraints go down. Sizes go up. Parent sets position."


Build → Layout → Paint in code

class _State extends State<MyWidget> {
  int _count = 0;

  void _inc() => setState(() => _count++);   // marks element dirty

  @override
  Widget build(BuildContext context) {       // BUILD phase
    return RepaintBoundary(                  // creates a paint boundary
      child: Container(                       // LAYOUT decides size
        padding: const EdgeInsets.all(16),
        child: Text('$_count'),               // PAINT draws text
      ),
    );
  }
}

What happens when _inc() runs:

  1. Element marked dirty
  2. Next frame → build() runs (Build)
  3. Layout pass updates the new subtree (Layout)
  4. Only this RepaintBoundary's layer repaints (Paint)
  5. GPU composites (Composite)

Performance levers

LeverWhat it doesWhen to use
const constructorsCanonicalised widget — Flutter skips rebuilding subtreeAnywhere a widget config is constant
RepaintBoundaryIsolates a subtree into its own layerAnimations, charts, anything that repaints often
Push state downSmaller dirty subtree → less rebuild workLift to the narrowest widget that needs the state
ListView.builderOnly builds visible itemsLong / infinite lists
Cache expensive computations in stateAvoid recomputing in build()Sorted lists, parsed data
MediaQuery.sizeOf (vs .of)Subscribe only to size, not all metricsAvoid unrelated rebuilds

Common mistakes to avoid

// ❌ Heavy work in build()
@override
Widget build(BuildContext context) {
  final sorted = items.toList()..sort();      // recomputed every rebuild
}
// ✅ Cache in didChangeDependencies/didUpdateWidget or compute once in state

// ❌ Skipping const constructors
return Padding(padding: EdgeInsets.all(8), child: ...);
// ✅ const Padding(padding: EdgeInsets.all(8), child: ...)

// ❌ Rebuilding the whole screen on a tiny state change
class _BigScreen extends StatefulWidget { ... }    // 1 setState rebuilds ALL of it
// ✅ Push the StatefulWidget down to the smallest possible subtree

// ❌ Wrapping every widget in RepaintBoundary
// They have a cost (extra layer). Use only around things that repaint independently.

// ❌ Assuming widgets are slow
// 99% of jank in real apps is in build() or layout(), not Flutter itself.
// Profile with the DevTools Performance tab before optimising.

Interview follow-ups

  1. Why are widgets immutable if they're rebuilt every frame? Doesn't that waste memory? Building widgets is cheap — they're just config objects. The expensive trees (Element, RenderObject) persist and are diffed. Immutability makes the diff predictable and unlocks const canonicalisation.

  2. What's a RepaintBoundary and when is it worth one? A boundary that gives the subtree its own compositing layer. A repaint inside that subtree doesn't dirty the parent. Worth it for animations, charts, complex CustomPaint — anywhere repaints happen at a different rate than the surroundings.

  3. How does Flutter achieve smooth scrolling on huge lists? ListView.builder (and the Sliver family) lazily builds items only inside the viewport plus a cache extent. Items leaving the cache are unbuilt; new items are built on demand.

  4. What runs on the UI thread vs the raster thread? Build, Layout, Paint run on the UI thread. Compositing/rasterisation runs on the raster thread. CPU-heavy work belongs on an isolate (compute / Isolate.run) so neither thread stalls.


How helpful was this content?

Please sign in to rate this article.