Bốn lần tôi tối ưu nhầm chỗ
“Hãy đo trước” là lời khuyên ai cũng đồng ý và tôi cứ liên tục không làm theo, vì một giả thuyết hợp lý cho cảm giác như tri thức. Đây là bốn lần nó không phải vậy, kèm những con số thật.
1. Cái danh sách hóa ra không phải cái danh sách
Triệu chứng: một màn hình mất khoảng 900ms mới hiện ra.
Giả thuyết của tôi: quá nhiều dòng. Tôi đã sẵn sàng thêm phân trang.
Trình đo nói gì: DateFormatter.string(from:) chiếm 61% self weight. Tôi đang tạo một bộ định
dạng bên trong một map trên phản hồi, mỗi dòng một cái, và việc khởi tạo DateFormatter nạp dữ liệu
locale mỗi lần.
Cách chữa: chuyển một dòng ra khỏi cái closure. 900ms → 140ms.
Phân trang lẽ ra đã mất hai ngày, làm trải nghiệm tệ đi, và cho ra một màn hình vẫn chậm ở từng trang.
2. Cái cache ảnh làm mọi thứ tệ hơn
Triệu chứng: cảnh báo bộ nhớ trên một lưới ảnh thu nhỏ.
Giả thuyết của tôi: quá nhiều ảnh trong bộ nhớ. Tôi dựng một cache LRU với chính sách loại bỏ.
Thứ thật sự đang xảy ra: mỗi ảnh thu nhỏ là một tấm ảnh nguyên độ phân giải. UIImage giữ
bitmap đã giải mã, nên một tấm ảnh 4000×3000 chiếm khoảng 48MB bất kể nó được vẽ ở kích thước 80
điểm.
Cái cache của tôi đang cẩn thận loại bỏ những đối tượng 48MB mà lẽ ra chẳng bao giờ nên là 48MB.
Giảm mẫu ngay lúc giải mã bằng CGImageSourceCreateThumbnailAtIndex đưa mỗi cái xuống khoảng 100KB,
và cái cache trở nên không cần thiết — cả cái lưới vừa trong lượng bộ nhớ ít hơn ba tấm ảnh cũ.
Cách chữa: xóa cái cache, giảm mẫu lúc giải mã. Bộ nhớ đỉnh 1,2GB → 90MB.
3. Bộ phân tích JSON vốn vẫn ổn
Triệu chứng: một lượt đồng bộ mất 12 giây.
Giả thuyết của tôi: JSONDecoder chậm. Tôi bỏ cả một buổi tối đi đánh giá các bộ phân tích nhanh
hơn.
Trình đo nói gì: việc giải mã chiếm 400ms trong 12 giây. Phần còn lại là 200 request mạng tuần
tự, mỗi cái chờ cái trước, vì tôi đã viết lượt đồng bộ thành một vòng for có await bên trong.
Cách chữa: một task group với giới hạn đồng thời là sáu.
try await withThrowingTaskGroup(of: Page.self) { group in
for url in urls {
group.addTask { try await fetch(url) }
}
…
}
12s → 2,1s, và cái bộ giải mã JSON mà tôi đã bỏ cả buổi tối nghiên cứu chưa bao giờ là vấn đề.
Cảnh báo
Đây là hình dạng phổ biến nhất của sai lầm: tối ưu công việc CPU trong khi cái giá thật là việc chờ đợi. Một trình đo lấy mẫu theo thời gian thực tế sẽ hiện các luồng bị chặn đang tích lũy trọng số, thứ trông y hệt như đang làm việc. Nếu đỉnh bản ghi của bạn là một khung mạng hay một khung khóa thì bạn có một vấn đề về đồng thời, không phải một vấn đề hiệu năng.
4. Cái animation hóa ra là một vấn đề bố cục
Triệu chứng: một chuyển cảnh bị giật trên các máy đời cũ.
Giả thuyết của tôi: animation quá phức tạp. Tôi đơn giản hóa nó, giảm đường spring, bỏ một hiệu ứng mờ.
Instruments nói gì: các cú giật hoàn toàn không nằm ở khâu dựng hình. Một GeometryReader bên
trong mỗi dòng đang tham gia vào lượt tính kích thước, và phần bố cục chạy vài lần trong mỗi khung
hình suốt chuyển cảnh.
Cách chữa: thay nó bằng containerRelativeFrame. Cái animation nguyên bản, không sửa gì, chạy ở
60fps.
Tôi đã bỏ hai ngày ra làm thiết kế tệ đi để sửa một thứ vốn không phải do thiết kế.
Cái quy luật
Trong cả bốn trường hợp, giả thuyết của tôi đều hợp lý. Danh sách dài thì chậm, cache ảnh thì có ích, phân tích JSON thì tốn kém, animation phức tạp thì rớt khung hình. Mỗi câu đều đúng nói chung và không câu nào đúng ở đây.
Điểm chung của chúng: tôi đang lập luận về đoạn code mình vừa viết gần đây, còn cái giá thì nằm ở chỗ khác — trong một bộ định dạng, trong một lượt giải mã, trong cấu trúc của một vòng lặp, trong hệ thống bố cục. Sự chú ý đi tới thứ bạn vừa động vào, còn cái giá thì không.
Tôi làm gì bây giờ
Lấy một bản ghi trước khi hình thành giả thuyết. Không phải sau. Nếu tôi đã quyết định thứ gì chậm rồi thì tôi sẽ đọc bản ghi để tìm sự xác nhận, và tôi sẽ tìm thấy.
Viết con số ra trước. “Màn hình mất 900ms.” Không có đường cơ sở thì “cảm giác nhanh hơn” là thước đo duy nhất có sẵn và nó luôn dương tính sau khi bạn đã bỏ ra một ngày.
Sửa một thứ, đo lại. Hai thay đổi kèm một cải thiện thì chẳng nói cho bạn biết cái nào có tác dụng.
Bản release, thiết bị thật. Bản debug tắt tối ưu; simulator dùng CPU của máy Mac bạn. Cả hai đều cho ra một hình dạng hiệu năng không phải của người dùng bạn.
Lập luận phản biện
Có một giới hạn thật cho câu “luôn phải đo”. Vài thứ đã được biết là đắt đỏ và không cần một bản ghi để biện minh cho việc tránh chúng — một vòng lặp O(n²) trên dữ liệu người dùng, một lời gọi mạng đồng bộ trên luồng chính, giải mã một tấm ảnh ở kích thước gấp mười lần kích thước nó được vẽ.
Sự phân biệt tôi dùng: hãy mặc định tránh những khuôn mẫu đã biết là tệ, và hãy đo trước khi làm bất cứ điều gì đánh đổi chất lượng thiết kế. Không đặt một lời gọi mạng lên luồng chính thì miễn phí. Thêm một cache, chẻ một màn hình thành nhiều trang, hay đơn giản hóa một animation thì không — những thứ đó đánh đổi một điều gì đó có thật, và cuộc đổi chác ấy cần bằng chứng.