Lazy Loading là chiến lược tải chậm dữ liệu, hình ảnh và thành phần, trong đó tài nguyên không được yêu cầu khi khởi động ứng dụng mà tại thời điểm người dùng thực sự cần. Theo Hướng dẫn Android Paging 3, tải chậm danh sách giảm mức tiêu thụ bộ nhớ 60–80% khi làm việc với các tập dữ liệu lớn. Khởi tạo chậm là nguyên tắc chính làm nền tảng cho tất cả các triển khai Lazy Loading.
Những điểm chính
Lazy Loading (tải chậm, tải trì hoãn) là một mẫu thiết kế và tối ưu hóa trong đó tài nguyên ứng dụng không được tải khi khởi động mà ngay trước khi sử dụng. Trong phát triển di động, Lazy Loading được áp dụng cho ba loại chính: dữ liệu (phân trang danh sách), hình ảnh (tải khi cuộn) và thành phần (ngăn xếp và view chậm).
Ngược lại với Lazy Loading là Eager Loading (tải ngay), nơi tất cả tài nguyên được tải khi khởi động màn hình. Eager Loading đơn giản hơn để triển khai nhưng tiêu thụ nhiều bộ nhớ hơn và tăng thời gian hiển thị đầu tiên. Đối với danh sách có hàng nghìn mục, Eager Loading dẫn đến OOM (hết bộ nhớ) trên các thiết bị có bộ nhớ hạn chế. Lazy Loading giải quyết vấn đề này bằng cách chỉ tải những gì hiển thị trên màn hình và tải phần còn lại khi người dùng cuộn.
Trong bối cảnh iOS và Android, Lazy Loading được triển khai ở các cấp độ khác nhau. SwiftUI cung cấp LazyVStack và LazyHStack để kết xuất chậm. UIKit sử dụng UITableView với dequeueReusableCell. Android sử dụng RecyclerView với nhóm ViewHolder. Ở cấp độ dữ liệu, Room với Paging 3 và Core Data với NSFetchedResultsController. Việc chọn công nghệ cụ thể phụ thuộc vào ngăn xếp và yêu cầu hiệu suất.
Nguyên tắc trung tâm của Lazy Loading là tải chính xác lượng dữ liệu cần thiết cho trạng thái màn hình hiện tại, cộng với bộ đệm dự đoán để cuộn mượt mà. Cách tiếp cận này dựa trên hai cơ chế: theo dõi khả năng hiển thị và ảo hóa phần tử.
Cơ chế theo dõi xác định phần tử nào nằm trong vùng hiển thị của màn hình (viewport). Trên Android, LinearLayoutManager hoặc GridLayoutManager thực hiện việc này thông qua các phương thức findFirstVisibleItemPosition và findLastVisibleItemPosition. Trong iOS, UIScrollView cung cấp bounds.origin.y và contentOffset.height để tính vùng hiển thị. Khi một phần tử vào viewport (hoặc bộ đệm tải trước), quá trình tải của nó được kích hoạt. Khi một phần tử rời khỏi màn hình, tài nguyên của nó có thể được giải phóng hoặc chuyển vào bộ nhớ đệm.
// Android — theo dõi khả năng hiển thị trong RecyclerView
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisible = layoutManager.findLastVisibleItemPosition()
val totalCount = layoutManager.itemCount
// Đang tải trang tiếp theo nếu còn < 5 phần tử
if (lastVisible >= totalCount - 5) {
loadMoreItems()
}
}
})
Bộ đệm tải trước là tải dự đoán các phần tử sắp xuất hiện trên màn hình. RecyclerView hỗ trợ GapWorker.Prefetch thông qua layoutManager.setItemPrefetchEnabled(true). iOS UITableView hỗ trợ tải trước thông qua UITableViewDataSourcePrefetching. Kích thước bộ đệm tải trước thường là 1–2 màn hình phía trước, tạo sự cân bằng giữa độ mượt khi cuộn và mức tiêu thụ bộ nhớ. Bộ đệm tải trước quá lớn làm mất đi lợi ích của Lazy Loading; quá nhỏ tạo ra khoảng trống khi cuộn nhanh.
Hình ảnh là loại tài nguyên nặng nhất trong ứng dụng di động. Một bức ảnh 12 MP có thể chiếm 3–5 MB ở dạng chưa nén. Lazy Loading hình ảnh ngăn việc tải hàng trăm hình ảnh không nhìn thấy vào bộ nhớ, điều này sẽ gây tử vong cho danh sách có ảnh đại diện người dùng hoặc danh mục sản phẩm.
Glide là thư viện tải hình ảnh phổ biến nhất cho Android với hỗ trợ bộ nhớ đệm, biến đổi và hoạt ảnh. Coil là một thay thế nhẹ hơn, được viết bằng Kotlin sử dụng coroutines. Cả hai thư viện đều tự động tạm dừng tải khi ImageView rời khỏi màn hình và hủy yêu cầu khi ViewHolder được sử dụng lại. Coil sử dụng coroutines và có kích thước ~1.5 MB so với ~4 MB của Glide, khiến nó được ưa chuộng cho các dự án tập trung vào kích thước APK.
// Coil — tải hình ảnh chậm
imageView.load("https://example.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.placeholder)
size(512, 512)
memoryCachePolicy(CachePolicy.ENABLED)
}
Kingfisher là thư viện cho iOS với hỗ trợ Swift Concurrency, bộ nhớ đệm đĩa và bộ nhớ, và tải trước cho UICollectionView. SDWebImage là thư viện cũ hơn có nguồn gốc từ Objective-C nhưng hỗ trợ Swift. Cả hai thư viện đều tích hợp với UIImageView và tự động quản lý vòng đời tải: hủy yêu cầu khi ô được sử dụng lại, chỉ tải hình ảnh khi ô hiển thị và giải phóng bộ nhớ khi nhận được thông báo bộ nhớ thấp.
Danh sách lớn dữ liệu là lĩnh vực ứng dụng chính của Lazy Loading trong ứng dụng di động. Bảng tin, danh mục sản phẩm, trò chuyện, lịch sử giao dịch — bất kỳ màn hình nào có danh sách tiềm năng vô hạn đều yêu cầu phân trang và tải chậm.
Paging 3 là thư viện từ Android Jetpack thực hiện chu trình tải chậm hoàn chỉnh: yêu cầu dữ liệu từ RemoteMediator (API + cơ sở dữ liệu), lưu vào bộ nhớ đệm trong Room, xuất từng trang qua PagingData và hiển thị qua AsyncPagingDataAdapter. Paging 3 hỗ trợ ba loại phân trang: dựa trên trang, dựa trên mục (offset/limit) và dựa trên khóa (khóa phân trang từ API). Separator là hỗ trợ tích hợp cho các dấu phân cách giữa các trang cho chỉ báo tải.
// Paging 3 — tải chậm từ API
class ArticlePagingSource(
private val api: ArticleApi
) : PagingSource<Int, Article>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, Article> {
return try {
val page = params.key ?: 1
val response = api.getArticles(page)
LoadResult.Page(
data = response.items,
prevKey = page.takeIf { it > 1 }?.dec(),
nextKey = page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
LazyVStack là một vùng chứa tích hợp của SwiftUI chỉ tạo và kết xuất các phần tử khi chúng xuất hiện trên màn hình. Không giống như VStack, ngay lập tức tính toán bố cục của tất cả các phần tử con, LazyVStack trì hoãn việc tạo view cho đến khi phần tử trở nên hiển thị hoặc vào phạm vi tải trước. LazyHStack là phiên bản ngang cho băng chuyền. Đối với danh sách lớn, Apple khuyên dùng List, bên trong hoạt động tương tự như LazyVStack với tính năng tái chế tích hợp bổ sung.
// SwiftUI — LazyVStack với tải chậm
ScrollView {
LazyVStack(spacing: 8) {
ForEach(articles) { article in
ArticleRow(article: article)
.onAppear {
if article == articles.last {
viewModel.loadMore()
}
}
}
}
}
Tải chậm thành phần UI là kỹ thuật mà các phần của giao diện (tiêu đề, chân trang, phần cài đặt, tab) không được tạo khi khởi động màn hình mà khi truy cập lần đầu. Điều này tăng tốc kết xuất ban đầu và giảm tải trên luồng chính.
ViewStub là trình giữ chỗ View nhẹ trong Android không chiếm không gian trong bố cục và không tạo View con cho đến khi inflate() được gọi. Lý tưởng cho các phần ít được sử dụng: bảng tìm kiếm, cài đặt nâng cao, khối quảng cáo. Tải chậm Fragment là kỹ thuật mà Fragment.onCreateView được trì hoãn cho đến khi người dùng chuyển sang tab đó. Được triển khai thông qua isVisible hoặc UserVisibleHint trong ViewPager.
AndroidX cung cấp SplitInstallManager để tải chậm các mô-đun theo yêu cầu. Các mô-đun thiết lập, chẩn đoán hoặc tính năng bổ sung chỉ được tải dưới dạng mô-đun Dynamic Feature khi người dùng yêu cầu lần đầu. Điều này giảm kích thước ứng dụng cơ sở từ 30–50% và đồng thời triển khai nguyên tắc Lazy Loading không chỉ ở cấp dữ liệu mà còn ở cấp mã.
TabView trong SwiftUI tải nội dung của mỗi tab một cách chậm — chỉ khi tab được kích hoạt. UIKit UITabBarController tạo tất cả các bộ điều khiển con khi khởi động theo mặc định, nhưng hành vi này có thể thay đổi bằng cách không thêm chúng vào tabBarController.viewControllers ngay lập tức và thay thế chúng khi người dùng chuyển đổi. UIStackView với arrangedSubviews được thêm động cũng tuân theo nguyên tắc Lazy Loading — chỉ thêm Subview khi người dùng thực hiện hành động yêu cầu phần đó của giao diện.
Để tối ưu hóa tải màn hình tổng thể, hãy kết hợp Lazy Loading ở tất cả các cấp: ViewStub cho các phần ít sử dụng, Paging 3 cho dữ liệu, Glide/Coil cho hình ảnh và khởi tạo chậm ViewModel thông qua Hilt/Dagger Scopes hoặc Swinject. Cách tiếp cận này tạo ra màn hình tải trong 200–400 ms ngay cả trên các thiết bị giá rẻ với 3 GB RAM.
Câu hỏi thường gặp
Nếu màn hình đảm bảo hiển thị ít mục (tối đa 20) và tất cả đều cần ngay lập tức, Lazy Loading là không cần thiết. Đối với danh sách hiếm khi cuộn, Eager Loading có thể đơn giản và nhanh hơn để triển khai mà không mất hiệu suất đáng kể.
Giảm mức tiêu thụ bộ nhớ cao nhất từ 3–10 lần cho danh sách lớn, vì chỉ các phần tử hiển thị cộng với bộ đệm tải trước được lưu trong bộ nhớ. Tuy nhiên, việc thêm tải trước và bộ nhớ đệm hình ảnh tạo ra mức tiêu thụ bộ nhớ vừa phải cần được quản lý.
List thích hợp hơn cho dữ liệu đồng nhất với hỗ trợ vuốt, kéo và thả và chọn tích hợp. LazyVStack dành cho bố cục tùy chỉnh với các loại ô, phần và khoảng cách không chuẩn khác nhau. List bên trong hoạt động như LazyVStack với chức năng bổ sung.
Trên Android, sử dụng Layout Inspector để xem hệ phân cấp View: với Lazy Loading, hầu hết các phần tử sẽ không có trong cây. Trên iOS, sử dụng Xcode Debug View Hierarchy. Nếu tất cả các phần tử đều có trong khi cuộn, Lazy Loading không hoạt động.
Cả hai thông số đều quan trọng, nhưng ưu tiên phụ thuộc vào nền tảng. Trên iOS với ARC và quản lý bộ nhớ hiệu quả, ưu tiên là tốc độ tải. Trên Android với JVM và GC, ưu tiên là tiết kiệm bộ nhớ, vì mỗi lần cấp phát trên luồng chính có thể gây ra đóng băng GC.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm