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:

GejalaKemungkinan BottleneckSolusi
Jank saat animasi pertamaShader compilation (Skia)Upgrade ke Impeller
Rebuild berlebihanBuild phaseGunakan const, pisahkan widget
Layout lambatLayout phase terlalu dalamKurangi kedalaman widget tree
OverdrawPaint phaseHindari widget transparan bertumpuk
Frame drop konsistenRaster phaseKurangi efek kompleks, gunakan RepaintBoundary
UI thread blockedDart code terlalu beratPindahkan 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 →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact