Pengoptimalan performa SwiftUI untuk daftar panjang: perbaikan praktis
Penyempurnaan performa SwiftUI untuk daftar panjang: perbaikan praktis untuk re-render, identitas baris stabil, pagination, pemuatan gambar, dan kelancaran gulir di iPhone lama.

Seperti apa “daftar lambat” di aplikasi SwiftUI nyata
Sebuah “daftar lambat” di SwiftUI biasanya bukan bug. Itu terjadi saat UI Anda tidak dapat mengikuti jari. Anda merasakannya saat menggulir: daftar tersendat, frame drop, dan semuanya terasa berat.
Tanda-tanda umum:
- Guliran tersendat, terutama di perangkat lama
- Baris berkedip atau sesaat menampilkan konten yang salah
- Ketukan terasa tertunda, atau aksi geser terlambat
- Ponsel menjadi hangat dan baterai cepat terkuras
- Penggunaan memori meningkat semakin lama Anda menggulir
Daftar panjang bisa terasa lambat walau tiap baris terlihat “kecil,” karena biayanya bukan hanya menggambar pixel. SwiftUI masih harus menentukan apa tiap baris itu, menghitung layout, menyelesaikan font dan gambar, menjalankan kode format Anda, dan melakukan diff saat data berubah. Jika salah satu pekerjaan itu terjadi terlalu sering, daftar menjadi hotspot.
Juga berguna memisahkan dua ide. Di SwiftUI, “re-render” sering berarti body view dihitung ulang. Bagian itu biasanya murah. Pekerjaan mahal adalah apa yang dipicu oleh re-hitungan itu: layout berat, decoding gambar, pengukuran teks, atau membangun ulang banyak baris karena SwiftUI mengira identitasnya berubah.
Bayangkan chat dengan 2.000 pesan. Pesan baru datang setiap detik, dan tiap baris memformat timestamp, mengukur teks multi-baris, dan memuat avatar. Walau Anda hanya menambahkan satu item, perubahan state yang tidak terlokalisir bisa menyebabkan banyak baris mengevaluasi ulang, dan beberapa dari mereka harus digambar ulang.
Tujuannya bukan mikro-optimisasi. Anda ingin gulir yang lancar, ketukan instan, dan pembaruan yang hanya menyentuh baris yang benar-benar berubah. Perbaikan di bawah fokus pada identitas stabil, baris yang lebih murah, lebih sedikit pembaruan tak perlu, dan pemuatan yang terkontrol.
Penyebab utama: identitas, kerja per baris, dan badai update
Saat daftar SwiftUI terasa lambat, jarang karena “terlalu banyak baris.” Itu karena pekerjaan ekstra terjadi saat Anda menggulir: membangun ulang baris, menghitung ulang layout, atau memuat gambar berulang kali.
Sebagian besar penyebab utama masuk dalam tiga kategori:
- Identitas tidak stabil: baris tidak punya
idkonsisten, atau Anda memakai\\.selfuntuk nilai yang bisa berubah. SwiftUI tidak bisa mencocokkan baris lama ke yang baru, jadi ia membangun ulang lebih banyak dari yang diperlukan. - Terlalu banyak kerja per baris: pemformatan tanggal, penyaringan, mengubah ukuran gambar, atau pekerjaan jaringan/disk di dalam view baris.
- Badai update: satu perubahan (pengetikan, timer tick, update progress) memicu update state yang sering, dan daftar menyegarkan berulang kali.
Contoh: Anda punya 2.000 order. Tiap baris memformat mata uang, membuat attributed string, dan memulai fetch gambar. Sementara itu, timer “last synced” memperbarui sekali per detik di parent view. Walau data order tidak berubah, timer itu masih dapat meng-invalidate daftar cukup sering sehingga gulir terasa tersendat.
Mengapa List dan LazyVStack bisa terasa berbeda
List lebih dari sekadar scroll view. Itu dirancang sebagai perilaku table/collection dan optimisasi sistem. Seringkali menangani dataset besar dengan memori lebih efisien, tetapi sensitif terhadap identitas dan update yang sering.
ScrollView + LazyVStack memberi Anda kontrol lebih atas layout dan visual, tapi juga lebih mudah tanpa sengaja melakukan pekerjaan layout ekstra atau memicu update mahal. Di perangkat lama, kerja ekstra itu akan terlihat lebih cepat.
Sebelum Anda menulis ulang UI, ukur dulu. Perbaikan kecil seperti ID stabil, memindahkan pekerjaan dari baris, dan mengurangi churn state sering menyelesaikan masalah tanpa mengganti container.
Perbaiki identitas baris sehingga SwiftUI bisa diff efisien
Saat daftar panjang terasa tidak mulus, identitas sering jadi tersangka. SwiftUI memutuskan baris mana yang bisa digunakan ulang dengan membandingkan ID. Jika ID berubah, SwiftUI menganggap baris baru, membuang yang lama, dan membangun ulang lebih banyak dari yang diperlukan. Itu bisa terlihat seperti re-render acak, posisi scroll hilang, atau animasi yang terjadi tanpa alasan.
Kemenangan paling sederhana: buat id tiap baris stabil dan terkait ke sumber data Anda.
Kesalahan umum adalah menghasilkan identitas di dalam view:
ForEach(items) { item in
Row(item: item)
.id(UUID())
}
Ini memaksa ID baru setiap render, jadi tiap baris menjadi “berbeda” setiap kali.
Gunakan ID yang sudah ada di model Anda, seperti primary key database, ID server, atau slug stabil. Jika Anda tidak punya, buat sekali saat model dibuat — bukan di dalam view.
struct Item: Identifiable {
let id: Int
let title: String
}
List(items) { item in
Row(item: item)
}
Hati-hati dengan indeks. ForEach(items.indices, id: \\.self) mengikat identitas ke posisi. Jika Anda menyisipkan, menghapus, atau mengurutkan, baris “bergerak,” dan SwiftUI mungkin menggunakan ulang view yang salah untuk data yang salah. Gunakan indeks hanya untuk array yang benar-benar statis.
Jika Anda pakai id: \\.self, pastikan nilai Hashable elemen stabil seiring waktu. Jika hash berubah ketika field diupdate, identitas baris juga berubah. Aturan aman untuk Equatable dan Hashable: dasarkan pada satu ID stabil, bukan properti yang dapat diubah seperti name atau isSelected.
Pemeriksaan sederhana:
- ID berasal dari sumber data (bukan
UUID()di view) - ID tidak berubah saat konten baris berubah
- Identitas tidak bergantung pada posisi array kecuali daftar tidak pernah di-reorder
Kurangi re-render dengan membuat view baris lebih ringan
Daftar panjang sering terasa lambat karena tiap baris melakukan terlalu banyak pekerjaan setiap kali SwiftUI mengevaluasi ulang body-nya. Targetnya sederhana: buat tiap baris murah untuk dibangun ulang.
Biaya tersembunyi yang umum adalah mengoper nilai “besar” ke dalam baris. Struct besar, model sarang dalam, atau properti computed berat dapat memicu kerja ekstra meski UI terlihat tidak berubah. Anda mungkin membangun ulang string, parsing tanggal, mengubah ukuran gambar, atau membuat pohon layout kompleks lebih sering dari yang Anda duga.
Pindahkan pekerjaan mahal keluar dari body
Jika sesuatu lambat, jangan bangun ulang itu di dalam body baris berulang-ulang. Prekomputasi saat data tiba, cache di view model, atau memoize di helper kecil.
Biaya tingkat baris yang cepat menumpuk:
- Membuat
DateFormatteratauNumberFormatterbaru per baris - Pemformatan string berat di
body(join, regex, parsing markdown) - Membuat array turunan dengan
.mapatau.filterdi dalambody - Membaca blob besar dan mengkonversinya (mis. decoding JSON) di view
- Layout terlalu kompleks dengan banyak nested stacks dan conditional
Contoh sederhana: simpan formatter statis, dan kirim string yang sudah diformat ke baris.
enum Formatters {
static let shortDate: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium
f.timeStyle = .none
return f
}()
}
struct OrderRow: View {
let title: String
let dateText: String
var body: some View {
HStack {
Text(title)
Spacer()
Text(dateText).foregroundStyle(.secondary)
}
}
}
Pisahkan baris dan gunakan Equatable bila sesuai
Jika hanya satu bagian kecil yang berubah (mis. badge count), isolasikan ke subview sehingga sisa baris tetap stabil.
Untuk UI yang driven oleh nilai, membuat subview Equatable (atau membungkusnya dengan EquatableView) bisa membantu SwiftUI melewatkan pekerjaan saat input tidak berubah. Jaga input equatable kecil dan spesifik—jangan melakukan equatable pada seluruh model.
Kontrol update state yang memicu refresh seluruh daftar
Kadang baris baik-baik saja, tapi sesuatu terus memberitahu SwiftUI untuk menyegarkan seluruh daftar. Saat menggulir, bahkan update kecil yang berlebih bisa menjadi stutter, terutama di perangkat lama.
Salah satu penyebab umum adalah membuat ulang model terlalu sering. Jika parent view dibuat ulang dan Anda menggunakan @ObservedObject untuk view model yang dimiliki view itu, SwiftUI mungkin membuat ulangnya, mereset subscription, dan memicu publish baru. Jika view memiliki model, gunakan @StateObject sehingga dibuat sekali dan tetap stabil. Gunakan @ObservedObject untuk objek yang disuntik dari luar.
Pembunuh performa lain adalah publishing terlalu sering. Timer, pipeline Combine, dan update progress dapat memicu berkali-kali per detik. Jika properti yang dipublikasikan memengaruhi daftar (atau berada di ObservableObject yang dibagi oleh layar), setiap tick dapat menginvalidasi daftar.
Contoh: Anda punya field pencarian yang memperbarui query setiap ketikan, lalu memfilter 5.000 item. Jika Anda memfilter langsung, daftar berdiff terus saat pengguna mengetik. Debounce query, dan perbarui array terfilter setelah jeda singkat.
Polanya yang biasanya membantu:
- Jaga nilai yang sering berubah keluar dari objek yang menggerakkan daftar (gunakan objek yang lebih kecil atau
@Statelokal) - Debounce pencarian dan filtering sehingga daftar diperbarui setelah jeda ketikan
- Hindari publish timer frekuensi tinggi; perbarui lebih jarang atau hanya saat nilai benar-benar berubah
- Simpan state per-barang lokal (mis.
@Statedi baris) daripada satu nilai global yang berubah terus - Pecah model besar: satu
ObservableObjectuntuk data daftar, lain untuk state UI tingkat layar
Idenya sederhana: buat waktu scroll senyap. Jika tidak ada yang berubah penting, daftar tidak perlu diminta melakukan kerja.
Pilih container yang tepat: List vs LazyVStack
Container yang Anda pilih mempengaruhi seberapa banyak kerja yang dilakukan iOS untuk Anda.
List biasanya pilihan paling aman bila UI Anda mirip tabel standar: baris dengan teks, gambar, swipe actions, seleksi, separator, edit mode, dan aksesibilitas. Di bawahnya, ia mendapat optimisasi platform yang telah disetel Apple selama bertahun-tahun.
ScrollView dengan LazyVStack cocok saat Anda butuh layout kustom: kartu, blok konten campuran, header spesial, atau desain feed. “Lazy” berarti membangun baris saat muncul di layar, tapi tidak memberi perilaku yang sama persis seperti List. Dengan dataset sangat besar, itu bisa berarti penggunaan memori lebih tinggi dan gulir lebih tersendat di perangkat lama.
Aturan keputusan sederhana:
- Gunakan
Listuntuk layar bergaya tabel klasik: pengaturan, inbox, order, daftar admin - Gunakan
ScrollView+LazyVStackuntuk layout kustom dan konten campuran - Jika Anda punya ribuan item dan hanya butuh tabel, mulai dengan
List - Jika butuh kontrol pixel-perfect, coba
LazyVStack, lalu ukur memori dan frame drop
Juga perhatikan styling yang diam-diam memperlambat gulir. Efek per-barang seperti shadow, blur, dan overlay kompleks dapat memaksa kerja rendering ekstra. Jika Anda mau efek dalam, terapkan efek berat ke elemen kecil (mis. ikon) bukan seluruh baris.
Contoh konkret: layar “Orders” dengan 5.000 baris sering tetap mulus di List karena baris digunakan ulang. Jika Anda beralih ke LazyVStack dan membuat baris bergaya kartu dengan bayangan besar dan banyak overlay, Anda mungkin melihat jank walau kodenya tampak bersih.
Pagination yang terasa mulus dan menghindari lonjakan memori
Pagination membuat daftar panjang tetap cepat karena Anda merender lebih sedikit baris, menyimpan lebih sedikit model di memori, dan memberi SwiftUI pekerjaan diffing lebih sedikit.
Mulailah dengan kontrak paging yang jelas: ukuran halaman tetap (mis. 30–60 item), flag “tidak ada hasil lagi”, dan baris loading yang hanya muncul saat sedang fetch.
Perangkap umum adalah memicu halaman berikutnya hanya saat baris terakhir muncul. Itu sering terlambat, sehingga pengguna mencapai akhir dan melihat jeda. Sebagai gantinya, mulai loading ketika salah satu dari beberapa baris terakhir muncul.
Berikut pola sederhana:
@State private var items: [Item] = []
@State private var isLoading = false
@State private var reachedEnd = false
func loadNextPageIfNeeded(currentIndex: Int) {
guard !isLoading, !reachedEnd else { return }
let threshold = max(items.count - 5, 0)
guard currentIndex >= threshold else { return }
isLoading = true
Task {
let page = try await api.fetchPage(after: items.last?.id)
await MainActor.run {
let newUnique = page.filter { p in !items.contains(where: { $0.id == p.id }) }
items.append(contentsOf: newUnique)
reachedEnd = page.isEmpty
isLoading = false
}
}
}
Ini menghindari masalah umum seperti baris duplikat (hasil API yang tumpang tindih), kondisi race dari banyak pemanggilan onAppear, dan memuat terlalu banyak sekaligus.
Jika daftar Anda mendukung pull to refresh, reset state paging dengan hati-hati (kosongkan items, reset reachedEnd, batalkan task yang sedang berjalan bila memungkinkan). Jika Anda mengontrol backend, ID stabil dan paging berbasis cursor membuat UI terasa jauh lebih mulus.
Gambar, teks, dan layout: buat rendering baris tetap ringan
Daftar panjang jarang terasa lambat karena container daftar. Sebagian besar kasus, penyebabnya adalah baris. Gambar biasanya biang kerok: decoding, mengubah ukuran, dan menggambar bisa melampaui kecepatan scroll, apalagi di perangkat lama.
Jika Anda memuat gambar remote, pastikan pekerjaan berat tidak terjadi di main thread saat scroll. Juga hindari mendownload aset resolusi penuh untuk thumbnail 44–80 pt.
Contoh: layar “Messages” dengan avatar. Jika tiap baris mendownload gambar 2000x2000, memperkecilnya, lalu menerapkan blur atau shadow, daftar akan tersendat walau model datanya sederhana.
Jaga pekerjaan gambar tetap terprediksi
Kebiasaan berdampak tinggi:
- Gunakan thumbnail yang dihasilkan server atau pra-generate yang mendekati ukuran tampilan
- Decode dan resize di luar main thread bila memungkinkan
- Cache thumbnail sehingga scroll cepat tidak mengulang fetch atau decode
- Gunakan placeholder dengan ukuran final supaya tidak ada flicker atau lompatan layout
- Hindari modifier mahal pada gambar di baris (shadow berat, mask, blur)
Stabilisasi layout untuk menghindari thrash
SwiftUI bisa menghabiskan lebih banyak waktu mengukur daripada menggambar jika tinggi baris terus berubah. Usahakan baris yang dapat diprediksi: frame tetap untuk thumbnail, batasan baris teks konsisten, dan spasi stabil. Jika teks bisa meluas, batasi (mis. 1–2 baris) sehingga satu pembaruan tidak memaksa pengukuran ulang besar.
Placeholder juga penting. Lingkaran abu-abu yang nantinya menjadi avatar harus menempati frame yang sama, agar baris tidak reflow saat menggulir.
Cara mengukur: pemeriksaan Instruments yang mengungkap hambatan nyata
Pekerjaan performa hanya berdasarkan feeling membuat Anda menebak. Instruments memberi tahu apa yang berjalan di main thread, apa yang dialokasikan saat scroll cepat, dan apa yang menyebabkan frame drop.
Tentukan baseline di perangkat nyata (pilih perangkat lama jika Anda mendukungnya). Lakukan satu aksi yang bisa direplikasi: buka layar, gulir dari atas ke bawah cepat, trigger load-more sekali, lalu gulir kembali ke atas. Catat titik hitch terburuk, puncak memori, dan apakah UI tetap responsif.
Tiga view Instruments yang memberi nilai
Gunakan ini bersama-sama:
- Time Profiler: cari spike main-thread saat Anda menggulir. Layout, pengukuran teks, parsing JSON, dan decoding gambar di sini sering menjelaskan hitch.
- Allocations: perhatikan lonjakan objek sementara saat scroll cepat. Itu sering menunjuk ke formatting berulang, attributed string baru, atau membangun ulang model per-barang.
- Core Animation: konfirmasi dropped frames dan waktu frame panjang. Ini membantu memisahkan tekanan rendering dari pekerjaan data yang lambat.
Saat Anda menemukan spike, klik ke call tree dan tanyakan: apakah ini terjadi sekali per layar, atau sekali per baris, per scroll? Yang kedua adalah yang memecah kelancaran scroll.
Tambahkan signpost untuk event scroll dan pagination
Banyak aplikasi melakukan pekerjaan ekstra saat menggulir (image loads, pagination, filtering). Signpost membantu Anda melihat momen-momen itu di timeline.
import os
let log = OSLog(subsystem: "com.yourapp", category: "list")
os_signpost(.begin, log: log, name: "LoadMore")
// fetch next page
os_signpost(.end, log: log, name: "LoadMore")
Uji ulang setelah setiap perubahan, satu per satu. Jika FPS membaik tapi Allocations memburuk, Anda mungkin menukar stutter dengan tekanan memori. Simpan catatan baseline dan hanya pertahankan perubahan yang menggerakkan angka ke arah yang benar.
Kesalahan umum yang diam-diam membunuh performa daftar
Beberapa masalah jelas (gambar besar, dataset raksasa). Lainnya baru muncul saat data tumbuh, terutama di perangkat lama.
1) ID baris tidak stabil
Kesalahan klasik adalah membuat ID di dalam view, seperti id: \\.self untuk reference type, atau UUID() di body baris. SwiftUI menggunakan identitas untuk diff update. Jika ID berubah, SwiftUI menganggap baris baru, membangunnya ulang, dan mungkin membuang cache layout.
Gunakan ID stabil dari model (primary key database, ID server, atau UUID yang disimpan sekali saat item dibuat). Jika tidak punya, tambahkan satu.
2) Pekerjaan berat di onAppear
onAppear berjalan lebih sering dari perkiraan karena baris masuk dan keluar saat scroll. Jika tiap baris memulai decoding gambar, parsing JSON, atau lookup database di onAppear, Anda akan mendapat spike berulang.
Pindahkan pekerjaan berat keluar dari baris. Prekomputasi saat data dimuat, cache hasil, dan batasi onAppear ke aksi murah (mis. memicu pagination saat mendekati akhir).
3) Mengikat seluruh daftar ke edit baris
Ketika setiap baris mendapatkan @Binding ke array besar, edit kecil bisa terlihat seperti perubahan besar. Itu bisa menyebabkan banyak baris mengevaluasi ulang, dan kadang seluruh daftar menyegarkan.
Lebih baik mengoper nilai immutable ke baris dan mengirim perubahan kembali dengan aksi ringan (mis. “toggle favorite untuk id”). Simpan state per-barang di baris hanya bila memang tempatnya di sana.
4) Terlalu banyak animasi saat menggulir
Animasi mahal di daftar karena bisa memicu pass layout tambahan. Menerapkan animation(.default, value:) di level tinggi (pada seluruh daftar) atau menganimasikan tiap perubahan kecil bisa membuat gulir terasa lengket.
Jaga sederhana:
- Batasi animasi ke baris yang berubah saja
- Hindari animasi saat scroll cepat (terutama untuk selection/highlight)
- Hati-hati dengan implicit animation pada nilai yang sering berubah
- Pilih transisi sederhana daripada efek gabungan kompleks
Contoh nyata: daftar chat di mana tiap baris memulai fetch jaringan di onAppear, menggunakan UUID() untuk id, dan menganimasikan perubahan status “seen”. Kombinasi itu menciptakan churn baris konstan. Memperbaiki identitas, caching kerja, dan membatasi animasi sering membuat UI yang sama terasa langsung lebih mulus.
Daftar periksa cepat, contoh sederhana, dan langkah selanjutnya
Jika Anda hanya melakukan beberapa hal, mulai di sini:
- Gunakan
idunik dan stabil untuk tiap baris (bukan indeks array, bukan UUID yang dibuat ulang) - Buat pekerjaan baris sangat kecil: hindari format berat, pohon view besar, dan properti computed mahal di
body - Kontrol publish: jangan biarkan state yang sering berubah (timer, ketikan, progress) meng-invalidate seluruh daftar
- Muat dengan halaman dan prefetch sehingga memori tetap stabil
- Ukur sebelum dan sesudah dengan Instruments agar Anda tidak menebak
Bayangkan inbox support dengan 20.000 percakapan. Tiap baris menunjukkan subjek, preview pesan terakhir, timestamp, badge belum dibaca, dan avatar. Pengguna bisa mencari, dan pesan baru datang saat mereka menggulir. Versi lambat biasanya melakukan beberapa hal sekaligus: membangun ulang baris pada setiap ketikan, mengukur teks terlalu sering, dan mem-fetch terlalu banyak gambar terlalu awal.
Rencana praktis yang tidak mengharuskan merobek basis kode:
- Baseline: rekam scroll singkat dan sesi pencarian di Instruments (Time Profiler + Core Animation).
- Perbaiki identitas: pastikan model Anda punya id nyata dari server/database, dan
ForEachmenggunakannya secara konsisten. - Tambahkan paging: mulai dengan 50–100 item terbaru, lalu muat lebih banyak saat pengguna mendekati akhir.
- Optimalkan gambar: gunakan thumbnail lebih kecil, cache hasil, dan hindari decoding di main thread.
- Ukur ulang: konfirmasi lebih sedikit pass layout, lebih sedikit update view, dan waktu frame yang lebih stabil di perangkat lama.
Jika Anda membangun produk lengkap (app iOS plus backend dan panel admin web), membantu juga merancang model data dan kontrak paging sejak awal. Platform seperti AppMaster (appmaster.io) dibangun untuk alur kerja full-stack itu: Anda bisa mendefinisikan data dan logika bisnis secara visual, dan tetap menghasilkan source code nyata yang bisa Anda deploy atau self-host.
FAQ
Mulailah dengan memperbaiki identitas baris. Gunakan id stabil dari model Anda dan hindari menghasilkan ID di dalam view, karena perubahan ID membuat SwiftUI menganggap baris sebagai baru dan membangun ulang lebih sering dari yang diperlukan.
Rekomputasi body biasanya murah; yang mahal adalah apa yang dipicu olehnya. Layout berat, pengukuran teks, decoding gambar, dan membangun ulang banyak baris akibat identitas yang tidak stabil biasanya menyebabkan drop frame.
Jangan gunakan UUID() di dalam baris atau bergantung pada indeks array untuk identity jika data dapat disisipkan, dihapus, atau diurutkan ulang. Pilih ID dari server/database atau UUID yang disimpan di model saat dibuat, sehingga ID tetap sama antar pembaruan.
Bisa. Terutama jika nilai hash berubah saat properti yang bisa diedit berubah — SwiftUI mungkin melihatnya sebagai baris berbeda. Jika perlu menggunakan Hashable, dasarkan pada satu identifier yang stabil, bukan properti yang dapat berubah seperti name, isSelected, atau teks turunan.
Pindahkan pekerjaan berat keluar dari body. Format tanggal dan angka sebelumnya, hindari membuat formatter baru per baris, dan jangan membangun array turunan besar dengan map/filter di dalam view; hitung sekali di model atau view model dan kirim nilai siap-tampil ke baris.
onAppear berjalan lebih sering daripada yang diperkirakan karena baris muncul dan hilang saat menggulir. Jika tiap baris memulai pekerjaan berat di onAppear (decoding gambar, baca database, parsing), Anda akan mendapat spike berulang; batasi onAppear untuk tugas ringan seperti memicu pagination saat mendekati akhir.
Nilai yang sering berubah dan dipublish bersama daftar dapat membuatnya terinvalidasi berulang kali, walau data baris tidak berubah. Jaga agar timer, state pengetikan, dan progress update tidak berada di objek utama yang menggerakkan daftar; debounce pencarian; dan pecah ObservableObject besar menjadi beberapa bagian jika perlu.
Gunakan List saat UI Anda bersifat tabel: baris standar, swipe action, seleksi, separator—karena List mendapat optimisasi platform. Gunakan ScrollView + LazyVStack saat butuh layout kustom, tapi ukur memori dan dropped frames karena mudah melakukan pekerjaan layout ekstra.
Mulailah memuat lebih awal daripada menunggu baris terakhir muncul — mulai saat pengguna mencapai threshold mendekati akhir, dan lindungi dari trigger duplikat. Pilih ukuran halaman yang wajar, lacak isLoading dan reachedEnd, dan deduplikasi hasil dengan ID stabil untuk menghindari baris duplikat dan diff ekstra.
Buat baseline di perangkat nyata dan gunakan Instruments untuk menemukan spike main-thread dan lonjakan alokasi saat scroll cepat. Time Profiler menunjukkan apa yang menghalangi scroll, Allocations mengungkap churn per-barang, dan Core Animation mengonfirmasi dropped frames sehingga Anda tahu apakah bottleneck di rendering atau pekerjaan data.


