Rendering Pipeline #
Setiap kali kita melihat transisi tombol yang memudar, halaman yang digulir, atau animasi kustom berjalan dengan lancar pada aplikasi Flutter, ada serangkaian proses rekayasa grafis terkoordinasi yang berjalan di latar belakang. Rangkaian proses sistematis ini disebut Rendering Pipeline — jalur perakitan data yang ditempuh dari baris kode Dart deklaratif yang kita tulis hingga menjadi deretan piksel fisik berwarna pada layar perangkat keras pengguna. Memahami setiap detail fase di dalam rendering pipeline adalah kunci utama bagi kita untuk menulis kode aplikasi berkinerja tinggi, menghindari hambatan performa (bottlenecks), dan mendiagnosis secara tepat penyebab terjadinya frame drop (tersendat) pada perangkat berlayar 60Hz maupun 120Hz.
Gambaran Besar Pipeline #
Rendering pipeline Flutter dirancang sebagai jalur perakitan dua thread utama yang bekerja secara sinkron dan paralel: UI Thread (yang menjalankan kode Dart dan Framework) dan Raster Thread (yang menjalankan C++ Engine dan kartu grafis GPU).
Secara garis besar, jalur perakitan ini dibagi menjadi lima fase utama yang berurutan:
flowchart TD
subgraph UI_Thread["UI Thread (Fase Framework Dart)"]
direction LR
Build["1. BUILD\n(Widget Tree)"] --> Layout["2. LAYOUT\n(Constraints & Size)"]
Layout --> Paint["3. PAINT\n(Rekam Gambar Canvas)"]
Paint --> Composite["4. COMPOSITE\n(Rakit Layer Tree)"]
end
subgraph Raster_Thread["Raster Thread (Fase C++ Engine & GPU)"]
Rasterize["5. RASTERIZE\n(Piksel di GPU Buffer)"]
end
Composite -->|"Kirim Scene via dart:ui"| Rasterize
style UI_Thread stroke:#0288d1,stroke-width:2px
style Raster_Thread stroke:#388e3c,stroke-width:2pxFase 1 sampai 4 diselesaikan sepenuhnya di dalam UI Thread menggunakan bahasa Dart. Setelah fase Composite selesai merakit representasi visual aplikasi, ia mengirimkan data biner tersebut ke C++ Engine pada Raster Thread untuk mengeksekusi fase 5 (Rasterize) langsung di perangkat keras kartu grafis.
VSync: Pemicu Setiap Frame #
Seluruh rendering pipeline Flutter bersifat pasif dan reaktif. Artinya, Flutter tidak akan memboroskan daya baterai perangkat dengan menggambar frame baru terus-menerus jika tidak ada perubahan state atau interaksi. Pemicu utama yang membangunkan dan menjalankan siklus pipeline ini adalah sinyal perangkat keras yang disebut VSync (Vertical Synchronization).
VSync adalah sinyal detak yang dikirimkan oleh unit display layar fisik kepada sistem operasi untuk memberitahukan bahwa layar siap menyajikan gambar baru.
- Layar 60Hz: Mengirim sinyal VSync setiap 16,67 milidetik sekali.
- Layar 120Hz: Mengirim sinyal VSync setiap 8,33 milidetik sekali.
flowchart LR
subgraph Display_60Hz["Display Refresh Rate (60Hz)"]
direction LR
V1["VSync 1 (0ms)"] -->|"Frame 1 (16.6ms)"| V2["VSync 2 (16.6ms)"]
V2 -->|"Frame 2 (16.6ms)"| V3["VSync 3 (33.3ms)"]
end
subgraph Display_120Hz["Display Refresh Rate (120Hz)"]
direction LR
W1["VSync 1 (0ms)"] -->|"Frame 1 (8.3ms)"| W2["VSync 2 (8.3ms)"]
W2 -->|"Frame 2 (8.3ms)"| W3["VSync 3 (16.6ms)"]
W3 -->|"Frame 3 (8.3ms)"| W4["VSync 4 (25.0ms)"]
end
style Display_60Hz stroke:#0288d1,stroke-width:2px
style Display_120Hz stroke:#388e3c,stroke-width:2pxKetika kita memicu perubahan state (misalnya memanggil setState), Flutter mendaftarkan permintaan frame berikutnya ke platform native. Saat sinyal VSync berikutnya dikirim oleh OS, Platform Embedder langsung meneruskannya ke Engine, yang kemudian memicu panggilan callback drawFrame() pada pustaka binding Framework. Dari sinilah, siklus perakitan rendering dimulai.
Build Phase — Membangun Widget Tree #
Build phase adalah tahap pertama di mana baris kode Dart kita dieksekusi di dalam UI Thread. Tugas utama fase ini adalah menyusun konfigurasi struktur antarmuka pengguna dalam bentuk pohon widget (Widget Tree).
Alur Kerja Pemicu Build #
Ketika kita memanggil fungsi setState(), Flutter menandai Element yang terkait dengan widget tersebut sebagai kotor (dirty). Element kotor ini dimasukkan ke dalam antrean dirty list yang dikelola oleh objek BuildOwner.
Saat siklus frame dimulai akibat detak VSync:
BuildOwnermemproses antrean dirty list dari atas ke bawah (menghindari proses pengulangan ganda).BuildOwnermemanggil metodebuild()pada setiap widget yang terdaftar sebagai dirty.- Fungsi
build()mengeksekusi kode Dart deklaratif baru untuk menghasilkan pohon konfigurasi widget yang baru.
Proses Reconciliation (Diffing Tree) #
Membangun ribuan objek widget baru setiap frame terdengar sangat lambat dan boros memori, namun Flutter memecahkannya melalui mekanisme Reconciliation (rekonsiliasi). Elemen aktual yang menetap di memori sebenarnya adalah Element Tree. Ketika widget baru selesai dibuat pada Build phase, Flutter membandingkan konfigurasi widget baru dengan elemen lama di memori menggunakan aturan efisien:
flowchart TD
NewWidget["Widget Baru"] --> Compare{"type & key sama dengan Widget Lama?"}
Compare -->|"Ya (canUpdate == true)"| Update["Perbarui Elemen yang Ada (Retain State)"]
Compare -->|"Tidak (canUpdate == false)"| Replace["Hancurkan Elemen & RenderObject Lama\nBuat Baru (Reset State)"]
style Compare stroke:#0288d1,stroke-width:2pxJika tipe kelas widget dan kunci identitas (Key) bernilai sama, elemen lama dipertahankan di memori dan hanya memperbarui referensi properti konfigurasinya saja. Proses diffing ini memiliki kompleksitas waktu sublinear $O(N)$ di mana $N$ adalah jumlah widget aktif yang mengalami perubahan.
Optimasi Menggunakan Kata KunciconstSaat kita membungkus instansiasi widget menggunakan kata kunciconst(misalnyaconst Text('Label Statis')), kita menginstruksikan compiler Dart untuk membuat objek tersebut secara permanen di memori compile-time. Ketika parent widget mengalami rebuild, Flutter mendeteksi kesamaan referensi instansiconsttersebut secara instan dan memintas (skip) seluruh proses build untuk widget itu beserta seluruh anak-anaknya, sehingga menghemat daya CPU secara signifikan.
Layout Phase — Menghitung Ukuran dan Posisi #
Setelah struktur pohon visual disepakati pada Build phase, Flutter harus menentukan dimensi spasial fisik dari setiap elemen visual di layar. Proses kalkulasi ini berjalan pada RenderObject Tree (bukan Widget Tree) di bawah pengelolaan PipelineOwner.
Siklus Constraints Go Down, Sizes Go Up #
Kalkulasi tata letak di Flutter berjalan dalam sistem lintasan tunggal (single-pass layout) yang sangat cepat dari atas ke bawah untuk menghindari perhitungan ulang ganda (backtracking). Proses ini mengikuti aturan tiga tahap:
sequenceDiagram
autonumber
participant Parent as Screen (Parent)
participant Padding as Padding RenderObject
participant Text as Text RenderObject
Parent->>Padding: Kirim Constraints (maxW: 360, maxH: 640)
Note over Padding: Kurangi padding (misal 16px di setiap sisi)
Padding->>Text: Kirim Constraints baru (maxW: 328, maxH: 608)
Note over Text: Hitung dimensi teks berdasarkan font & karakter
Text-->>Padding: Kembalikan Size (width: 120, height: 20)
Note over Padding: Tambahkan ukuran padding (120+32, 20+32)
Padding-->>Parent: Kembalikan Size baru (width: 152, height: 52)
Parent->>Padding: Tentukan Posisi Offset (0, 0)
Padding->>Text: Tentukan Posisi Offset (16, 16)- Constraints Go Down: Induk mengirimkan batas ukuran maksimum dan minimum (
BoxConstraints) kepada anaknya. - Sizes Go Up: Anak menghitung ukuran aktualnya sendiri berdasarkan batasan induk (misalnya teks mengukur panjang kata-katanya) dan mengembalikan ukuran dimensi tersebut (
Size) ke atas kepada induknya. - Parent Sets Position: Induk menentukan posisi koordinat anak (
Offset) di layar berdasarkan geometri ruang yang tersedia. Anak tidak boleh menempatkan koordinat dirinya sendiri secara sepihak.
Isolasi Tata Letak: Relayout Boundary #
Sama seperti build, Flutter meminimalkan perhitungan tata letak ulang menggunakan konsep Relayout Boundary. Jika suatu widget mengalami perubahan dimensi (misalnya widget teks memanjang karena menerima karakter baru), perubahan ini dapat memicu layout ulang ke atas pohon induk.
Untuk menghentikan perambatan layout ulang ini, Flutter mendirikan Relayout Boundary di sekitar widget tertentu (seperti widget berukuran tetap SizedBox atau widget fleksibel Expanded). Batasan ini mengunci perambatan layout ulang agar tidak keluar dari areanya, sehingga mengisolasi beban kerja hanya pada sub-pohon yang kotor saja.
Paint Phase — Merekam Instruksi Gambar #
Paint phase bertanggung jawab untuk menyusun instruksi grafis yang mendefinisikan tampilan visual dari setiap elemen.
Mitos Paint: Paint Tidak Menggambar Piksel #
Ada satu kesalahpahaman umum yang sering terjadi: banyak pengembang mengira bahwa Paint phase di Flutter bertugas langsung untuk mengubah warna piksel layar. Ini keliru. Paint phase tidak pernah menggambar piksel.
Fase ini murni bertugas merekam perintah grafis ke dalam objek biner display list yang disebut Picture. Piksel aktual baru akan diproduksi oleh kartu grafis GPU pada fase terakhir (Rasterize).
Sebagai contoh, output dari Paint phase adalah serangkaian instruksi biner seperti berikut:
- Instruksi 1: Simpan status kanvas saat ini.
- Instruksi 2: Gambar bentuk persegi panjang pada koordinat $(10, 10)$ dengan warna biru.
- Instruksi 3: Gambar teks “Halo” di posisi $(20, 20)$ menggunakan font kustom.
- Instruksi 4: Kembalikan status kanvas awal.
PaintingContext & Canvas #
Selama fase ini berjalan, setiap RenderObject menerima objek pengontrol bernama PaintingContext yang mengekspos objek Canvas. Kita dapat menulis instruksi gambar secara langsung dengan menimpa metode paint di tingkat RenderObject atau CustomPainter:
class MyCustomRenderBox extends RenderBox {
@override
void paint(PaintingContext context, Offset offset) {
// Canvas bertindak sebagai perekam instruksi biner
final canvas = context.canvas;
final paint = Paint()
..color = const Color(0xFFE91E63)
..style = PaintingStyle.fill;
// Merekam perintah penggambaran lingkaran pink
canvas.drawCircle(
offset + Offset(size.width / 2, size.height / 2),
size.width / 3,
paint,
);
}
}
Repaint Boundary: Isolasi Paint #
Untuk performa animasi yang mulus, kita harus menggunakan RepaintBoundary. Saat sebuah widget mengalami pembaruan tampilan visual, secara default seluruh area halaman aplikasi akan ikut digambar ulang oleh sistem.
Dengan menyelubungi widget dinamis tersebut menggunakan RepaintBoundary, kita mendirikan pembatas tampilan yang memicu pembuatan lapisan (layer) visual terpisah di memori. Ketika widget di dalam pembatas tersebut digambar ulang, widget statis lainnya di luar pembatas diabaikan, menghemat kerja CPU untuk merekam ulang instruksi gambar yang tidak berubah.
Composite Phase — Menyusun Layer Tree #
Setelah Paint phase selesai merekam seluruh instruksi visual ke dalam objek Picture, Flutter mengumpulkan seluruh rekaman gambar tersebut berdampingan dengan informasi dekoratif (seperti transparansi, pemotongan area, dan efek bayangan) untuk dirakit menjadi Layer Tree (Pohon Lapisan).
Mengapa Kita Butuh Layer Tree? #
Pemisahan visual menjadi beberapa lapisan (layers) sangat krusial untuk mendukung konsep Retained Rendering. Ketika aplikasi kita berpindah ke frame berikutnya, kartu grafis GPU tidak perlu memproses ulang seluruh gambar dari awal.
- Jika sebuah lapisan tidak mengalami perubahan visual (misalnya area latar belakang statis), GPU dapat langsung memanggil cache tekstur yang sudah ada di memorinya.
- Hanya lapisan yang berubah saja (seperti area kursor berkedip) yang akan diperbarui instruksinya.
Komposisi Akhir via SceneBuilder #
Setelah seluruh lapisan visual (seperti PictureLayer, OpacityLayer, TransformLayer, dan OffsetLayer) tersusun secara hierarkis, objek SceneBuilder dipicu untuk merakit pohon tersebut menjadi objek tunggal terpadu yang siap dikonsumsi oleh kartu grafis perangkat bernama Scene.
flowchart TD
subgraph LayerTree["Pohon Lapisan (Layer Tree)"]
direction TB
Root["OffsetLayer (Root)"]
Transform["TransformLayer (Scroll/Animation)"]
PictureBG["PictureLayer (Background)"]
RepaintL["OffsetLayer (RepaintBoundary)"]
PictureAnim["PictureLayer (Animasi)"]
PictureStatic["PictureLayer (Teks Statis)"]
Root --> Transform
Transform --> PictureBG
Transform --> RepaintL
RepaintL --> PictureAnim
Transform --> PictureStatic
end
LayerTree -->|"SceneBuilder"| Scene["ui.Scene (Biner Terkomposisi)"]
Scene -->|"window.render()"| Engine["Engine C++ (Flow Compositor)"]
style LayerTree stroke:#f57c00,stroke-width:2pxBegitu objek Scene selesai dirakit oleh SceneBuilder, Framework mengirimkan biner scene ini ke Engine melalui pemanggilan metode ui.Window.render(scene). Langkah ini menandai berakhirnya seluruh tugas UI Thread Dart pada frame tersebut.
Rasterize Phase — Menghasilkan Piksel #
Fase terakhir ini berpindah sepenuhnya ke Raster Thread dan dijalankan oleh subsistem C++ Engine (menggunakan Impeller atau Skia) yang berkomunikasi langsung dengan kartu grafis GPU perangkat.
Alur Eksekusi Kartu Grafis #
Begitu Raster Thread menerima data biner Scene dari UI Thread:
- Engine memecah Layer Tree menjadi serangkaian operasi menggambar kartu grafis.
- Engine mengambil shader AOT yang telah dikompilasi sebelumnya oleh Impeller.
- Engine mengeksekusi panggilan instruksi gambar GPU (GPU Draw Calls) menggunakan driver platform native (seperti Vulkan untuk Android atau Metal untuk iOS).
- GPU merasterisasi gambar tersebut menjadi piksel berwarna aktual ke dalam Framebuffer.
Mekanisme Pengamanan Layar: Double Buffering #
Untuk mencegah terjadinya cacat tampilan robek (screen tearing) — kondisi di mana layar menampilkan setengah gambar lama dan setengah gambar baru secara bersamaan — Flutter menerapkan teknik Double Buffering:
flowchart LR
subgraph Buffers["Double Buffering Mechanism"]
direction LR
BufA["Buffer A (Ditampilkan di Layar)"]
BufB["Buffer B (Dirasterisasi oleh GPU)"]
VSync{"VSync Signal?"} -->|"Ya (Tukar Buffer)"| Swap["Tukar Buffer (Swap)"]
Swap -->|"B menjadi Layar"| BufA_New["Buffer B (Ditampilkan)"]
Swap -->|"A dikosongkan untuk frame baru"| BufB_New["Buffer A (Dirasterisasi)"]
end
style Buffers stroke:#0288d1,stroke-width:2px- Buffer A (Front Buffer): Menyimpan frame visual yang saat ini sedang disajikan secara aktif ke mata pengguna di layar fisik.
- Buffer B (Back Buffer): Menyimpan area memori tempat GPU sedang merasterisasi piksel untuk frame berikutnya secara paralel.
Ketika sinyal VSync dikirim oleh perangkat keras layar, sistem secara instan menukar peran kedua buffer tersebut (buffer swapping). Gambar di Buffer B yang telah selesai dirasterisasi disajikan ke layar, sementara Buffer A dikosongkan untuk menampung proses rasterisasi frame selanjutnya.
Bottleneck dan Cara Mengidentifikasinya #
Masing-masing fase rendering pipeline memiliki potensi mengalami kelebihan beban kerja (overhead) yang dapat memicu penurunan performa visual. Tabel di bawah ini merangkum jenis hambatan kerja (bottlenecks), gejala visual, serta langkah solusinya:
| Fase Pipeline | Gejala Masalah Performa | Kemungkinan Penyebab | Solusi Optimal |
|---|---|---|---|
| Build | UI tersendat saat membuka halaman baru yang kompleks. | Logika parsing JSON berat atau komputasi matematika berjalan di UI Thread. | Pindahkan kalkulasi berat ke Dart Isolate latar belakang menggunakan compute(). |
| Build | Penurunan performa visual secara konstan saat widget kecil diperbarui. | Rebuild yang berlebihan pada widget tree yang tidak berubah. | Gunakan const constructor dan pisahkan widget dinamis kecil menjadi kelas StatelessWidget terpisah. |
| Layout | Penggunaan CPU yang tinggi dengan pesan log “performing layout” berulang. | Struktur widget tree terlalu dalam atau terjadi pemformatan ukuran berulang (layout passes). | Batasi kedalaman layout dan gunakan widget tata letak sederhana seperti SizedBox alih-alih Container. |
| Paint | GPU usage tinggi saat menampilkan daftar dinamis (scrolling). | Paint ulang yang tidak perlu pada widget statis di sebelah widget animasi. | Bungkus widget dinamis atau area visual terpisah menggunakan RepaintBoundary. |
| Rasterize | Efek visual buram (blur) atau bayangan (shadow) terasa tersendat di perangkat lama. | Efek grafis yang terlalu kompleks membebani kerja GPU. | Kurangi penggunaan filter visual berat atau ganti engine grafis lama Skia menjadi Impeller. |
Analisis Pipeline Menggunakan DevTools Performance Tab #
Kita dapat melacak performa rendering pipeline secara presisi menggunakan panel Performance pada Flutter DevTools. DevTools menampilkan grafik visual batang untuk setiap frame:
- Batang bagian atas menampilkan performa UI Thread (mencakup fase Build, Layout, Paint, dan Composite).
- Batang bagian bawah menampilkan performa Raster Thread (fase Rasterize).
Jika tinggi salah satu batang tersebut melebihi batas garis horizontal 16,6ms (pada layar 60Hz) atau 8,3ms (pada layar 120Hz), kita tahu persis di thread mana frame drop terjadi dan dapat mengarahkan langkah optimasi secara terfokus.
Ringkasan #
- Pemicu Utama VSync — Siklus rendering pipeline Flutter dibangunkan secara reaktif oleh detak sinyal VSync dari layar fisik perangkat.
- Lima Fase Berurutan — Proses pemrosesan data visual terbagi menjadi: Build → Layout → Paint → Composite → Rasterize.
- Reconciliation Sublinear — Fase Build membandingkan pohon widget baru dengan elemen lama di memori untuk menghemat rendering menggunakan kompleksitas waktu $O(N)$.
- Layout Lintasan Tunggal — Fase Layout menentukan dimensi spasial RenderObject menggunakan sistem satu arah: “constraints go down, sizes go up, parent sets position.”
- Perekaman Paint — Fase Paint murni bertugas merekam perintah instruksi grafis 2D ke dalam biner
Picture, bukan menggambar piksel visual secara langsung.- Penyusunan Layer Tree — Fase Composite mengumpulkan picture visual terisolasi menjadi struktur Layer Tree terpadu demi mendukung retained rendering.
- Rasterisasi di GPU — Rasterize phase berjalan secara paralel di Raster Thread asinkron untuk merasterisasi Layer Tree menjadi piksel di memori GPU.
- Double Buffering & DevTools — Menerapkan buffer ganda untuk mencegah robeknya gambar (tearing) dan menyediakan panel visual Performance di DevTools untuk mendiagnosis jank.
← Sebelumnya: Platform Embedder Berikutnya: Skia & Impeller →