Rendering Pipeline #
Setiap kali Flutter menampilkan sebuah frame di layar, ada serangkaian fase yang dieksekusi secara berurutan dan terkoordinasi. Proses ini disebut rendering pipeline — jalur yang ditempuh data dari kode Dart yang kamu tulis hingga menjadi piksel aktual di layar. Memahami pipeline ini adalah kunci untuk menulis kode Flutter yang performan dan tahu persis di mana harus mengoptimasi ketika aplikasi terasa lambat.
Gambaran Besar Pipeline #
Rendering pipeline Flutter terdiri dari lima fase utama, semuanya dipicu oleh sinyal VSync dari hardware display:
VSync Signal (dari display hardware, 60Hz/120Hz)
|
v
+--------+--------+--------+----------+----------+
| | | | | |
| BUILD | LAYOUT | PAINT |COMPOSITE |RASTERIZE |
| | | | | |
| Widget | ukuran | gambar | susun | piksel |
| tree | posisi | canvas | layer | ke GPU |
+--------+--------+--------+----------+----------+
^ ^ ^ ^ ^
UI Thread Raster Thread
(Dart) (C++/GPU)
Fase Build, Layout, Paint, dan Composite berjalan di UI Thread (Dart). Fase Rasterize berjalan di Raster Thread (C++) yang terpisah — keduanya bisa berjalan secara paralel antar frame.
VSync: Pemicu Setiap Frame #
Sebelum masuk ke fase-fasenya, penting untuk memahami apa yang memulai seluruh pipeline: VSync signal.
VSync (Vertical Synchronization) adalah sinyal yang dikirim oleh hardware display setiap kali layar siap menerima frame baru. Untuk layar 60Hz, VSync dikirim setiap ~16,6ms. Untuk layar 120Hz, setiap ~8,3ms.
Layar 60Hz:
|--16.6ms--|--16.6ms--|--16.6ms--|
^VSync ^VSync ^VSync
Frame 1 Frame 2 Frame 3
Layar 120Hz:
|-8.3ms-|-8.3ms-|-8.3ms-|-8.3ms-|
^VSync ^VSync ^VSync ^VSync
Frame 1 Frame 2 Frame 3 Frame 4
Flutter mendaftarkan diri ke sistem operasi untuk menerima sinyal VSync melalui Embedder. Ketika VSync diterima, Engine memanggil drawFrame() di WidgetsFlutterBinding — inilah yang memulai seluruh rendering pipeline.
Flutter hanya me-render frame ketika ada sesuatu yang berubah. Jika tidak ada perubahan state, animasi, atau input, Flutter tidak meminta frame baru — tidak ada pekerjaan yang dilakukan, dan baterai tidak terkuras. Ini berbeda dengan game engine yang selalu me-render setiap VSync meskipun tidak ada perubahan.
Build Phase — Membangun Widget Tree #
Build phase adalah fase pertama — di sinilah kode Dart kamu berjalan.
Apa yang Terjadi #
Ketika setState() dipanggil, markNeedsBuild() dipanggil pada Element yang bersangkutan. Element tersebut kemudian ditambahkan ke dirty elements list yang dikelola oleh BuildOwner.
Saat Build phase dimulai, BuildOwner mengiterasi semua dirty elements dan memanggil build() pada masing-masing:
// Kamu memanggil setState()
void _onButtonTap() {
setState(() {
_counter++; // state berubah
});
// Flutter menandai element ini sebagai "dirty"
// dan menjadwalkan frame baru
}
// Flutter kemudian memanggil build() saat frame berikutnya
@override
Widget build(BuildContext context) {
// Widget tree baru dibuat berdasarkan state terbaru
return Text('Counter: $_counter');
}
Reconciliation — Diffing Widget Tree #
Flutter tidak membangun ulang seluruh widget tree dari nol setiap frame. Ia melakukan reconciliation — membandingkan widget tree baru dengan element tree yang sudah ada:
Widget tree lama: Widget tree baru:
Column Column
Text("Halo") Text("Halo") --> sama, skip
Button(warna=biru) Button(warna=merah) --> berbeda, update
Icon(star) Icon(star) --> sama, skip
Proses reconciliation mengikuti tiga aturan sederhana:
Aturan 1 — Tipe sama, key sama:
Update widget yang ada (paling efisien)
Aturan 2 — Tipe berbeda:
Hapus element lama, buat element baru
Aturan 3 — Key berbeda:
Cari element dengan key yang cocok di level yang sama
Jika tidak ketemu, buat element baru
Sublinear Rendering #
Build phase Flutter bersifat sublinear — menambah jumlah widget tidak berarti menambah waktu render secara proporsional. Hanya widget yang benar-benar berubah yang di-rebuild. Widget yang stabil (terutama yang di-mark const) sepenuhnya di-skip:
// Widget ini TIDAK pernah di-rebuild karena const
const Text('Label statis')
// Widget ini di-rebuild setiap kali parent-nya rebuild
Text('Label dinamis: $nilai')
Layout Phase — Menghitung Ukuran dan Posisi #
Setelah widget tree dibangun, Flutter perlu mengetahui seberapa besar dan di mana setiap elemen harus ditempatkan. Ini adalah tanggung jawab Layout phase, yang bekerja pada Render Object Tree (bukan Widget Tree).
Constraint System: “Constraints Go Down, Sizes Go Up” #
Flutter menggunakan sistem layout berbasis constraint yang mengikuti prinsip fundamental:
"Constraints go down"
Parent --> [maxWidth: 360, maxHeight: 80] --> Child
|
"Sizes go up" v
Parent <-- [width: 200, height: 40] <---- Child menghitung ukurannya
"Parent sets position"
Parent menetapkan: "Child berada di koordinat (80, 20)"
Contoh Constraint Flow #
// Ini yang terjadi saat Flutter melayout widget ini:
Padding(
padding: EdgeInsets.all(16),
child: Text('Halo Flutter'),
)
// 1. Screen memberikan constraint ke Padding:
// maxWidth: 360, maxHeight: 640
// 2. Padding mengurangi constraint sesuai padding-nya:
// maxWidth: 360-32=328, maxHeight: 640-32=608
// Lalu meneruskan ke Text
// 3. Text menghitung ukurannya sendiri:
// width: 120 (sesuai panjang teks), height: 20 (sesuai font)
// Laporkan ke Padding
// 4. Padding menambahkan padding ke ukuran Text:
// width: 120+32=152, height: 20+32=52
// Laporkan ke Screen
// 5. Screen menetapkan posisi Padding: (0, 0)
Relayout Boundary #
Tidak semua RenderObject di-relayout setiap frame. Flutter menggunakan konsep relayout boundary — sebuah RenderObject yang menjadi batas isolasi layout:
Widget tree:
Screen
|
+-- Column (relayout boundary)
| |
| +-- Text A --> hanya Column ke bawah yang di-relayout
| +-- Text B jika ada perubahan di dalamnya
|
+-- Sidebar (relayout boundary)
|
+-- Icon --> tidak terpengaruh perubahan di Column
+-- Label
Widget yang bersifat relayout boundary secara otomatis antara lain: Expanded, Flexible, SizedBox, ConstrainedBox, dan widget yang menerima constraint yang tidak tergantung parent-nya.
Paint Phase — Merekam Instruksi Gambar #
Setelah ukuran dan posisi setiap RenderObject diketahui, Paint phase merekam instruksi gambar ke dalam struktur bernama Picture.
Paint Bukan Langsung Menggambar Piksel #
Ini adalah kesalahpahaman yang umum: Paint phase tidak langsung menghasilkan piksel. Ia hanya merekam instruksi yang akan dieksekusi nanti oleh GPU.
Paint phase output = Picture (display list)
"Gambar rectangle di (10,10) ukuran 100x50 warna biru"
"Gambar teks 'Halo' di (20,20) font 16sp"
"Gambar lingkaran di (60,60) radius 30 warna merah"
... dst
Piksel aktual dibuat di Rasterize phase (oleh GPU)
PaintingContext dan Canvas #
Setiap RenderObject menerima PaintingContext yang menyediakan akses ke Canvas:
class MyRenderBox extends RenderBox {
@override
void paint(PaintingContext context, Offset offset) {
final canvas = context.canvas;
// Semua perintah ini DIREKAM, belum dieksekusi ke piksel
canvas.drawRect(
offset & size, // shorthand untuk Rect.fromLTWH
Paint()..color = Colors.blue,
);
canvas.drawShadow(
Path()..addRect(offset & size),
Colors.black,
4.0,
true,
);
}
}
Repaint Boundary — Isolasi Paint #
Sama seperti relayout boundary, Flutter juga memiliki repaint boundary untuk mengisolasi area yang perlu di-repaint:
// Tanpa RepaintBoundary:
// Animasi di ListTile pertama menyebabkan SELURUH list di-repaint
// Dengan RepaintBoundary:
ListView.builder(
itemBuilder: (context, index) {
return RepaintBoundary( // setiap item punya layer sendiri
child: ListTile(
leading: AnimatedIcon(...), // hanya item INI yang di-repaint
title: Text('Item $index'),
),
);
},
)
Composite Phase — Menyusun Layer Tree #
Setelah Paint phase selesai, Flutter memiliki sekumpulan Picture yang perlu disusun menjadi Layer Tree — representasi terstruktur yang siap dikirim ke GPU.
Mengapa Perlu Layer? #
Layer memungkinkan beberapa optimasi penting:
TANPA layer:
Setiap frame: gambar ulang SEMUA elemen dari awal
DENGAN layer:
Frame N: gambar semua layer, cache di GPU
Frame N+1: hanya update layer yang BERUBAH
layer yang tidak berubah di-REUSE dari cache GPU
(retained rendering)
Jenis Layer yang Dihasilkan #
// Opacity widget menghasilkan OpacityLayer
Opacity(opacity: 0.5, child: ...)
// Transform widget menghasilkan TransformLayer
Transform.rotate(angle: pi/4, child: ...)
// ClipRect menghasilkan ClipRectLayer
ClipRect(child: ...)
// RepaintBoundary menghasilkan OffsetLayer (layer baru yang terisolasi)
RepaintBoundary(child: ...)
// Konten yang digambar menghasilkan PictureLayer
CustomPaint(painter: MyPainter())
SceneBuilder — Merakit Scene Final #
SceneBuilder adalah komponen yang merakit semua layer menjadi sebuah Scene yang siap dikirim ke Engine:
Layer Tree:
OffsetLayer (root)
|-- TransformLayer (transformasi kamera/scroll)
| |-- PictureLayer (background)
| |-- OffsetLayer (RepaintBoundary)
| | |-- PictureLayer (konten animasi)
| |-- PictureLayer (UI statis)
|-- OpacityLayer (overlay/modal)
|-- PictureLayer (konten modal)
SceneBuilder merakit ini menjadi Scene
|
v
Window.render(scene) --> dikirim ke Engine
Rasterize Phase — Menghasilkan Piksel #
Rasterize phase berjalan di Raster Thread (terpisah dari UI Thread) dan mengeksekusi semua instruksi gambar yang direkam selama Paint phase menjadi piksel aktual di GPU memory.
Alur Rasterisasi #
Scene (dari Composite phase)
|
v
Raster Thread menerima Layer Tree
|
v
Impeller / Skia memproses setiap layer:
- Eksekusi drawing commands (drawRect, drawText, dll.)
- Aplikasikan efek (opacity, blur, shadow)
- Hasilkan GPU draw calls (Vulkan/Metal/OpenGL)
|
v
GPU mengeksekusi draw calls
|
v
Frame buffer terisi piksel
|
v
Embedder menyerahkan frame buffer ke display
|
v
Layar menampilkan frame baru ✓
Double Buffering #
Flutter menggunakan double buffering untuk mencegah tearing (artefak visual saat frame yang belum selesai ditampilkan):
Buffer A: Frame yang sedang DITAMPILKAN di layar
Buffer B: Frame yang sedang DIRASTERISASI oleh GPU
VSync:
--> Tukar A dan B
--> B (yang sudah selesai) mulai ditampilkan
--> A (yang baru dikosongkan) mulai diisi frame berikutnya
Bottleneck dan Cara Mengidentifikasinya #
Memahami pipeline membantu mengidentifikasi di mana masalah performa terjadi:
| Gejala | Kemungkinan Bottleneck | Solusi |
|---|---|---|
| Jank saat animasi pertama | Shader compilation (Skia) | Upgrade ke Impeller |
| Rebuild berlebihan | Build phase | Gunakan const, pisahkan widget |
| Layout lambat | Layout phase terlalu dalam | Kurangi kedalaman widget tree |
| Overdraw | Paint phase | Hindari widget transparan bertumpuk |
| Frame drop konsisten | Raster phase | Kurangi efek kompleks, gunakan RepaintBoundary |
| UI thread blocked | Dart code terlalu berat | Pindahkan ke Isolate |
Cara Menggunakan Flutter DevTools untuk Analisis Pipeline #
Flutter DevTools > Performance tab
Timeline view menampilkan:
[Build] [Layout] [Paint] [Composite] - di UI Thread
[Raster] - di Raster Thread
Setiap bar > 16ms = frame drop (60 FPS)
Setiap bar > 8ms = frame drop (120 FPS)
Ringkasan #
- Rendering pipeline Flutter dipicu oleh VSync signal dan terdiri dari 5 fase: Build → Layout → Paint → Composite → Rasterize.
- Build phase menggunakan reconciliation (diffing) untuk hanya me-rebuild widget yang benar-benar berubah — proses ini bersifat sublinear.
- Layout phase menggunakan sistem constraint: “constraints go down, sizes go up, parent sets position.” Relayout boundary mengisolasi area yang di-relayout.
- Paint phase hanya merekam instruksi gambar ke Picture — belum menghasilkan piksel. Repaint boundary mengisolasi area yang di-repaint.
- Composite phase menyusun Picture menjadi Layer Tree menggunakan SceneBuilder — retained rendering memungkinkan reuse layer yang tidak berubah dari cache GPU.
- Rasterize phase berjalan di Raster Thread terpisah dari UI Thread — mengeksekusi instruksi gambar menjadi piksel aktual di GPU menggunakan Impeller atau Skia.
- Flutter menggunakan double buffering untuk mencegah visual tearing saat frame ditampilkan.
- Gunakan Flutter DevTools Performance tab untuk mengidentifikasi di fase mana bottleneck terjadi.
← Sebelumnya: Platform Embedder Berikutnya: Skia & Impeller →