Engineering
How Flutter Works: The Rendering Pipeline Explained
Flutter doesn't use the platform's UI components — it draws every pixel itself. Here's what actually happens between your widget code and the glass.
Flutter draws, it doesn't delegate
Most cross-platform frameworks are translators. React Native, for instance, maps your components onto real UIView and android.view.View objects, then keeps two worlds in sync across a bridge. Flutter takes the opposite approach: it ships its own rendering engine and paints every pixel onto a single canvas.
That one decision explains almost everything else about the framework — why a Flutter app looks identical on an iPhone 15 and a five-year-old Android, why animations stay smooth, and why the binary is a few megabytes larger than a native equivalent.
The three trees
The mental model that unlocks Flutter is that there isn't one tree, there are three, and they have very different lifetimes.
- The widget tree is your code. Widgets are immutable configuration objects — cheap descriptions of what the UI should look like. They're thrown away and rebuilt constantly.
- The element tree is the long-lived middle layer. Each element holds a reference to a widget and to its render object, and decides on rebuild whether the new widget can update the existing render object or whether it needs a new one.
- The render tree does the real work: layout, painting and hit testing. Render objects are expensive, so Flutter works hard to reuse them.
When you call setState, you are not repainting the screen. You are marking an element dirty. On the next frame Flutter rebuilds that widget subtree, diffs it against the elements, and only the render objects that genuinely changed get re-laid-out and repainted.
This is why const constructors matter so much. A const widget is canonicalised at compile time, so on rebuild Flutter sees the identical instance, short-circuits the diff, and skips that entire subtree. It's the cheapest performance win in the framework.
What happens in a single frame
The engine asks for a frame when the display is ready — 60 times a second on most devices, 120 on newer hardware. That gives you roughly 16ms, or 8ms at 120Hz, to do everything. Inside that window Flutter runs a fixed pipeline:
- Animate — tick any running animation controllers and update their values.
- Build — rebuild every widget marked dirty, producing a new widget subtree.
- Layout — walk the render tree passing constraints down and receiving sizes back up. Constraints go down, sizes go up, parent sets position.
- Paint — record drawing commands into a layer tree. Nothing is rasterised yet; this just builds a display list.
- Composite — flatten the layer tree and hand it to the GPU thread.
- Rasterise — the engine turns those commands into actual pixels.
Layout is single-pass, which is the reason it's fast. A render object gets constraints from its parent, picks its own size within them, and never asks its children twice. If you've fought with reflow loops in a web layout engine, this is the constraint that removes that entire category of problem.
Skia, and now Impeller
For most of Flutter's life, rasterisation went through Skia — the same 2D graphics library that powers Chrome. Skia compiles shaders lazily, which produced Flutter's most notorious symptom: the first run of an animation janks while a shader compiles, then is perfectly smooth forever after.
Impeller, now the default on iOS and Android, fixes this by compiling its shader set ahead of time at build. You trade a slightly larger binary for predictable first-run performance. If you're on a recent Flutter version you're already using it, and the old "warm up your shaders" workarounds are no longer needed.
Talking to the platform
Because Flutter draws its own UI, anything belonging to the operating system — the camera, Bluetooth, biometric prompts, the share sheet — has to be reached over a platform channel. You send a message from Dart, native code handles it, and a result comes back asynchronously.
This is a real cost, and it's the honest limit of the framework. A plugin exists for most common needs, but if your product's core value is a tight loop with platform hardware — a custom camera pipeline, high-frequency sensor processing, AR — you are going to spend a lot of time on that boundary. That is usually the point at which we recommend native instead.
Why this matters when you're commissioning an app
You don't need to know any of this to hire a team. But it explains the trade you're making: Flutter buys you one codebase and pixel-identical output on both platforms, at the cost of a larger binary and a bridge between you and the OS. For most apps — screens, lists, forms, networking, business logic — that's a good trade. For hardware-heavy products, it isn't.
Building something with Flutter?
We'll tell you honestly whether Flutter is right for it — including when it isn't. Free 30-minute call, no pitch deck.