Cắt thời gian khởi động nguội từ 2,1s xuống 900ms
Hướng dẫn của Apple là một ứng dụng nên dùng được trong vòng 400ms. Ứng dụng của tôi mất 2,1 giây từ lúc chạm đến khung hình đầu tiên dùng được, và tôi chưa bao giờ đo vì ứng dụng “cảm giác vẫn ổn” trên máy tôi — máy mới nhất tôi có, đang ấm, với cache đầy đủ.
Hãy đo cho tử tế trước đã
Khởi động nguội nghĩa là ứng dụng không nằm trong bộ nhớ. Tắt cưỡng bức thôi thì chưa đủ; cách chắc chắn nhất là khởi động lại thiết bị, hoặc để ứng dụng đóng đủ lâu để bị đẩy ra khỏi bộ nhớ.
Con số quan trọng nằm ngay trong console, miễn phí, không cần đo đạc gì:
Edit Scheme → Run → Arguments → Environment Variables
DYLD_PRINT_STATISTICS = 1
Nó in ra thời gian trước main — liên kết động, rebase, binding, khởi tạo runtime ObjC, và mọi
phương thức +load cùng các bộ khởi tạo tĩnh của bạn — trước khi bất kỳ dòng code nào của bạn chạy.
Total pre-main time: 780.19 milliseconds
dylib loading time: 410.31 milliseconds
rebase/binding time: 91.24 milliseconds
ObjC setup time: 84.02 milliseconds
initializer time: 194.55 milliseconds
Mọi thứ sau main là phần bạn tự đo, và mẫu App Launch trong Instruments phân tách nó ra tử tế.
Nhưng DYLD_PRINT_STATISTICS tốn mười giây và nói ngay cho tôi biết một phần ba thời gian khởi động
đã mất trước khi main kịp được gọi.
Thời gian của tôi đi đâu
Đại khái: 780ms trước main, 600ms trong application(_:didFinishLaunchingWithOptions:), và 700ms để
dựng màn hình đầu tiên.
Thư viện động — 410ms. Tôi có mười một framework liên kết động, vài cái đến từ các phụ thuộc SPM vốn mặc định là động. Mỗi cái tốn một lần dlopen lúc khởi động. Chuyển những cái tôi kiểm soát được sang liên kết tĩnh đưa con số này xuống khoảng 120ms.
Trong Swift Package Manager, đây là phần type: trên sản phẩm thư viện:
.library(name: "DesignSystem", type: .static, targets: ["DesignSystem"])
Liên kết tĩnh có cái giá của nó — binary lớn hơn, và nó không dùng được cho thứ gì chia sẻ code với một extension — nhưng với một nhúm module nội bộ thì đó là khoản lợi khởi động lớn nhất có sẵn.
Bộ khởi tạo tĩnh — 194ms. Hai SDK phân tích làm việc trong +load, và một static let trong
code của chính tôi dựng ra một bảng tra cứu lớn. +load chạy trước main và không có cách nào để
hoãn nó lại; cách chữa cho các SDK là cập nhật lên phiên bản đã chuyển sang khởi tạo lười, còn với
cái bảng của tôi là làm cho nó lười thật sự thay vì là một biến toàn cục.
didFinishLaunching — 600ms. Đây mới là chỗ đáng xấu hổ. Trong đó có: khởi tạo kết nối cơ sở dữ
liệu, đọc và giải mã một file JSON 400KB nội dung đã cache, cấu hình ba SDK, và đăng ký nhận thông
báo đẩy.
Không thứ nào trong đó cần xảy ra trước khung hình đầu tiên.
func application(_ app: UIApplication, didFinishLaunchingWithOptions: …) -> Bool {
// chỉ những gì khung hình đầu tiên thật sự cần
window = …
return true
}
// mọi thứ khác, sau khung hình đầu tiên
.task {
await database.connect()
await analytics.configure()
await pushRegistration.register()
}
Cảnh báo
Chuyển việc ra khỏi didFinishLaunching không phải không có hệ quả. Bất cứ thứ gì phải quan sát
một sự kiện lúc khởi động — một thông báo đẩy đã mở ứng dụng, một URL, một mục shortcut — đều
phải ở lại, hoặc phải được ghi lại rồi phát lại. Hãy chuyển từng thứ một và kiểm tra lại các
đường deep link.
Màn hình đầu tiên — 700ms. View gốc là một danh sách nạp dữ liệu từ đĩa một cách đồng bộ ngay
trong bộ khởi tạo của nó, nên khung hình đầu tiên phải chờ lượt đọc. Tái cấu trúc để nó vẽ ra một
khung tạm rồi nạp trong .task đã đẩy toàn bộ 700ms ra sau một giao diện nhìn thấy được.
Thay đổi cuối đó đáng được phát biểu thành một nguyên tắc: khung hình đầu tiên không cần chứa dữ liệu thật. Nó cần xuất hiện. Một danh sách các dòng khung xương rồi được điền vào sau 300ms thì tốt hơn hẳn một màn hình đen suốt một giây, và nó được đo là một lần khởi động nhanh hơn nhiều, vì nó đúng là như vậy.
Kết quả
| Giai đoạn | Trước | Sau |
|---|---|---|
| Trước main | 780ms | 310ms |
didFinishLaunching |
600ms | 40ms |
| Khung hình đầu | 700ms | 550ms |
| Tổng | 2,1s | 900ms |
Khung hình đầu tiên vẫn là phần đắt nhất, và đó là công việc dựng view thật sự mà tôi chưa tìm được cách bỏ đi.
Thứ tôi sẽ kiểm tra đầu tiên trong ứng dụng của người khác
Xếp theo tần suất nó là câu trả lời:
- Có bao nhiêu framework động? Chạy
otool -Ltrên binary. Nếu nhiều hơn một nhúm thì đó là chỗ nhìn đầu tiên. - Trong
didFinishLaunchingcó gì? Bất cứ thứ gì không bắt buộc cho khung hình đầu tiên đều là ứng viên để chuyển đi. - Có lượt đọc đĩa hay đọc mạng đồng bộ nào trên đường tới view đầu tiên không? Nhất là việc giải mã JSON, thứ chậm hơn người ta tưởng.
- Có
static letnào đang làm việc thật không? Chúng lười trong Swift, nhưng một biến toàn cục bị chạm vào lúc khởi động thì được khởi tạo ngay lúc khởi động.
Không có gì trong số đó cần đến Instruments. Nó cần một lần đo, một cách thành thật, trên thiết bị cũ nhất bạn còn hỗ trợ — đúng cái bước mà tôi đã bỏ qua suốt hai năm.