Flutter’s Rendering Pipeline: How Flutter Builds, Updates UI, Repaints & Re-layouts
Introduction
Flutter is known for its high-performance UI rendering and smooth animations. But what actually happens behind the scenes when you build or update a UI?
Understanding Flutter’s Rendering Pipeline is critical for:
- Writing efficient Flutter apps
- Avoiding unnecessary rebuilds
- Improving performance
- Handling complex UI updates
This guide explains:
- How Flutter builds UI
- How updates propagate
- Difference between repaint and relayout
- Real-world coding scenarios
What is Flutter Rendering Pipeline?
Flutter rendering pipeline is the step-by-step process used to convert your widget code into pixels on the screen.
Pipeline Flow:
Widgets → Elements → Render Objects → Painting → Display
Key Stages:
- Widget Tree (Configuration)
- Element Tree (Lifecycle management)
- Render Object Tree (Layout & painting)
- Painting (GPU rendering)
1. How Flutter Builds UI
Flutter uses a declarative UI approach.
Key Idea:
You describe what UI should look like, Flutter decides how to render it.
Example: Basic UI Build
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("MyApp build called");
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("Rendering Example")),
body: Center(
child: Text("Hello Flutter"),
),
),
);
}
}
Output:
- Console prints: MyApp build called
- UI renders text on screen
Important Concept
Widgets are Immutable
- Widgets are just blueprints
- They do not store state
2. Widget → Element → RenderObject
1. Widget
- Lightweight configuration
- Immutable
2. Element
- Maintains widget lifecycle
- Connects widget to render tree
3. RenderObject
- Handles layout & painting
Visualization:
Widget Tree
↓
Element Tree
↓
RenderObject Tree
↓
Screen Pixels
3. How Flutter Updates UI
UI updates occur when:
- setState() is called
- Parent widget rebuilds
- Dependency changes
Real-Time Example: UI Update:
class CounterExample extends StatefulWidget {
@override
_CounterExampleState createState() => _CounterExampleState();
}
class _CounterExampleState extends State<CounterExample> {
int count = 0;
@override
Widget build(BuildContext context) {
print("Build called");
return Scaffold(
appBar: AppBar(title: Text("Counter App")),
body: Center(
child: Text("Count: $count"),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
count++;
});
},
child: Icon(Icons.add),
),
);
}
}
What Happens Internally?
When button is pressed:
- setState() is called
- Flutter marks widget as dirty
- build() method runs again
- UI updates
4. Rebuild vs Relayout vs Repaint
This is the core concept for performance optimization.
4.1 Rebuild
Occurs when:
- build() method runs again
Trigger:
- setState()
Cost:
- Low (fast operation)
4.2 Relayout
Occurs when:
- Size or constraints change
Example:
Container( width: isExpanded ? 200 : 100, height: 100, )
Trigger:
- Size changes
- Parent constraints change
Cost:
- Medium
4.3 Repaint
Occurs when:
- Visual appearance changes
- Layout remains same
Container( width: 100, height: 100, color: isRed ? Colors.red : Colors.blue, )
Cost:
- High (affects GPU rendering)
5. Real-Time Scenario: Rebuild vs Repaint vs Relayout
Example Code:
class RenderDemo extends StatefulWidget {
@override
_RenderDemoState createState() => _RenderDemoState();
}
class _RenderDemoState extends State<RenderDemo> {
bool isRed = true;
double width = 100;
@override
Widget build(BuildContext context) {
print("Build triggered");
return Scaffold(
appBar: AppBar(title: Text("Rendering Pipeline Demo")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: width,
height: 100,
color: isRed ? Colors.red : Colors.blue,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
setState(() {
isRed = !isRed; // Repaint only
});
},
child: Text("Change Color"),
),
ElevatedButton(
onPressed: () {
setState(() {
width += 50; // Relayout + repaint
});
},
child: Text("Change Size"),
),
],
),
),
);
}
}
Analysis:
| Action | Effect |
|---|---|
| Change Color | Repaint only |
| Change Width | Relayout + Repaint |
| setState() | Rebuild always happens |
6. Avoiding Unnecessary Rebuilds
Column(
children: [
Text("Static Text"),
Text("Dynamic: $count"),
],
)
Entire column rebuilds unnecessarily.
Optimized Solution:
Column(
children: [
const Text("Static Text"),
Text("Dynamic: $count"),
],
)
Why?
- const widgets do not rebuild
7. Using RepaintBoundary for Optimization
RepaintBoundary(
child: Container(
width: 200,
height: 200,
color: Colors.green,
),
)
Benefit:
- Prevents unnecessary repaint propagation
8. Flutter Rendering Lifecycle Summary
User Action → setState()
↓
Widget Rebuild
↓
Element Update
↓
RenderObject Layout
↓
Painting
↓
Display on Screen
9. Best Practices for Performance
1. Use const Widgets
const Text(“Hello”)
2. Minimize setState Scope
Only update necessary widgets.
3. Use ListView.builder
Efficient rendering for large lists.
4. Avoid Deep Widget Trees
Flatten UI when possible.
5. Use Keys for Efficient Updates
ListView(
children: [
Text("Item 1", key: ValueKey(1)),
],
)
10. Real-World Use Case
Scenario: E-commerce App
- Product list → uses lazy loading
- Cart updates → partial rebuild
- Price change → repaint only
Conclusion
Flutter’s rendering pipeline is designed for high performance and flexibility. Understanding how UI builds, updates, relayouts, and repaints helps you:
- Write optimized Flutter apps
- Avoid unnecessary rendering
- Improve app responsiveness
Key Takeaways
- Flutter uses a declarative UI model
- Widgets are immutable
- setState() triggers rebuild
- Relayout happens when size changes
- Repaint happens when appearance changes
- Optimization is critical for performance
FAQ
Q1: Does setState always rebuild UI?
Yes, but Flutter optimizes rendering internally.
Q2: What is most expensive: rebuild, relayout, or repaint?
Repaint is generally the most expensive.
Q3: How to improve Flutter performance?
- Use const widgets
- Avoid unnecessary rebuilds
- Use RepaintBoundary