Lazy Loading คือกลยุทธ์การโหลดข้อมูล รูปภาพ และคอมโพเนนต์แบบหน่วงเวลา โดยทรัพยากรจะไม่ถูกขอเมื่อเริ่มต้นแอป แต่จะถูกขอในเวลาที่ผู้ใช้ต้องการจริง ๆ ตาม Android Paging 3 Guide การโหลดรายการแบบหน่วงเวลาช่วยลดการใช้หน่วยความจำลง 60–80% เมื่อทำงานกับชุดข้อมูลขนาดใหญ่ การเริ่มต้นแบบหน่วงเวลา เป็นหลักการสำคัญที่อยู่ภายใต้การใช้งาน Lazy Loading ทั้งหมด
ประเด็นสำคัญ
Lazy Loading (การโหลดแบบหน่วงเวลา) เป็นรูปแบบการออกแบบและการปรับแต่งซึ่งทรัพยากรของแอปพลิเคชันจะไม่ถูกโหลดเมื่อเริ่มต้น แต่จะถูกโหลดก่อนการใช้งานทันที ในการพัฒนาแอปมือถือ Lazy Loading ใช้กับสามหมวดหมู่หลัก: ข้อมูล (การแบ่งหน้ารายการ) รูปภาพ (การโหลดเมื่อเลื่อน) และคอมโพเนนต์ (สแต็กและวิวแบบหน่วง)
สิ่งที่ตรงกันข้ามกับ Lazy Loading คือ Eager Loading (การโหลดทันที) ซึ่งทรัพยากรทั้งหมดจะถูกโหลดเมื่อเริ่มต้นหน้าจอ Eager Loading ใช้งานได้ง่ายกว่าแต่ใช้หน่วยความจำมากกว่าและเพิ่มเวลาในการแสดงผลครั้งแรก สำหรับรายการที่มีรายการนับพัน Eager Loading นำไปสู่ OOM (หน่วยความจำไม่เพียงพอ) บนอุปกรณ์ที่มีหน่วยความจำจำกัด Lazy Loading แก้ปัญหานี้โดยโหลดเฉพาะสิ่งที่เห็นบนหน้าจอและโหลดส่วนที่เหลือเมื่อผู้ใช้ เลื่อน
ในบริบทของ iOS และ Android Lazy Loading ถูกนำไปใช้ในระดับต่าง ๆ SwiftUI มี LazyVStack และ LazyHStack สำหรับการเรนเดอร์แบบหน่วง UIKit ใช้ UITableView กับ dequeueReusableCell Android ใช้ RecyclerView กับพูล ViewHolder ในระดับข้อมูล Room กับ Paging 3 และ Core Data กับ NSFetchedResultsController การเลือกเทคโนโลยีเฉพาะขึ้นอยู่กับ สแต็ก และข้อกำหนดด้านประสิทธิภาพ
หลักการสำคัญ ของ Lazy Loading คือการโหลดข้อมูลในปริมาณที่จำเป็นสำหรับสถานะหน้าจอปัจจุบันเท่านั้น รวมถึงบัฟเฟอร์เชิงคาดการณ์สำหรับการเลื่อนที่ราบรื่น วิธีการนี้ขึ้นอยู่กับสองกลไก: การติดตามการมองเห็นและการจำลองเสมือนขององค์ประกอบ
กลไกการติดตาม กำหนดว่าองค์ประกอบใดอยู่ในพื้นที่ที่มองเห็นได้ของหน้าจอ (viewport) บน Android LinearLayoutManager หรือ GridLayoutManager จะดำเนินการนี้ผ่านเมธอด findFirstVisibleItemPosition และ findLastVisibleItemPosition ใน iOS UIScrollView มี bounds.origin.y และ contentOffset.height สำหรับคำนวณพื้นที่ที่มองเห็น เมื่อองค์ประกอบเข้าสู่ viewport (หรือบัฟเฟอร์การโหลดล่วงหน้า) การโหลดจะเริ่มต้นขึ้น เมื่อองค์ประกอบออกจากหน้าจอ ทรัพยากรของมันสามารถ ถูกปล่อย หรือย้ายไปยังแคช
// Android — การติดตามการมองเห็นใน 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
// กำลังโหลดหน้าถัดไปหากยังเหลือ < 5 องค์ประกอบ
if (lastVisible >= totalCount - 5) {
loadMoreItems()
}
}
})
บัฟเฟอร์การโหลดล่วงหน้า คือการโหลดเชิงคาดการณ์ขององค์ประกอบที่จะปรากฏบนหน้าจอเร็ว ๆ นี้ RecyclerView รองรับ GapWorker.Prefetch ผ่าน layoutManager.setItemPrefetchEnabled(true) iOS UITableView รองรับการโหลดล่วงหน้าผ่าน UITableViewDataSourcePrefetching ขนาดของบัฟเฟอร์การโหลดล่วงหน้าปกติคือ 1–2 หน้าจอล่วงหน้า ซึ่งให้ความสมดุลระหว่างความราบรื่นในการเลื่อนและการใช้หน่วยความจำ บัฟเฟอร์การโหลดล่วงหน้าที่ ใหญ่เกินไป จะลบล้างข้อดีของ Lazy Loading เล็กเกินไปจะสร้างพื้นที่ว่างเมื่อเลื่อนเร็ว
รูปภาพ เป็นทรัพยากรประเภทที่หนักที่สุดในแอปพลิเคชันมือถือ รูปภาพ 12 MP เพียงรูปเดียวอาจใช้พื้นที่ 3–5 MB ในรูปแบบที่ไม่บีบอัด Lazy Loading ของรูปภาพป้องกันการโหลดรูปภาพที่มองไม่เห็นหลายร้อยภาพเข้าสู่หน่วยความจำ ซึ่งจะเป็นอันตรายสำหรับรายการที่มีรูปโปรไฟล์ผู้ใช้หรือแคตตาล็อกสินค้า
Glide เป็นไลบรารีโหลดรูปภาพที่ได้รับความนิยมมากที่สุดสำหรับ Android รองรับการแคช การแปลง และแอนิเมชัน Coil เป็นทางเลือกที่เบากว่า เขียนด้วย Kotlin โดยใช้ coroutines ไลบรารีทั้งสองจะหยุดโหลดโดยอัตโนมัติเมื่อ ImageView ออกจากหน้าจอและยกเลิกคำขอเมื่อ ViewHolder ถูกใช้ซ้ำ Coil ใช้ coroutines และมีขนาด ~1.5 MB เทียบกับ ~4 MB ของ Glide ทำให้เป็นที่นิยมสำหรับโปรเจกต์ที่เน้นขนาด APK
// Coil — การโหลดรูปภาพแบบหน่วงเวลา
imageView.load("https://example.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.placeholder)
size(512, 512)
memoryCachePolicy(CachePolicy.ENABLED)
}
Kingfisher เป็นไลบรารีสำหรับ iOS รองรับ Swift Concurrency การแคชดิสก์และหน่วยความจำ และการโหลดล่วงหน้าสำหรับ UICollectionView SDWebImage เป็นไลบรารีเก่าที่มีรากฐานจาก Objective-C แต่รองรับ Swift ไลบรารีทั้งสองทำงานร่วมกับ UIImageView และจัดการวงจรการโหลดโดยอัตโนมัติ: ยกเลิกคำขอเมื่อเซลล์ถูกใช้ซ้ำ โหลดรูปภาพเมื่อเซลล์มองเห็นเท่านั้น และปล่อยหน่วยความจำเมื่อได้รับการ แจ้งเตือนหน่วยความจำต่ำ
รายการข้อมูลขนาดใหญ่ เป็นพื้นที่หลักของการใช้ Lazy Loading ในแอปมือถือ ฟีดข่าว แคตตาล็อกสินค้า แชท ประวัติธุรกรรม — หน้าจอใด ๆ ที่มีรายการที่อาจไม่มีที่สิ้นสุดจำเป็นต้องมีการแบ่งหน้าและการโหลดแบบหน่วงเวลา
Paging 3 เป็นไลบรารีจาก Android Jetpack ที่ใช้วงจรการโหลดแบบหน่วงเวลาอย่างสมบูรณ์: การขอข้อมูลจาก RemoteMediator (API + ฐานข้อมูล) การแคชใน Room การส่งออกทีละหน้าผ่าน PagingData และการแสดงผลผ่าน AsyncPagingDataAdapter Paging 3 รองรับการแบ่งหน้าสามประเภท: แบบหน้า, แบบรายการ (offset/limit) และแบบคีย์ (คีย์การแบ่งหน้าจาก API) Separator คือการรองรับตัวคั่นระหว่างหน้าในตัวสำหรับตัวบ่งชี้การโหลด
// Paging 3 — การโหลดแบบหน่วงเวลาจาก 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 คือคอนเทนเนอร์ในตัวของ SwiftUI ที่สร้างและเรนเดอร์องค์ประกอบเมื่อปรากฏบนหน้าจอเท่านั้น แตกต่างจาก VStack ที่คำนวณเลย์เอาต์ขององค์ประกอบย่อยทั้งหมดทันที LazyVStack จะเลื่อนการสร้างวิวจนกว่าองค์ประกอบจะมองเห็นหรือเข้าสู่ช่วงการโหลดล่วงหน้า LazyHStack คือเวอร์ชันแนวนอนสำหรับภาพหมุน สำหรับรายการขนาดใหญ่ Apple แนะนำให้ใช้ List ซึ่งภายในทำงานคล้ายกับ LazyVStack พร้อมการรีไซเคิลในตัวเพิ่มเติม
// SwiftUI — LazyVStack พร้อมการโหลดแบบหน่วงเวลา
ScrollView {
LazyVStack(spacing: 8) {
ForEach(articles) { article in
ArticleRow(article: article)
.onAppear {
if article == articles.last {
viewModel.loadMore()
}
}
}
}
}
การโหลดคอมโพเนนต์ UI แบบหน่วงเวลา เป็นเทคนิคที่ส่วนต่าง ๆ ของอินเทอร์เฟซ (ส่วนหัว ส่วนท้าย ส่วนการตั้งค่า แท็บ) จะไม่ถูกสร้างเมื่อเริ่มต้นหน้าจอ แต่จะถูกสร้างเมื่อเข้าถึงครั้งแรก ซึ่งช่วยเร่งการเรนเดอร์เริ่มต้นและลดภาระบนเธรดหลัก
ViewStub คือตัวยึดตำแหน่ง View แบบเบาใน Android ที่ไม่กินพื้นที่ในเลย์เอาต์และไม่สร้าง View ย่อยจนกว่าจะเรียก inflate() เหมาะสำหรับส่วนที่ใช้งานน้อย: แผงค้นหา การตั้งค่าขั้นสูง บล็อกโฆษณา การโหลด Fragment แบบหน่วงเวลา เป็นเทคนิคที่ Fragment.onCreateView จะถูกเลื่อนออกไปจนกว่าผู้ใช้จะสลับไปที่แท็บนั้น ใช้งานผ่าน isVisible หรือ UserVisibleHint ใน ViewPager
AndroidX มี SplitInstallManager สำหรับการโหลดโมดูลแบบหน่วงเวลาตามความต้องการ โมดูลการตั้งค่า การวินิจฉัย หรือคุณสมบัติเพิ่มเติมจะถูกโหลดเป็นโมดูล Dynamic Feature เมื่อผู้ใช้ร้องขอครั้งแรกเท่านั้น ซึ่งช่วยลดขนาดฐานของแอปได้ 30–50% และนำ หลักการ ของ Lazy Loading ไปใช้ไม่เพียงในระดับข้อมูล แต่ยังในระดับโค้ดด้วย
TabView ใน SwiftUI โหลดเนื้อหาของแต่ละแท็บแบบหน่วงเวลา — เฉพาะเมื่อแท็บถูกเปิดใช้งาน UIKit UITabBarController สร้างคอนโทรลเลอร์ย่อยทั้งหมดเมื่อเริ่มต้นตามค่าเริ่มต้น แต่พฤติกรรมนี้สามารถเปลี่ยนแปลงได้โดยไม่เพิ่มลงใน tabBarController.viewControllers ทันทีและแทนที่เมื่อผู้ใช้สลับ UIStackView ที่มี arrangedSubviews เพิ่มแบบไดนามิกก็เป็นไปตามหลักการ Lazy Loading เช่นกัน — เพิ่ม Subview เมื่อผู้ใช้ดำเนินการที่ต้องการส่วนนั้นของอินเทอร์เฟซเท่านั้น
เพื่อ ปรับแต่ง การโหลดหน้าจอโดยรวม ให้รวม Lazy Loading ในทุกระดับ: ViewStub สำหรับส่วนที่ใช้งานน้อย Paging 3 สำหรับข้อมูล Glide/Coil สำหรับรูปภาพ และการเริ่มต้น ViewModel แบบหน่วงเวลาผ่าน Hilt/Dagger Scopes หรือ Swinject วิธีการนี้ให้หน้าจอที่โหลดภายใน 200–400 มิลลิวินาทีแม้บนอุปกรณ์ราคาประหยัดที่มี RAM 3 GB
คำถามที่พบบ่อย
หากหน้าจอแสดง รายการน้อย (สูงสุด 20) และทั้งหมดจำเป็นทันที Lazy Loading ก็เกินความจำเป็น สำหรับรายการที่ไม่ค่อยมีการเลื่อน Eager Loading อาจใช้งานได้ง่ายและเร็วกว่าโดยไม่สูญเสียประสิทธิภาพที่เห็นได้ชัด
ลดการใช้หน่วยความจำสูงสุดลง 3–10 เท่าสำหรับรายการขนาดใหญ่ เนื่องจากมีเพียงองค์ประกอบที่มองเห็นและบัฟเฟอร์การโหลดล่วงหน้าเท่านั้นที่ถูกเก็บในหน่วยความจำ อย่างไรก็ตาม การเพิ่มการโหลดล่วงหน้าและการแคชรูปภาพสร้าง ค่าใช้จ่ายหน่วยความจำปานกลาง ที่ต้องจัดการ
List เหมาะกับข้อมูลที่เหมือนกัน รองรับการปัด การลากและวาง และการเลือกในตัว LazyVStack สำหรับเลย์เอาต์ที่กำหนดเองที่มีประเภทเซลล์ ส่วน และระยะห่างที่ไม่ได้มาตรฐานต่างกัน List ภายในทำงานเหมือน LazyVStack พร้อมฟังก์ชันเพิ่มเติม
บน Android ใช้ Layout Inspector เพื่อดูลำดับชั้น View: ด้วย Lazy Loading องค์ประกอบส่วนใหญ่ควรหายไปจากทรี บน iOS ใช้ Xcode Debug View Hierarchy หากองค์ประกอบทั้งหมดปรากฏอยู่ระหว่างการเลื่อน แสดงว่า Lazy Loading ไม่ทำงาน
ทั้งสองพารามิเตอร์ สำคัญ แต่ลำดับความสำคัญขึ้นอยู่กับแพลตฟอร์ม บน iOS ที่มี ARC และการจัดการหน่วยความจำที่มีประสิทธิภาพ ลำดับความสำคัญคือความเร็วในการโหลด บน Android ที่มี JVM และ GC ลำดับความสำคัญคือการประหยัดหน่วยความจำ เนื่องจากการจัดสรรทุกครั้งบนเธรดหลักอาจทำให้ GC หยุดชะงัก
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ