17 thg 11, 2025·8 phút đọc

Tối ưu hiệu năng SwiftUI cho danh sách dài: các sửa thực tế

Tối ưu hiệu năng SwiftUI cho danh sách dài: các cách khắc phục thực tế cho re-render, danh tính hàng ổn định, phân trang, tải ảnh và cuộn mượt trên iPhone cũ.

Tối ưu hiệu năng SwiftUI cho danh sách dài: các sửa thực tế

Giao diện “danh sách chậm” trông thế nào trong app SwiftUI thực tế

Một “danh sách chậm” trong SwiftUI thường không phải là lỗi. Đó là khi giao diện không theo kịp ngón tay bạn. Bạn thấy điều đó khi cuộn: danh sách giật, số khung rớt, và mọi thứ cảm thấy nặng nề.

Dấu hiệu điển hình:

  • Cuộn bị giật, đặc biệt trên thiết bị cũ hơn
  • Hàng nhấp nháy hoặc thoáng hiện nội dung sai
  • Nhấn cảm thấy chậm, hoặc các hành động vuốt bắt đầu muộn
  • Máy nóng lên và pin tụt nhanh hơn mong đợi
  • Bộ nhớ tăng dần theo thời gian cuộn

Danh sách dài có thể cảm thấy chậm ngay cả khi mỗi hàng trông “nhỏ”, vì chi phí không chỉ là vẽ điểm ảnh. SwiftUI vẫn phải xác định mỗi hàng là gì, tính toán layout, giải quyết font và ảnh, chạy mã định dạng của bạn, và diff các cập nhật khi dữ liệu thay đổi. Nếu bất kỳ công việc nào trong số đó xảy ra quá thường xuyên, danh sách sẽ trở thành điểm nóng.

Cũng nên tách hai ý: trong SwiftUI, “re-render” thường có nghĩa là body của view được tính lại. Phần đó thường rẻ. Công việc tốn kém là những gì tính lại đó kích hoạt: layout nặng, giải mã ảnh, đo văn bản, hoặc dựng lại nhiều hàng vì SwiftUI nghĩ danh tính của chúng đã thay đổi.

Hãy tưởng tượng một chat với 2.000 tin nhắn. Tin mới tới mỗi giây, và mỗi hàng định dạng timestamp, đo text nhiều dòng, và tải avatar. Ngay cả khi bạn chỉ thêm một mục, một thay đổi trạng thái bị scope sai có thể khiến nhiều hàng đánh giá lại, và một số trong đó phải vẽ lại.

Mục tiêu không phải micro-optimization. Bạn muốn cuộn mượt, nhấn phản hồi ngay lập tức, và cập nhật chỉ chạm tới những hàng thực sự thay đổi. Các sửa dưới đây tập trung vào danh tính ổn định, hàng nhẹ hơn, giảm cập nhật không cần thiết, và kiểm soát tải tài nguyên.

Những nguyên nhân chính: danh tính, công việc trên mỗi hàng, và cơn bão cập nhật

Khi một danh sách SwiftUI cảm thấy chậm, hiếm khi là “quá nhiều hàng”. Đó là công việc thừa xảy ra khi bạn cuộn: dựng lại hàng, tính toán lại layout, hoặc tải ảnh đi tải lại.

Hầu hết nguyên nhân gốc rơi vào ba nhóm:

  • Danh tính không ổn định: hàng không có id nhất quán, hoặc bạn dùng \.self cho giá trị có thể thay đổi. SwiftUI không thể ghép hàng cũ với hàng mới, nên nó dựng lại nhiều hơn cần.
  • Quá nhiều công việc trên mỗi hàng: định dạng ngày, lọc, thay đổi kích thước ảnh, hoặc thực hiện công việc mạng/đĩa trong view hàng.
  • Cơn bão cập nhật: một thay đổi (gõ, timer tick, tiến trình) kích hoạt các cập nhật trạng thái tần suất cao, và danh sách làm mới liên tục.

Ví dụ: bạn có 2.000 đơn hàng. Mỗi hàng định dạng tiền tệ, xây dựng attributed string, và bắt một fetch ảnh. Trong khi đó, một timer “last synced” cập nhật mỗi giây ở view cha. Ngay cả khi dữ liệu đơn hàng không đổi, timer đó vẫn có thể invalidate danh sách đủ thường để khiến cuộn bị giật.

Tại sao ListLazyVStack lại khác cảm nhận

List nhiều hơn là một scroll view. Nó được thiết kế quanh hành vi table/collection và các tối ưu hệ thống. Nó thường xử lý dataset lớn với bộ nhớ ít hơn, nhưng có thể nhạy cảm với danh tính và cập nhật thường xuyên.

ScrollView + LazyVStack cho bạn kiểm soát nhiều hơn về layout và hiển thị, nhưng cũng dễ làm vô tình thêm công việc layout hoặc kích hoạt cập nhật tốn kém. Trên các thiết bị cũ hơn, công việc thừa đó thể hiện sớm hơn.

Trước khi viết lại UI, đo trước. Những sửa nhỏ như ID ổn định, dời công việc ra khỏi hàng, và giảm trạng thái thay đổi thường giải quyết vấn đề mà không thay container.

Sửa danh tính hàng để SwiftUI diff hiệu quả

Khi một danh sách dài cảm thấy giật, danh tính thường là thủ phạm. SwiftUI quyết định hàng nào có thể tái sử dụng bằng cách so sánh ID. Nếu ID thay đổi, SwiftUI coi hàng là mới, vứt bỏ hàng cũ, và dựng lại nhiều hơn cần. Điều đó có thể trông như re-render ngẫu nhiên, mất vị trí cuộn, hoặc animation bật ra không lý do.

Chiến thắng đơn giản nhất: làm cho id của mỗi hàng ổn định và gắn với nguồn dữ liệu của bạn.

Một lỗi phổ biến là sinh ID bên trong view:

ForEach(items) { item in
  Row(item: item)
    .id(UUID())
}

Điều này ép mỗi lần render có ID mới, nên mỗi hàng trở thành “khác” mỗi lần.

Ưu tiên ID đã có trong model, như khóa chính DB, ID server, hoặc slug ổn định. Nếu bạn không có, tạo nó một lần khi tạo model — không phải trong view.

struct Item: Identifiable {
  let id: Int
  let title: String
}

List(items) { item in
  Row(item: item)
}

Cẩn thận với chỉ số. ForEach(items.indices, id: \.self) gắn danh tính với vị trí. Nếu bạn chèn, xóa, hoặc sắp xếp, hàng sẽ “di chuyển”, và SwiftUI có thể tái sử dụng view sai cho dữ liệu sai. Dùng indices chỉ cho mảng thật sự tĩnh.

Nếu bạn dùng id: \.self, đảm bảo giá trị Hashable của phần tử ổn định theo thời gian. Nếu hash thay đổi khi một trường cập nhật, danh tính hàng cũng thay đổi. Quy tắc an toàn cho EquatableHashable: dựa trên một ID ổn định duy nhất, không phải các thuộc tính có thể sửa như name hoặc isSelected.

Kiểm tra nhanh:

  • ID đến từ nguồn dữ liệu (không phải UUID() trong view)
  • ID không thay đổi khi nội dung hàng thay đổi
  • Danh tính không phụ vào vị trí mảng trừ khi danh sách không bao giờ xáo trộn

Giảm re-render bằng cách làm cho view hàng rẻ hơn

Danh sách dài thường cảm thấy chậm vì mỗi hàng làm quá nhiều công việc mỗi khi SwiftUI đánh giá lại body. Mục tiêu là làm cho mỗi hàng rẻ để dựng lại.

Một chi phí ẩn phổ biến là truyền giá trị “lớn” vào hàng. Struct lớn, model lồng nhau sâu, hoặc thuộc tính tính toán nặng có thể kích hoạt công việc thừa ngay cả khi UI không thay đổi. Bạn có thể đang dựng lại chuỗi, parse ngày, thay đổi kích thước ảnh, hoặc sinh cây layout phức tạp nhiều hơn bạn nghĩ.

Dời công việc nặng ra khỏi body

Nếu cái gì đó chậm, đừng dựng lại nó trong body hàng nhiều lần. Tính trước khi dữ liệu tới, cache trong view model, hoặc memoize trong helper nhỏ.

Chi phí ở cấp hàng cộng dồn nhanh:

  • Tạo DateFormatter hoặc NumberFormatter mới cho mỗi hàng
  • Định dạng chuỗi nặng trong body (join, regex, parse markdown)
  • Xây mảng dẫn xuất với .map hoặc .filter trong body
  • Đọc blob lớn và chuyển đổi (như decode JSON) trong view
  • Layout quá phức tạp với nhiều stack lồng nhau và điều kiện

Ví dụ đơn giản: giữ formatter là static, và truyền chuỗi đã được định dạng vào hàng.

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)
        }
    }
}

Tách các phần của hàng và dùng Equatable khi phù hợp

Nếu chỉ một phần nhỏ thay đổi (như badge count), tách nó thành subview để phần còn lại của hàng giữ nguyên.

Với UI thực sự dựa trên giá trị, làm subview Equatable (hoặc bọc với EquatableView) có thể giúp SwiftUI bỏ qua công việc khi input không đổi. Giữ input equatable nhỏ và cụ thể — không phải toàn bộ model.

Kiểm soát cập nhật gây làm mới toàn bộ danh sách

Từ UI đến full stack
Phát hành app SwiftUI sẵn sàng production với backend Go và web admin panel bằng Vue3.
Generate Code

Đôi khi hàng không vấn đề, mà là thứ gì đó liên tục bảo SwiftUI làm mới cả danh sách. Khi cuộn, ngay cả các cập nhật nhỏ thừa cũng có thể biến thành giật, đặc biệt trên thiết bị cũ.

Nguyên nhân phổ biến là tái tạo model quá thường xuyên. Nếu view cha dựng lại và bạn dùng @ObservedObject cho view model mà view sở hữu, SwiftUI có thể tái tạo nó, đặt lại subscription, và kích hoạt publish mới. Nếu view sở hữu model, dùng @StateObject để tạo một lần và giữ ổn định. Dùng @ObservedObject cho object được inject từ bên ngoài.

Một sát thủ hiệu năng im lặng khác là publish quá thường xuyên. Timer, pipeline Combine, và cập nhật tiến trình có thể bắn nhiều lần trong một giây. Nếu một property được publish ảnh hưởng đến danh sách (hoặc nằm trên ObservableObject chia sẻ bởi màn hình), mỗi tick có thể làm invalid danh sách.

Ví dụ: bạn có trường tìm kiếm cập nhật query mỗi phím, rồi lọc 5.000 item. Nếu bạn lọc ngay lập tức, danh sách diff liên tục khi người dùng gõ. Debounce query và cập nhật mảng lọc sau một khoảng tạm dừng ngắn.

Các pattern hay giúp:

  • Giữ giá trị thay đổi nhanh ra khỏi object điều khiển danh sách (dùng object nhỏ hơn hoặc @State cục bộ)
  • Debounce tìm kiếm và lọc để danh sách cập nhật sau khi gõ tạm dừng
  • Tránh publish từ timer tần suất cao; cập nhật ít hơn hoặc chỉ khi giá trị thực sự thay đổi
  • Giữ trạng thái per-row cục bộ (@State trong hàng) thay vì một giá trị global thay đổi liên tục
  • Tách model lớn: một ObservableObject cho dữ liệu danh sách, một khác cho trạng thái UI màn hình

Ý tưởng đơn giản: làm cho thời gian cuộn yên tĩnh. Nếu không có gì quan trọng thay đổi, danh sách không nên bị yêu cầu làm việc.

Chọn container phù hợp: List vs LazyVStack

Container bạn chọn ảnh hưởng lượng công việc iOS làm cho bạn.

List thường là lựa chọn an toàn khi UI của bạn giống bảng tiêu chuẩn: hàng với text, ảnh, swipe actions, selection, separators, edit mode, và accessibility. Ở trong, nó hưởng lợi từ các tối ưu hệ thống Apple đã tinh chỉnh qua nhiều năm.

ScrollView với LazyVStack tuyệt khi bạn cần layout tùy chỉnh: card, block nội dung hỗn hợp, header đặc biệt, hoặc feed-style. “Lazy” nghĩa là nó dựng hàng khi tới màn hình, nhưng nó không cho bạn cùng hành vi như List ở mọi trường hợp. Với dataset rất lớn, điều đó có thể dẫn tới dùng bộ nhớ cao hơn và cuộn giật hơn trên các thiết bị cũ.

Quy tắc quyết định đơn giản:

  • Dùng List cho màn hình dạng bảng: settings, inbox, orders, list admin
  • Dùng ScrollView + LazyVStack cho layout tùy chỉnh và nội dung hỗn hợp
  • Nếu có hàng nghìn mục và chỉ cần bảng, bắt đầu với List
  • Nếu cần kiểm soát pixel-perfect, thử LazyVStack, rồi đo bộ nhớ và rớt khung

Cũng lưu ý styling có thể âm thầm làm chậm cuộn. Hiệu ứng trên mỗi hàng như shadow, blur, và overlay phức tạp có thể ép thêm công việc render. Nếu muốn tạo độ sâu, áp dụng hiệu ứng nặng lên phần nhỏ (ví dụ icon) thay vì cả hàng.

Ví dụ cụ thể: màn hình “Orders” với 5.000 hàng thường mượt trong List vì hàng được tái sử dụng. Nếu bạn chuyển sang LazyVStack và dựng hàng kiểu card với shadow lớn và nhiều overlay, có thể thấy giật dù mã trông gọn.

Phân trang để mượt và tránh tăng đột ngột bộ nhớ

Ra mắt với những thứ thiết yếu kèm theo
Dùng auth và Stripe payments có sẵn để tập trung vào hiệu năng, không phải hạ tầng.
Get Started

Phân trang giữ danh sách nhanh vì bạn render ít hàng hơn, giữ ít model trong bộ nhớ hơn, và cho SwiftUI ít công việc diff hơn.

Bắt đầu với hợp đồng phân trang rõ ràng: kích thước trang cố định (ví dụ 30 đến 60 item), flag “không còn kết quả”, và một hàng loading chỉ xuất hiện khi đang fetch.

Bẫy phổ biến là kích hoạt trang tiếp theo chỉ khi hàng cuối cùng xuất hiện. Thường quá muộn, nên người dùng chạm cuối và thấy pause. Thay vào đó, bắt đầu tải khi một trong vài hàng cuối xuất hiện.

Pattern đơn giản:

@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
        }
    }
}

Cách này tránh các vấn đề như hàng trùng (API trả kết quả chồng lặp), race condition từ nhiều onAppear gọi, và tải quá nhiều cùng lúc.

Nếu danh sách hỗ trợ pull to refresh, đặt lại trạng thái phân trang cẩn thận (xóa items, reset reachedEnd, hủy task đang chạy nếu có thể). Nếu bạn kiểm soát backend, ID ổn định và phân trang theo cursor làm UI mượt hơn rõ rệt.

Ảnh, văn bản và layout: giữ việc render hàng nhẹ

Biến cuộn mượt thành một sản phẩm
Tạo app iOS có backend thực sự để cập nhật danh sách giữ ổn định khi dữ liệu tăng.
Bắt đầu Xây dựng

Danh sách dài hiếm khi chậm vì container. Phần lớn thời gian, vấn đề là hàng. Ảnh thường là thủ phạm: giải mã, thay đổi kích thước, và vẽ có thể không theo kịp tốc độ cuộn, đặc biệt trên thiết bị cũ.

Nếu tải ảnh từ mạng, đảm bảo công việc nặng không xảy ra trên main thread trong lúc cuộn. Cũng tránh tải tài sản phân giải cao cho thumbnail 44–80 pt.

Ví dụ: màn hình “Messages” với avatar. Nếu mỗi hàng tải ảnh 2000x2000, scale xuống, và áp blur hoặc shadow, danh sách sẽ giật ngay cả khi model đơn giản.

Giữ công việc ảnh dự đoán được

Thói quen ảnh hưởng lớn:

  • Dùng thumbnail từ server hoặc sinh trước có kích thước gần với kích thước hiển thị
  • Giải mã và thay đổi kích thước ngoài main thread nếu có thể
  • Cache thumbnail để cuộn nhanh không phải fetch hoặc decode lại
  • Dùng placeholder đúng kích thước để tránh nhấp nháy và nhảy layout
  • Tránh modifier tốn kém trên ảnh trong hàng (shadow nặng, mask, blur)

Ổn định layout để tránh đo lặp

SwiftUI có thể tốn nhiều thời gian đo hơn vẽ nếu chiều cao hàng thay đổi liên tục. Cố gắng giữ hàng dự đoán được: frame cố định cho thumbnail, giới hạn dòng ổn định, và khoảng cách ổn định. Nếu text có thể mở rộng, giới hạn nó (ví dụ 1–2 dòng) để một cập nhật không ép đo lại nhiều.

Placeholder cũng quan trọng. Một vòng xám trở thành avatar sau nên chiếm cùng frame để hàng không reflow giữa lúc cuộn.

Cách đo: những kiểm tra Instruments chỉ ra nút thắt thực sự

Làm hiệu năng theo cảm tính nếu chỉ dựa vào “cảm thấy giật” là chơi may rủi. Instruments cho bạn biết gì chạy trên main thread, gì được cấp phát trong lúc cuộn nhanh, và gì gây rớt khung.

Định nghĩa baseline trên thiết bị thật (nếu hỗ trợ, chọn thiết bị cũ hơn). Làm một hành động lặp lại: mở màn hình, cuộn nhanh từ trên xuống dưới, kích hoạt load-more một lần, rồi cuộn trở lên. Ghi nhận điểm giật tồi nhất, đỉnh bộ nhớ, và xem UI còn phản hồi không.

Ba view Instruments hữu ích

Dùng chung:

  • Time Profiler: tìm spike trên main thread khi cuộn. Layout, đo văn bản, parse JSON, và decode ảnh ở đây thường giải thích giật.
  • Allocations: để ý các cơn bùng nổ object tạm trong lúc cuộn nhanh. Thường chỉ ra định dạng lặp, attributed string mới, hoặc dựng model per-row.
  • Core Animation: xác nhận rớt khung và thời gian frame dài. Giúp bạn phân biệt áp lực rendering và công việc dữ liệu.

Khi tìm thấy spike, click vào call tree và hỏi: cái này xảy ra một lần cho màn hình, hay một lần cho mỗi hàng, mỗi lần cuộn? Thứ hai là thứ phá mượt cuộn.

Thêm signpost cho sự kiện cuộn và phân trang

Nhiều app làm thêm công việc trong lúc cuộn (tải ảnh, phân trang, lọc). Signpost giúp bạn thấy những khoảnh khắc đó trên 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")

Kiểm tra lại sau mỗi thay đổi, từng thay đổi một. Nếu FPS cải thiện nhưng Allocations tệ hơn, bạn có thể đổi giật lấy áp lực bộ nhớ. Giữ ghi chú baseline và chỉ giữ những thay đổi làm các con số đi theo hướng tốt.

Những sai lầm thường giết hiệu năng danh sách một cách âm thầm

Giảm bão cập nhật từ nguồn
Tự động hóa workflow bằng quy trình nghiệp vụ trực quan và giữ logic app nhất quán khắp nơi.
Start Project

Một vài vấn đề rõ (ảnh lớn, dataset khổng lồ). Những vấn đề khác chỉ lộ ra khi dữ liệu tăng, đặc biệt trên máy cũ.

1) ID hàng không ổn định

Lỗi kinh điển là tạo ID trong view như id: \.self cho reference type, hoặc UUID() trong body. SwiftUI dùng danh tính để diff. Nếu ID thay đổi, SwiftUI coi hàng là mới, dựng lại và có thể vứt cache layout.

Dùng ID ổn định từ model (khóa chính DB, ID server, hoặc UUID lưu một lần khi tạo item). Nếu không có, thêm một cái.

2) Công việc nặng trong onAppear

onAppear chạy nhiều hơn bạn nghĩ vì hàng vào/ra khi cuộn. Nếu mỗi hàng bắt đầu decode ảnh, parse JSON, hoặc lookup DB trong onAppear, bạn sẽ có spike lặp.

Dời công việc nặng ra khỏi hàng. Tính trước khi dữ liệu tải, cache kết quả, và giữ onAppear cho hành động rẻ như kích hoạt phân trang khi gần cuối.

3) Bind cả danh sách cho chỉnh sửa hàng

Khi mỗi hàng có @Binding vào một mảng lớn, một chỉnh sửa nhỏ có thể trông như thay đổi lớn. Điều đó khiến nhiều hàng đánh giá lại, và đôi khi cả danh sách làm mới.

Ưu tiên truyền giá trị bất biến vào hàng và gửi thay đổi về bằng action nhẹ (ví dụ: “toggle favorite cho id”). Giữ trạng thái per-row cục bộ khi nó thực sự thuộc về hàng.

4) Quá nhiều animation khi cuộn

Animation tốn kém trong danh sách vì có thể kích hoạt thêm nhiều lần layout. Áp animation(.default, value:) quá cao (trên toàn danh sách) hoặc animate mọi thay đổi nhỏ sẽ khiến cuộn dính.

Giữ đơn giản:

  • Giới hạn animation trong một hàng thay đổi
  • Tránh animation khi cuộn nhanh (đặc biệt cho selection/highlight)
  • Cẩn thận với implicit animation trên giá trị thay đổi thường xuyên
  • Ưu tiên transition đơn giản hơn các hiệu ứng kết hợp phức tạp

Ví dụ thực tế: một danh sách chat mà mỗi hàng khởi fetch mạng trong onAppear, dùng UUID() cho id, và animate trạng thái “seen”. Bộ ba đó tạo churn liên tục. Sửa danh tính, cache công việc, và hạn chế animation thường làm giao diện cùng một UI mượt hẳn.

Checklist nhanh, ví dụ đơn giản, và bước tiếp theo

Nếu bạn chỉ làm vài việc, bắt đầu ở đây:

  • Dùng id duy nhất, ổn định cho mỗi hàng (không phải index, không phải UUID mới mỗi lần)
  • Giữ công việc trên hàng nhỏ: tránh định dạng nặng, view tree lớn, và computed property tốn trong body
  • Kiểm soát publish: đừng để trạng thái thay đổi nhanh (timer, gõ, tiến trình) invalidate toàn bộ danh sách
  • Tải theo trang và prefetch để bộ nhớ phẳng
  • Đo trước và sau bằng Instruments để không phỏng đoán

Hãy tưởng tượng inbox hỗ trợ với 20.000 cuộc hội thoại. Mỗi hàng hiện subject, preview tin cuối, timestamp, badge chưa đọc, và avatar. Người dùng có thể tìm kiếm, và tin mới tới trong khi họ cuộn. Phiên bản chậm thường làm vài việc cùng lúc: dựng lại hàng trên mỗi phím gõ, đo lại text quá nhiều, và fetch quá nhiều ảnh quá sớm.

Kế hoạch thực tế không đòi xé toang codebase:

  • Baseline: ghi một đoạn cuộn và phiên tìm kiếm ngắn trong Instruments (Time Profiler + Core Animation).
  • Sửa danh tính: đảm bảo model có id thật từ server/DB, và ForEach dùng nó nhất quán.
  • Thêm phân trang: bắt đầu với 50–100 item mới nhất, sau đó tải thêm khi sắp hết.
  • Tối ưu ảnh: dùng thumbnail nhỏ hơn, cache kết quả, và tránh decode trên main thread.
  • Đo lại: xác nhận giảm số lần layout, ít cập nhật view hơn, và thời gian frame ổn định hơn trên thiết bị cũ.

Nếu bạn xây sản phẩm hoàn chỉnh (app iOS cộng backend và admin web), sẽ giúp nếu thiết kế model dữ liệu và hợp đồng phân trang sớm. Nền tảng như AppMaster (appmaster.io) xây cho workflow full-stack: bạn định nghĩa dữ liệu và business logic trực quan, và vẫn sinh mã nguồn thật để deploy hoặc self-host.

Câu hỏi thường gặp

What’s the fastest fix when my SwiftUI list scrolls with stutters?

Bắt đầu bằng cách sửa danh tính hàng. Dùng id ổn định từ model của bạn và tránh sinh ID trong view, vì ID thay đổi sẽ khiến SwiftUI coi mỗi hàng là mới và phải dựng lại nhiều hơn mức cần thiết.

Is SwiftUI slow because it “re-renders” too much?

body được tính lại thường rẻ; phần tốn kém là những gì nó kích hoạt. Bố cục nặng, đo văn bản, giải mã ảnh, và việc dựng lại nhiều hàng do danh tính không ổn định thường là nguyên nhân gây rớt khung hình.

How do I choose a stable `id`for `ForEach` and `List`?

Đừng dùng UUID() trong view hay dựa vào chỉ số mảng nếu dữ liệu có thể chèn, xóa hoặc sắp xếp lại. Ưu tiên ID từ server/database hoặc một UUID được lưu trên model khi nó được tạo, để ID không đổi qua các cập nhật.

Can `id: \.self` make list performance worse?

Có thể, đặc biệt nếu giá trị băm thay đổi khi các trường edit được cập nhật; SwiftUI có thể coi đó là hàng khác. Nếu cần Hashable, hãy dựa trên một định danh ổn định thay vì các thuộc tính có thể thay đổi như name, isSelected hoặc text sinh ra.

What should I avoid doing inside a row’s `body`?

Dời công việc nặng ra khỏi body. Tiền định dạng ngày/ số, tránh tạo formatter mới cho mỗi hàng, và không xây dựng mảng dẫn xuất lớn bằng map/filter trong view; tính sẵn trong model hoặc view model và truyền chuỗi/giá trị đã sẵn sàng hiển thị vào hàng.

Why is my `onAppear` firing so often in a long list?

onAppear chạy thường xuyên khi hàng vào/ra màn hình trong lúc cuộn. Nếu mỗi hàng bắt đầu công việc nặng ở đó (giải mã ảnh, đọc DB, parse), bạn sẽ thấy các spike lặp lại; giữ onAppear cho những tác vụ rẻ như kích hoạt phân trang khi gần cuối.

What causes “update storms” that make scrolling feel sticky?

Bất cứ giá trị published nào thay đổi nhanh và được chia sẻ với danh sách đều có thể làm nó bị invalid nhiều lần, ngay cả khi dữ liệu hàng không đổi. Giữ timers, trạng thái gõ, và tiến trình ra khỏi object chính điều khiển danh sách, debounce tìm kiếm, và tách ObservableObject lớn thành các object nhỏ hơn khi cần.

When should I use `List` vs `LazyVStack` for large datasets?

Dùng List khi giao diện giống bảng chuẩn (hàng, swipe actions, selection, separators) và bạn muốn lợi thế tối ưu của hệ thống. Dùng ScrollView + LazyVStack khi cần layout tùy chỉnh, nhưng đo bộ nhớ và rớt khung vì dễ vô tình làm thêm công việc bố cục.

What’s a simple pagination approach that stays smooth?

Bắt đầu tải trước khi tới hàng cuối cùng, khi người dùng chạm ngưỡng gần cuối, và đảm bảo tránh gọi trùng lặp. Giữ kích thước trang hợp lý, theo dõi isLoadingreachedEnd, và loại bỏ kết quả trùng bằng ID ổn định để tránh hàng trùng và diff thừa.

How do I measure what’s actually slowing my SwiftUI list down?

Trên thiết bị thật, ghi một baseline và dùng Instruments để tìm spike trên main thread và bùng nổ allocation khi cuộn nhanh. Time Profiler chỉ ra khối chặn cuộn, Allocations cho biết rác tạm thời per-row, và Core Animation xác nhận rớt khung để bạn phân biệt giữa rendering hay công việc dữ liệu.

Dễ dàng bắt đầu
Tạo thứ gì đó tuyệt vời

Thử nghiệm với AppMaster với gói miễn phí.
Khi bạn sẵn sàng, bạn có thể chọn đăng ký phù hợp.

Bắt đầu