Question #32MediumFlutter Basics

How flutter renders ?

#flutter

Answer

Overview

Flutter's rendering pipeline transforms widget trees into pixels on the screen. Understanding this process is crucial for building performant apps and debugging layout issues.


Rendering Pipeline

text
Widget Tree → Element Tree → RenderObject Tree → Layer Tree → Rasterization → Pixels

The Three Trees

1. Widget Tree (Configuration)

Immutable blueprint describing what the UI should look like.

dart
Container(
  color: Colors.blue,
  child: Text('Hello'),
)

Characteristics:

  • Lightweight (just configuration)
  • Rebuilt frequently
  • Immutable (recreated each frame)
  • Cheap to create and destroy

2. Element Tree (Lifecycle Manager)

Mutable connection between widgets and render objects.

dart
// Internally created by Flutter
ComponentElement(Container)
  └─ RenderObjectElement(ColoredBox)
      └─ ComponentElement(Text)

Responsibilities:

  • Manage widget lifecycle
  • Hold state
  • Update render objects
  • Persist across rebuilds

3. RenderObject Tree (Layout & Paint)

Mutable objects that perform actual layout, painting, and hit testing.

dart
RenderBox(ColoredBox)
  └─ RenderParagraph(Text)

Responsibilities:

  • Calculate size
  • Position children
  • Paint to canvas
  • Handle gestures

Complete Rendering Process

Step 1: Build Phase

Widgets describe UI structure.

dart

Widget build(BuildContext context) {
  return Container(
    width: 200,
    height: 100,
    child: Text('Hello'),
  );
}

Output: Widget tree created


Step 2: Element Tree Update

Flutter compares new widgets with existing element tree.

dart
// If widget type matches existing element
Element.update(newWidget)

// If widget type changed
Element.unmount()
newElement = createElement()

Optimization: Reuses elements when possible


Step 3: RenderObject Creation/Update

Elements create or update render objects.

dart
class RenderPositionedBox extends RenderAligningShiftedBox {
  
  void performLayout() {
    // Calculate size and position
    size = constraints.biggest;
    if (child != null) {
      child!.layout(constraints);
    }
  }
}

Step 4: Layout Phase

Each RenderObject calculates its size based on constraints.

text
Parent Constraints → Child Size → Parent Size
dart
// Constraints flow down
BoxConstraints(
  minWidth: 0,
  maxWidth: 400,
  minHeight: 0,
  maxHeight: 800,
)

// Sizes flow up
Size(200, 100)

Layout Process:

  1. Parent sends constraints to child
  2. Child determines its size within constraints
  3. Parent positions child
  4. Repeat recursively

Step 5: Paint Phase

RenderObjects paint themselves onto canvases.

dart

void paint(PaintingContext context, Offset offset) {
  // Paint background
  context.canvas.drawRect(
    offset & size,
    Paint()..color = Colors.blue,
  );
  
  // Paint children
  if (child != null) {
    context.paintChild(child!, offset);
  }
}

Paint Order:

  1. Parent paints first (background)
  2. Then children (foreground)
  3. Bottom to top in tree

Step 6: Compositing

Flutter creates layers for efficient repainting.

text
┌─────────────────────────┐
│  Layer Tree             │
│  ┌─────────────────┐    │
│  │ Container Layer │    │
│  │  ┌───────────┐  │    │
│  │  │ Picture   │  │    │
│  │  │ Layer     │  │    │
│  │  └───────────┘  │    │
│  └─────────────────┘    │
└─────────────────────────┘

Benefits:

  • Only repaint changed layers
  • GPU acceleration
  • Smooth animations

Step 7: Rasterization

Skia graphics engine converts layers to pixels.

text
Layer Tree → GPU Commands → Pixels → Screen

Handled by:

  • Skia (2D graphics engine)
  • Platform GPU (Metal on iOS, Vulkan/OpenGL on Android)

Frame Rendering Timeline

text
┌──────────────────────────────────────────────┐
│  Frame (16.67ms for 60fps)                   │
├──────────────────────────────────────────────┤
│  Build:     2-4ms    (Widget tree)           │
│  Layout:    2-4ms    (RenderObject tree)     │
│  Paint:     2-4ms    (Drawing commands)      │
│  Composite: 2-4ms    (Layer tree)            │
│  Raster:    2-4ms    (GPU rendering)         │
├──────────────────────────────────────────────┤
│  Total: ~10-15ms (within 16.67ms budget)     │
└──────────────────────────────────────────────┘

Rendering Optimization

1. Widget Reuse

Flutter minimizes widget rebuilds using

text
const
and keys.

dart
// ✅ Good - Widget reused
const Text('Hello')

// ❌ Bad - New widget every build
Text('Hello')

2. Element Reuse

Elements are reused when widget type and key match.

dart
// Same widget type → Element reused
Text('Hello')Text('World')  // ✅ Reuse

// Different type → New element
Text('Hello')Icon(Icons.home)  // ❌ Replace

3. RenderObject Reuse

RenderObjects persist and only update changed properties.

dart
// Only color changes → Same RenderObject updated
Container(color: Colors.blue)Container(color: Colors.red)

4. Layer Reuse

Unchanged layers skip repainting.

dart
RepaintBoundary(
  child: ExpensiveWidget(),  // Won't repaint if parent changes
)

Practical Example

dart
class AnimatedBox extends StatefulWidget {
  
  _AnimatedBoxState createState() => _AnimatedBoxState();
}

class _AnimatedBoxState extends State<AnimatedBox> {
  double _width = 100;
  
  void _expand() {
    setState(() => _width = 200);
  }
  
  
  Widget build(BuildContext context) {
    // 1. BUILD: Create widget tree
    return GestureDetector(
      onTap: _expand,
      child: Container(
        width: _width,  // Changed value
        height: 100,
        color: Colors.blue,
        child: const Text('Tap me'),  // Const = reused
      ),
    );
  }
}

What happens when setState() is called:

  1. Build: New Widget tree created
  2. Element Update: GestureDetector element reused, Container element reused
  3. RenderObject Update: RenderBox width property updated (100 → 200)
  4. Layout: RenderBox recalculates layout with new width
  5. Paint: RenderBox repaints at new size
  6. Composite: New layer created
  7. Raster: GPU renders new frame

Debugging Rendering

Performance Overlay

dart
MaterialApp(
  showPerformanceOverlay: true,  // Shows GPU/UI threads
  home: MyApp(),
)

Debug Paint

dart
import 'package:flutter/rendering.dart';

void main() {
  debugPaintSizeEnabled = true;  // Shows layout boundaries
  debugPaintBaselinesEnabled = true;  // Shows text baselines
  debugPaintPointersEnabled = true;  // Shows tap areas
  runApp(MyApp());
}

Flutter DevTools

bash
flutter run
# Open DevTools in browser
# View widget tree, timeline, memory

Key Concepts

Constraints Go Down, Sizes Go Up

dart
// Parent sends constraints
child.layout(
  BoxConstraints(
    minWidth: 0,
    maxWidth: 400,
  ),
);

// Child returns size
return Size(200, 100);

Painting Order

dart
// Bottom to top
Stack(
  children: [
    Container(color: Colors.red),   // Painted first (back)
    Container(color: Colors.blue),  // Painted second
    Container(color: Colors.green), // Painted last (front)
  ],
)

Layer Separation

dart
// Create separate layer for optimization
RepaintBoundary(
  child: AnimatedWidget(),  // Won't affect parent repainting
)

Render Pipeline Comparison

PhaseFrequencyCostOptimization
BuildEvery setState()LowUse const widgets
LayoutWhen size changesMediumAvoid deep trees
PaintWhen appearance changesMediumUse RepaintBoundary
CompositeEvery frameLowHandled by engine
RasterEvery frameHigh (GPU)Reduce layers

Best Practices

Important: Minimize unnecessary rebuilds and layouts

✅ Do

dart
// Use const constructors
const Text('Hello')

// Separate expensive widgets
RepaintBoundary(child: HeavyWidget())

// Avoid deep widget trees
Column(children: [
  Widget1(),
  Widget2(),
])

❌ Don't

dart
// Avoid creating widgets in methods
Widget getWidget() => Container();  // Bad

// Don't nest too deep
Column(
  children: [
    Row(children: [
      Column(children: [
        Row(children: [...])  // Too deep!
      ])
    ])
  ]
)

Resources