Hiệu năng
Tìm ra thứ gì đang vẽ lại, thay vì đoán
Việc tối ưu hiệu năng SwiftUI gần như hoàn toàn xoay quanh một câu hỏi: những view nào đang được
tính lại, và vì sao? Việc dựng hình hiếm khi là vấn đề. body chạy một nghìn lần một giây mới là
vấn đề.
Tin tốt là chuyện này đo được chứ không huyền bí.
Tìm ra thứ gì đang được tính lại
Thứ đo đạc rẻ nhất trong cả framework:
var body: some View {
let _ = Self._printChanges()
…
}
Ở mỗi lần tính lại, nó in ra tên view và nguyên nhân — một tên thuộc tính, @self nếu chính giá trị
view đã đổi, hoặc @identity nếu đó hoàn toàn là một view mới. Hãy đặt nó vào view bạn nghi ngờ rồi
nhìn console trong lúc dùng ứng dụng.
Phần in ra cho bạn biết mình đang gặp cái nào trong ba vấn đề:
- Một tên thuộc tính — một trạng thái nào đó đang đổi nhiều hơn bạn tưởng, hoặc đang bị đọc ở quá cao.
@self— các thuộc tính của chính struct view đã đổi, thường vì một view cha truyền giá trị mới xuống mỗi lần. Một closure hay một mảng viết thẳng tại chỗ là nguyên nhân phổ biến.@identity— view đang bị thay thế chứ không phải cập nhật. Hãy quay lại chương về danh tính.
Cảnh báo
_printChanges là API có gạch dưới. Nó dành cho lúc gỡ lỗi, không dành cho code phát hành —
hãy bọc nó trong #if DEBUG nếu nó còn sống qua buổi làm việc mà bạn đã thêm nó vào.
Để nhìn toàn ứng dụng, Instruments có mẫu SwiftUI. Rãnh View Body cho thấy mỗi body chạy mất bao
lâu và chạy bao nhiêu lần; rãnh Update Groups cho thấy thứ gì kích hoạt mỗi chu kỳ. Những body chạy
lâu thì đáng xem, nhưng một body ngắn chạy bốn trăm lần mới là phát hiện phổ biến hơn.
Những thay đổi thật sự có tác dụng
Chẻ nhỏ những view lớn. Việc làm mất hiệu lực diễn ra theo từng view, nên một body khổng lồ sẽ
chạy lại toàn bộ vì bất cứ thay đổi nào nó phụ thuộc vào. Hãy tách ra một struct — không phải một
thuộc tính tính toán hay một hàm @ViewBuilder, vì chúng bị nội tuyến vào view cha và không có danh
tính riêng.
Đọc trạng thái ở chỗ thấp nhất có thể. Đã nói hai lần rồi, và đây là thay đổi có đòn bẩy lớn nhất. Một thuộc tính đọc ở view cha làm mất hiệu lực view cha và toàn bộ cây con của nó; đúng lần đọc ấy đặt ở view lá thì chỉ làm mất hiệu lực một cái lá.
Cho ForEach những id ổn định. Một id dựa trên chỉ số hoặc mới sinh ra sẽ biến mọi dòng thành
một view mới ở mọi lần cập nhật, thứ vô hiệu hóa mọi tối ưu mà framework có.
Dùng container lười khi danh sách dài. VStack bên trong ScrollView tạo mọi đứa con ngay lập
tức; LazyVStack tạo chúng khi chúng tiến gần vùng nhìn thấy. Với hai mươi dòng thì khác biệt là
nhiễu, với hai nghìn dòng thì đó là toàn bộ vấn đề. List vốn đã lười.
Đừng đặt GeometryReader trong một dòng. Nó tham lam ở cả hai chiều, tham gia vào lượt tính kích
thước, và chạy lại mỗi khi bất cứ điều gì về bố cục thay đổi. Đặt trong một dòng danh sách, nó là
cách đáng tin cậy để làm cú cuộn bị giật. Các đường dẫn căn chỉnh, containerRelativeFrame hay
onGeometryChange lo được phần lớn những trường hợp người ta với tới nó.
Giữ công việc tốn kém ra khỏi body. body có thể chạy nhiều lần trong một khung hình. Việc sắp
xếp, lọc, định dạng và tính toán ngày tháng đặt trong đó sẽ chạy mỗi lần.
struct OrderList: View {
let orders: [Order]
private var sorted: [Order] {
orders.sorted { $0.date > $1.date } // chạy ở mọi lần tính
}
}
Hãy sắp xếp một lần ở chỗ dữ liệu thay đổi — trong mô hình — rồi đưa cho view một danh sách đã sẵn thứ tự.
Những thứ trông như vấn đề mà không phải
View được tạo ra liên tục. Một struct view chỉ vài byte trên ngăn xếp, và tạo ra hàng triệu cái
thật sự là rẻ. Cái giá nằm ở việc body được tính, không phải ở việc struct tồn tại. Đừng bẻ cong
một thiết kế chỉ để tránh tạo ra các giá trị view.
AnyView ở khắp nơi. Nó có xóa kiểu thật và có tốn kém thật, nhưng còn xa mới là thứ đầu tiên
cần sửa. Xóa kiểu ở một nhánh của một câu điều kiện thì không sao; cả một cây view dựng bằng AnyView
là một thiết kế đáng xem lại vì tính dễ đọc trước khi vì hiệu năng.
Cây view sâu. Độ sâu đến từ các modifier là bình thường và gần như miễn phí. Phần lồng nhau ấy được giải quyết ngay lúc biên dịch.
Đo lúc khởi động, không phải lúc dựng khung hình
Hai con số đáng theo dõi mà chẳng liên quan gì đến body:
Khởi động nguội. Bất cứ thứ gì nằm trên đường tới khung hình đầu tiên — một bộ khởi tạo @State
làm việc thật, một mô hình nạp đồng bộ trong init, một cây scene @main lớn — đều làm nó chậm đi.
Hãy chuyển công việc vào .task để khung hình đầu tiên được vẽ từ trạng thái tạm.
Bộ nhớ do ảnh. Việc giải mã Image không miễn phí, và một tấm ảnh nguyên độ phân giải trong một
lưới ảnh thu nhỏ sẽ giữ bitmap đã giải mã chứ không phải cái file. Hãy giảm mẫu xuống đúng kích thước
đang hiển thị; đây thường xuyên là khác biệt giữa một cái lưới mượt mà và một ứng dụng bị hệ thống
đá ra khỏi bộ nhớ.
Quy tắc sống sót qua tất cả: đo, đổi một thứ, đo lại. Hành vi cập nhật của SwiftUI phản trực giác
đủ thường xuyên để việc lập luận từ nguyên lý đầu tiên về thứ “đáng lẽ” phải nhanh hơn trở nên không
đáng tin, còn _printChanges thì chỉ tốn mười giây để thử.