การเลื่อนไร้สิ้นสุด (Infinite Scroll) เป็นเทคนิคการโหลดเนื้อหาโดยอัตโนมัติเมื่อผู้ใช้ถึงจุดสิ้นสุดของรายการปัจจุบัน ตามข้อมูลจาก UX Design Collective, 2024, Infinite Scroll เพิ่มเวลาเซสชั่นในโซเชียลมีเดียได้ 40–60% เมื่อเทียบกับการแบ่งหน้า ในการพัฒนามือถือ เทคนิคนี้ถูกนำไปใช้โดยการผสมผสานของ scroll listeners และคำขอ API ด้วยการแบ่งหน้าแบบ cursor-based การเลื่อนไร้สิ้นสุดได้กลายเป็นมาตรฐานโดยพฤตินัยสำหรับฟีดเนื้อหา แต่ต้องมีการนำไปใช้อย่างระมัดระวังเพื่อหลีกเลี่ยงปัญหาด้านประสิทธิภาพและการนำทาง
หลักสำคัญ
เลื่อนไร้สิ้นสุด (Infinite Scroll) เป็นรูปแบบการโหลดข้อมูลที่องค์ประกอบใหม่จะถูกเพิ่มไปยังท้ายของรายการโดยอัตโนมัติเมื่อผู้ใช้เลื่อน ผู้ใช้ไม่ต้องคลิกปุ่ม "ถัดไป" หรือ "โหลดเพิ่ม" — ระบบเองเป็นผู้กำหนดเมื่อที่จะขอชุดข้อมูลถัดไป และแทรกรายการใหม่ไปยังรายการที่มีอยู่อย่างราบรื่น
Infinite Scroll ได้รับความนิยมโดยเพศจากโซเชียลมีเดีย — Twitter, Instagram และ TikTok ใช้มันเป็นกลไกการจัดส่งเนื้อหาหลัก ตามข้อมูลจาก Nielsen Norman Group (2024), เลื่อนไร้สิ้นสุดเพิ่มการมีส่วนร่วมขึ้น 30–50% สำหรับแอปพลิเคชันเนื้อหาเพราะลดภาระทางปัญญา: ผู้ใช้ไม่ต้องตัดสินใจที่จะไปหน้าถัดไป อย่างไรก็ตาม สำหรับงานที่ต้องการการนำทางที่แม่นยำ (การค้นหา, การเปรียบเทียบสินค้า), เลื่อนไร้สิ้นสุดอาจลดประสิทธิภาพ
ในทางเทคนิค, เลื่อนไร้สิ้นสุดประกอบด้วยสามองค์ประกอบ: scroll listener (ติดตามตำแหน่งการเลื่อน), threshold (ระยะทางไปยังจุดสิ้นสุดของรายการเพื่อทริกเกอร์การโหลด) และ กลไกการแบ่งหน้า (คำขอ API และการแทรกข้อมูล) การกำหนดค่า threshold ที่เหมาะสมเป็นสิ่งสำคัญ: หากตัวกระตุ้นเร็วเกินไป (1000 px จากจุดสิ้นสุด) ผู้ใช้จะได้รับคำขอที่ไม่จำเป็น; หากช้าเกินไป (50 px) ผู้ใช้จะสังเกตเห็นการหยุดชั่วขณะในการโหลด
สถาปัตยกรรมของการเลื่อนไร้สิ้นสุดสร้างบน โมเดลที่ขับเคลื่อนโดยเหตุการณ์: องค์ประกอบรายการสร้างเหตุการณ์เมื่อถึงเกณฑ์การเลื่อน, ViewModel ประมวลผลมันและเรียกพื้นที่เก็บข้อมูลเพื่อโหลดชุดข้อมูลถัดไป หลังจากได้รับคำตอบ, องค์ประกอบใหม่จะถูกแทรกในรายการ และ UI จะถูกอัพเดตผ่านอะแดปเตอร์ ห่วงโซ่นี้ต้องเป็นแบบอะซิงโครนัสและต้องไม่บล็อกเธรด UI
ขั้นตอนวิธีพื้นฐานของการเลื่อนไร้สิ้นสุดประกอบด้วยสี่ขั้นตอน การเริ่มต้น: เมื่อหน้าจอถูกเปิดครั้งแรก, ชุดข้อมูลแรกจะถูกโหลด (หน้า 1 หรือ cursor = null) การติดตาม: scroll listener ตรวจสอบว่าผู้ใช้ถึงเกณฑ์หรือไม่ — โดยปกติ 200–500 px จากจุดสิ้นสุดของรายการ การโหลด: จะมีการส่งคำขอไปยัง API พร้อมพารามิเตอร์การแบ่งหน้า, ตัวบ่งชี้การโหลด (spinner ในส่วนล่าง) จะแสดงบน UI การแทรก: องค์ประกอบใหม่จะถูกเพิ่มในอะแดปเตอร์, ตำแหน่งการเลื่อนจะถูกปรับเพื่อหลีกเลี่ยงการกระโดด
ประเด็นสำคัญคือ debounce ของคำขอ หากผู้ใช้เลื่อนไปยังจุดสิ้นสุดอย่างรวดเร็ว, ตัวกระตุ้นอาจทำงานหลายครั้งก่อนที่จะได้รับคำตอบจากเซิร์ฟเวอร์ หากไม่มี debounce, สิ่งนี้จะนำไปสู่คำขอที่ซ้ำกัน (race condition) วิธีแก้คือบล็อกคำขอใหม่จนกว่าคำขอก่อนหน้าจะเสร็จสิ้น แฟล็ก isLoading ใน ViewModel จะป้องกันการเรียกหลายครั้ง: ตั้ง isLoading = true เมื่อส่งคำขอ, รีเซ็ตเมื่อได้รับคำตอบหรือข้อผิดพลาด
บนแพลตฟอร์มมือถือ, กลไกเฉพาะทางถูกใช้สำหรับการเลื่อนไร้สิ้นสุด บน iOS, คือ prefetchDataSource ใน UICollectionView ซึ่งจะขอข้อมูลโดยอัตโนมัติสำหรับเซลล์ที่อยู่นอกหน้าจอ บน Android, คือ Paging 3 Library ของ Google ซึ่งให้สถาปัตยกรรมพร้อมใช้กับ PagingSource, PagingData และ PagingDataAdapter Paging 3 รองรับ RemoteMediator สำหรับการรวมข้อมูลเครือข่ายและข้อมูลในเครื่องและจัดการสถานะการโหลดโดยอัตโนมัติ
การแบ่งหน้าแบบ Offset-based ใช้พารามิเตอร์ page และ size: page=2, size=20 จะคืนรายการ 21–40 วิธีการนี้ง่ายต่อการนำไปใช้แต่มีปัญหาพื้นฐาน — หากมีการเพิ่มหรือลบรายการในฐานข้อมูลระหว่างคำขอ, ค่า offset จะคลาดเคลื่อน (ผู้ใช้เห็นข้อมูลซ้ำหรือช่องว่าง) สำหรับฟีดที่มีความถี่การเปลี่ยนแปลงสูง (ข่าว, ความคิดเห็น), การแบ่งหน้าแบบ offset-based จะให้ผลลัพธ์ที่ไม่ถูกต้อง
การแบ่งหน้าแบบ Cursor-based ใช้ตัวระบุเฉพาะขององค์ประกอบสุดท้าย (cursor): after=id_12345&limit=20 เซิร์ฟเวอร์จะคืน 20 รายการหลังจาก cursor ที่ระบุ วิธีการนี้รับประกัน ความสม่ำเสมอของข้อมูล โดยไม่คำนึงถึงการเพิ่มและการลบ ตาม GraphQL Best Practices (2024), การแบ่งหน้าแบบ cursor-based ได้รับคำแนะนำสำหรับแอปพลิเคชันแบบเรียลไทม์ทั้งหมดที่ข้อมูลเปลี่ยนแปลงแบบไดนามิก
การเลือกระหว่างวิธีการขึ้นอยู่กับประเภทของแอปพลิเคชัน สำหรับ โซเชียลมีเดีย (Instagram, TikTok) — เฉพาะ cursor-based, เนื่องจากฟีดถูกอัพเดตอย่างต่อเนื่อง สำหรับ แคตตาล็อก ที่มีการเปลี่ยนแปลงน้อย (หมวดหมู่สินค้าของร้านค้าออนไลน์), การแบ่งหน้าแบบ offset-based เป็นที่ยอมรับได้ สำหรับสถานการณ์แบบลูกผสม, Google แนะนำ Paging 3 RemoteMediator ซึ่งรวมการแบ่งหน้าแบบ cursor-based จากเครือข่ายกับการแบ่งหน้าแบบ offset-based จากฐานข้อมูล Room ในเครื่อง
บน Android วิธีการมาตรฐานคือไลบรารี Paging 3 จาก Jetpack PagingSource กำหนดแหล่งข้อมูล (เครือข่ายหรือฐานข้อมูล), PagingData ประกอบด้วยชุดข้อมูล, และ PagingDataAdapter แสดงผลใน RecyclerView Paging 3 จัดการระยะทางการโหลดล่วงหน้า, การลองใหม่ และการรีเฟรชโดยอัตโนมัติ สำหรับการรวมเครือข่าย, ใช้ RemoteMediator: มันโหลดข้อมูลจาก API, บันทึกใน Room, และแจ้ง PagingSource เกี่ยวกับการอัพเดต ตาม Google I/O 2024, มากกว่า 60% ของแอป Android ที่ใช้เลื่อนไร้สิ้นสุดใช้ Paging 3
ตัวอย่างการนำไปใช้ Paging 3 ขั้นพื้นฐาน:
class FeedPagingSource(
private val api: FeedApi
) : PagingSource<String, Post>() {
override suspend fun load(
params: LoadParams<String>
): LoadResult<String, Post> {
val response = api.getFeed(
cursor = params.key,
limit = params.loadSize
)
return LoadResult.Page(
data = response.items,
prevKey = null,
nextKey = response.nextCursor
)
}
}
บน iOS การเลื่อนไร้สิ้นสุดถูกนำไปใช้โดยใช้ UICollectionView กับ prefetchDataSource โปรโตคอล UICollectionViewDataSourcePrefetching มีเมธอด collectionView(_:prefetchItemsAt:) ซึ่งจะถูกเรียกเมื่อระบบคาดการณ์การเลื่อนไปยัง index paths บางอย่าง แตกต่างจาก Android Paging 3, iOS ไม่มีไลบรารีการแบ่งหน้าในตัว — นักพัฒนาต้องนำไปใช้ด้วยตนเองหรือใช้โซลูชันของบุคคลที่สามเช่น RxSwift + NSLayoutConstraint หรือไปป์ไลน์ที่ใช้ Combine
ตัวอย่าง prefetch บน iOS:
extension FeedViewController: UICollectionViewDataSourcePrefetching {
func collectionView(
_ collectionView: UICollectionView,
prefetchItemsAt indexPaths: [IndexPath]
) {
let lastRow = collectionView.numberOfItems(inSection: 0) - 1
if indexPaths.contains(IndexPath(row: lastRow, section: 0)) {
viewModel.loadNextPage()
}
}
}
SwiftUI ให้วิธีการที่ประกาศมากกว่าผ่านตัวแก้ไข onAppear นักพัฒนาจะวาง ProgressView ที่ท้ายของรายการและเมื่อมันปรากฏขึ้น, จะทริกเกอร์การโหลดหน้าถัดไป ตาม Apple WWDC 2024, API ใหม่ AsyncSequence และ Swift Algorithms ทำให้การนำไปใช้การเลื่อนไร้สิ้นสุดง่ายขึ้นโดยการให้ตัวดำเนินการ chunking และ debounce ในตัว
ปัญหา UX หลักของการเลื่อนไร้สิ้นสุดคือ การสูญเสียส่วนล่างและการนำทาง ในร้านค้าออนไลน์, ผู้ใช้มักต้องการไปที่ส่วนล่างเพื่อข้อมูลติดต่อหรือลิงก์ การเลื่อนไร้สิ้นสุดทำให้ส่วนล่างไม่สามารถเข้าถึงได้ — มันขยับลงไปเรื่อยๆ เมื่อโหลดเนื้อหามากขึ้น วิธีแก้คือเพิ่มปุ่มการกระทำแบบลอย (FAB) สำหรับเลื่อนขึ้นอย่างรวดเร็วหรือยึดส่วนล่างแยกจากรายการ
ปัญหาที่สองคือ การขาดประวัติการเลื่อน หากผู้ใช้เห็นสินค้าที่น่าสนใจที่ตำแหน่ง 3, เลื่อนไปที่ตำแหน่ง 50, แล้วกด "กลับ" — พวกเขาจะกลับไปที่จุดเริ่มต้นของรายการและต้องเลื่อนไปที่ตำแหน่ง 50 อีกครั้ง วิธีแก้คือบันทึกตำแหน่งการเลื่อนใน ViewModel หรือใช้การคืนสภาพสถานะในระดับ Activity/UIViewController iOS รองรับ NSUserActivity สำหรับการคืนสภาพตำแหน่ง, Android รองรับ onSaveInstanceState
ปัญหาที่สามคือ ประสิทธิภาพเมื่อมีองค์ประกอบเป็นพันรายการ หากไม่ได้ตั้งค่าการทำเสมือนส่วนประกอบ, หลังจากโหลดองค์ประกอบ 500–1000 รายการ แอปจะเริ่มช้าเนื่องจากการใช้หน่วยความจำที่เพิ่มขึ้น วิธีแก้คือใช้ การทำเสมือนส่วนประกอบ กับ RecyclerView หรือ UICollectionView ซึ่งเก็บเฉพาะเซลล์ที่เห็นและที่โหลดล่วงหน้าไว้ในหน่วยความจำ การล้างข้อมูลเก่าเป็นระยะ (การทิ้งหน้าที่อยู่เกิน N หน้า) ก็ลดภาระได้เช่นกัน
คำถามที่พบบ่อย
เลื่อนไร้สิ้นสุด (Infinite Scroll) เป็นเทคนิคการโหลดเนื้อหาโดยอัตโนมัติเมื่อผู้ใช้ถึงจุดสิ้นสุดของรายการ ข้อมูลใหม่จะถูกเพิ่มอย่างราบรื่นโดยไม่ต้องคลิกปุ่มแบ่งหน้า ใช้ในโซเชียลมีเดีย, ฟีดข่าว และแคตตาล็อกที่มีเนื้อหาแบบไดนามิก
การแบ่งหน้า ต้องการการนำทางด้วยตนเองระหว่างหน้า (ปุ่ม "1, 2, 3") ในขณะที่ Infinite Scroll โหลดข้อมูลโดยอัตโนมัติ การแบ่งหน้าสามารถคาดการณ์ได้และรักษาบริบทการนำทางไว้; เลื่อนไร้สิ้นสุดเพิ่มการมีส่วนร่วมแต่ทำให้การเข้าถึงส่วนล่างและประวัติการเลื่อนซับซ้อน การเลือกขึ้นอยู่กับประเภทเนื้อหาและเป้าหมายของแอปพลิเคชัน
บน Android, ไลบรารีที่แนะนำคือ Paging 3 จาก Jetpack มันให้ PagingSource สำหรับแหล่งข้อมูล, PagingData สำหรับชุดข้อมูล และ PagingDataAdapter สำหรับ RecyclerView Paging 3 จัดการการโหลดล่วงหน้า, สถานะการโหลด และการลองใหม่โดยอัตโนมัติ สำหรับสถานการณ์แบบลูกผสมออฟไลน์/ออนไลน์, ให้ใช้ RemoteMediator
คำขอซ้ำกันจะถูกป้องกันโดยใช้ แฟล็ก debounce isLoading เมื่อส่งคำขอครั้งแรก, แฟล็กจะถูกตั้งเป็น true และบล็อกการเรียกใหม่จนกว่าจะได้รับคำตอบ หลังจากคำตอบที่สำเร็จ, แฟล็กจะถูกรีเซ็ต นอกจากนี้, คุณสามารถยกเลิก coroutine (Kotlin) หรือ Cancellable (Swift) เมื่อเลื่อนกลับ
เลื่อนไร้สิ้นสุดไม่เหมาะสม สำหรับอีคอมเมิร์ซที่มีการค้นหาและเปรียบเทียบสินค้า, สำหรับแอปที่มีส่วนล่างสำคัญ (ติดต่อ, ลิงก์), สำหรับหน้าผลการค้นหา (ผู้ใช้ต้องกลับไปยังรายการที่เฉพาะเจาะจง) และสำหรับหน้าสถิติ/รายงานที่จำนวนรวมสำคัญ ในกรณีเหล่านี้, ให้ใช้การแบ่งหน้าแบบคลาสสิกหรือปุ่ม "โหลดเพิ่ม"
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม