Skip to content

The Render Pipeline

When Flutter draws a frame, it runs a fixed pipeline. Knowing the phases tells you where your time goes and where to optimize.

flowchart LR
  build["1. Build
(run build methods)"] --> layout["2. Layout
(constraints down, sizes up)"]
  layout --> paint["3. Paint
(record draw commands)"]
  paint --> composite["4. Composite
(assemble layers, to GPU)"]
The per-frame render pipeline
  1. Build — dirty widgets rebuild, producing an updated widget description.
  2. Layout — the constraints-go-down/sizes-go-up pass computes every render object’s size and position.
  3. Paint — each render object records drawing commands (not pixels yet) into layers.
  4. Composite — the layers are assembled and handed to the GPU to rasterize.

The budget is tight: to hit 60fps you have ~16ms per frame (~8ms at 120Hz). Jank is a frame that misses that budget, and it almost always comes from too much work in build or layout.

Widgets are lightweight, immutable descriptions. The objects that actually lay out and paint are RenderObjects, and they live in the third of Flutter’s three trees:

  • Widget tree — your immutable configuration (rebuilt often, cheap).
  • Element tree — the mutable glue that holds state and links widgets to render objects.
  • Render tree — the RenderObjects that compute layout and paint (expensive; kept and mutated, not rebuilt).

This separation is why rebuilding widgets is cheap: a new widget is compared against the old one, and Flutter mutates the existing render object in place rather than recreating the expensive part.

By default, when a render object repaints, it can force its neighbors to repaint too — they share a layer. A RepaintBoundary puts a subtree on its own layer, so its repaints do not ripple outward (and vice versa). This is the main tool for isolating an animating or frequently-repainting region.

RepaintBoundary(
child: SpinningLogo(), // repaints on its own layer, not the whole screen
)

Use it around things that repaint often (animations, progress spinners) so the rest of the frame is not re-recorded. Overusing it costs memory (each boundary is a layer), so apply it where profiling shows repaint churn.

Flutter also creates relayout boundaries automatically where a subtree’s size cannot affect its parent (for example, under tight constraints) — so a change inside does not force the whole tree to re-lay-out. You rarely manage these by hand, but they are why a localized change stays localized.

To see all of this, use Flutter DevTools: the performance view shows per-frame build/layout/paint times, and the “Highlight repaints” and “Track widget rebuilds” tools show exactly what is churning. Optimize what you measure, not what you guess.

What is the order of the per-frame render pipeline?
Why is rebuilding widgets cheap?
What does a RepaintBoundary do?
Where does jank usually come from?