แล็ก ในแอปมือถือคือความล่าช้าที่สังเกตได้ระหว่างการกระทำของผู้ใช้และการตอบสนองของอินเทอร์เฟซ ซึ่งเกิดจากเธรดหลักทำงานหนักเกินไป หน่วยความจำรั่ว หรือการดำเนินการ I/O ที่ไม่เหมาะสม ต่างจากข้อบกพร่องที่เกี่ยวข้องกับข้อผิดพลาดทางตรรกะ แล็กเป็นปัญหาเรื่องประสิทธิภาพ: แอปทำงานถูกต้องแต่ช้า ตามรายงานของ AppDynamics Mobile App Performance Report 2024 ผู้ใช้ 62% ลบแอปหากแอปแล็กนานกว่า 3 วินาที การวินิจฉัยแล็กต้องทำการโปรไฟล์ CPU หน่วยความจำ และเครือข่ายด้วย Android Studio Profiler และ Xcode Instruments
ประเด็นสำคัญ
แล็ก ในแอปมือถือคือความล่าช้าที่รับรู้ได้ตามอัตวิสัยระหว่างการกระทำของผู้ใช้ (การแตะ การปัด การป้อนข้อความ) และการตอบสนองของอินเทอร์เฟซ ในทางเทคนิค แล็กวัดเป็นเวลาระหว่างเหตุการณ์อินพุตและการเรนเดอร์เฟรมที่สมบูรณ์: เกณฑ์ที่สบายคือสูงสุด 100 ms สังเกตได้ตั้งแต่ 200 ms วิกฤตมากกว่า 500 ms
ในศัพท์เฉพาะของผู้ใช้ “แล็ก” และ “ช้า” มักใช้เป็นคำพ้องความหมาย แต่ในทางเทคนิค แล็ก คือความล่าช้าที่คงที่ (เช่น 300 ms ทุกครั้งที่แตะ) ในขณะที่ “ช้า” คือการชะลอตัวเป็นระยะ ๆ แอปทำงานลื่นแล้วค้างเป็นวินาที ข้อบกพรุงแตกต่างจากแล็กตรงที่ไม่เกี่ยวกับความเร็วแต่เกี่ยวกับความถูกต้องของการแสดงผล
Google Play และ App Store พิจารณาเมตริกประสิทธิภาพเมื่อจัดอันดับแอป อัตรา ANR ความถี่ของ jank และเวลาเริ่มต้นส่งผลต่อการมองเห็นในการค้นหาและการแปลงการติดตั้ง แอปที่มีแล็กต่อเนื่องจะสูญเสียผู้ใช้ถึง 40% หลังจากการเปิดครั้งแรก
แล็กเกิดขึ้นเมื่อเธรด UI หลักไม่สามารถประมวลผลเฟรมที่ 60 FPS (16.6 ms ต่อเฟรม) หรือ 120 FPS (8.3 ms) มาดูสาเหตุหลักของความล่าช้ากัน
การดำเนินการแบบซิงโครนัสใด ๆ ในเธรด UI — การอ่านจาก SharedPreferences การทำงานกับฐานข้อมูลผ่าน Room โดยไม่มี suspend การถอดรหัสรูปภาพเป็น Bitmap — จะบล็อกการเรนเดอร์เฟรม บน Android สิ่งนี้ทำให้เกิด jank บน iOS ทำให้เกิดความล่าช้าในการเรนเดอร์ Core Animation
เมื่อ ตัวเก็บขยะ บน Android หรือ ARC บน iOS ทำการคืนหน่วยความจำ เธรดทั้งหมดจะถูกหยุดชั่วคราว การหยุด GC บ่อยครั้งเกิดขึ้นเมื่อสร้างอ็อบเจกต์ชั่วคราวจำนวนมาก — เช่น การสร้างอินสแตนซ์ ViewHolder ใหม่ทุกครั้งที่เรียกอะแดปเตอร์ สิ่งนี้แสดงออกมาเป็นการเลื่อนที่สะดุด
ConstraintLayout ที่ซ้อนกัน LinearLayout หลายชั้น View ที่ซ้อนทับกัน — แต่ละระดับการซ้อนจะเพิ่มเวลาในการ วัด และ layout pass Xcode ระบุว่าลำดับชั้นเลเยอร์ที่ลึก (มากกว่า 10 ระดับ) ทำให้ FPS ลดลง 20-30%
เพื่อระบุสาเหตุของแล็ก จะใช้โปรไฟล์เลอร์ที่ติดตั้งใน IDE และเครื่องมือตรวจสอบระบบ แต่ละเครื่องมือแก้ปัญหาของตัวเอง
CPU Profiler แสดงว่าเมธอดใดใช้เวลา CPU และทำงานในเธรดใด หากเมธอดการคำนวณหนักทำงานในเธรดหลัก — นั่นคือสาเหตุหลัก การบันทึกการติดตามโดยเปิดใช้งาน sample Java Method ช่วยให้เห็นสแต็กการเรียกได้ทุกขณะและค้นหาจุดร้อน
เครื่องมือที่เทียบเท่าสำหรับ iOS — Time Profiler — รวบรวมตัวอย่างสแต็กทุกมิลลิวินาทีและแสดงเปอร์เซ็นต์ของเวลา CPU ที่แต่ละเมธอดใช้ไป เมื่อรวมกับแฟล็ก Main Thread Only จะกรองเฉพาะการดำเนินการบนเธรดหลัก ซึ่งชี้ไปยังแหล่งที่มาของแล็กโดยตรง
คำขอเครือข่ายที่ช้าสร้างความประทับใจของแล็กแม้ว่าเธรด UI จะไม่ได้ถูกบล็อก Network Profiler ใน Android Studio และ Network Link Conditioner ใน Xcode ช่วยให้จำลองการเชื่อมต่อที่ช้าและระบุว่าแอปทำงานอย่างไรในสภาพจริง การตอบสนองแบบแบ่งส่วนโดยไม่มีความคืบหน้าและเพย์โหลด JSON ขนาดใหญ่เป็นแหล่งทั่วไปของแล็กที่เห็นได้ชัด
ตัวอย่างการโปรไฟล์คำขอเครือข่ายด้วย OkHttp พร้อมการวัดเวลา:
class TimingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val start = System.nanoTime()
val response = chain.proceed(chain.request())
val duration = (System.nanoTime() - start) / 1_000_000
Log.d("Timing", "Request took $duration ms")
return response
}
}
การแก้ไขแล็กต้องทำงานอย่างเป็นระบบ: ตั้งแต่การปรับแต่งเมธอดเดียวไปจนถึงการเปลี่ยนแปลงทางสถาปัตยกรรม มาดูเทคนิคที่มีประสิทธิภาพที่สุดกัน
Kotlin Coroutines ด้วย Dispatchers.IO สำหรับคำขอเครือข่ายและ Dispatchers.Default สำหรับการคำนวณทำให้มั่นใจว่าเธรดหลักว่างสำหรับ UI บน iOS Grand Central Dispatch ด้วย queue .global(qos: .userInitiated) สำหรับงานพื้นหลังและ .main สำหรับการอัปเดต UI เป็นแนวทางมาตรฐาน หลีกเลี่ยงการดำเนินการ sync ระหว่างคิว
RecyclerView บน Android และ UICollectionView บน iOS ต้องการการกำหนดค่าที่ถูกต้อง: ViewHolder ที่มีการสร้างอ็อบเจกต์น้อยที่สุดใน onBindViewHolder, DiffUtil สำหรับการคำนวณการเปลี่ยนแปลง, prefetching สำหรับการโหลดข้อมูลล่วงหน้า บน iOS ใช้ diffable data source สำหรับการอัปเดตแบบเคลื่อนไหวโดยไม่ต้องจัดการด้วยตนเอง
การโหลดรูปภาพเดียวกันทุกครั้งที่เลื่อนคือแล็กที่แน่นอน Coil (Android) และ Kingfisher (iOS) แคชรูปภาพในหน่วยความจำและบนดิสก์ ทำให้แสดงผลทันทีเมื่อมีการร้องขอซ้ำ สำหรับข้อมูล ให้ใช้ Room พร้อมชั้นแคชที่อิงตาม Flow หรือ Combine
ตัวอย่างการกำหนดค่าแคชรูปภาพด้วย Coil บน Android:
val imageLoader = ImageLoader(context) {
memoryCachePolicy(CachePolicy.ENABLED)
diskCachePolicy(CachePolicy.ENABLED)
crossfade(true)
size(512, 512)
}
// Loading with auto-caching enabled
imageView.load("https://example.com/image.jpg") {
placeholder(R.drawable.placeholder)
error(R.drawable.error)
}
การป้องกันแล็กถูกกว่าการแก้ไขในโปรดักชัน มาตรการป้องกันถูกสร้างไว้ในกระบวนการพัฒนาในระดับเครื่องมือและสถาปัตยกรรม
StrictMode เป็นเครื่องมือในตัวของ Android ที่ตรวจจับการดำเนินการ I/O และการเรียกเครือข่ายโดยไม่ได้ตั้งใจบนเธรดหลักระหว่างการพัฒนา เปิดใช้งานใน Application.onCreate ด้วยนโยบาย penaltyDeath สำหรับการละเมิดที่สำคัญ นี่เป็นวิธีเดียวที่จะมั่นใจได้ว่านักพัฒนาจะเห็นปัญหาก่อนคอมมิต
เครื่องมือที่เทียบเท่าสำหรับ iOS — Main Thread Checker ใน Xcode ซึ่งเป็นส่วนหนึ่งของ Runtime Sanitization — ตรวจสอบโดยอัตโนมัติว่าการเรียก UIKit และ AppKit ทั้งหมดทำงานบนเธรดหลัก เปิดใช้งานในสคีมาบิลด์ Debug และตั้งเป้าหมายให้ไม่มีคำเตือนใน CI
เพิ่มการรัน Macrobenchmark (Android) และ XCTMetrics (iOS) ในไปป์ไลน์ CI ของคุณเพื่อวัดเวลาเริ่มต้น FPS การเลื่อน และการใช้หน่วยความจำ กำหนดเกณฑ์: หากคอมมิตใหม่เพิ่มเวลาเริ่มต้นมากกว่า 5% — บิลด์จะล้มเหลว
คำถามที่พบบ่อย
แล็กคือความรู้สึกส่วนตัวของความล่าช้าที่สามารถเกิดขึ้นได้แม้ที่ FPS สูง หากความล่าช้าเกิดจาก เวลาประมวลผล อินพุต ไม่ใช่การเรนเดอร์ FPS ต่ำ (น้อยกว่า 30 fps) เป็นสาเหตุหนึ่งของแล็ก แต่ไม่ใช่สาเหตุเดียว
ใช้ Frame Timing API บน Android (Choreographer) และ CADisplayLink บน iOS เพื่อวัดเวลาระหว่างเฟรม Google Play Vitals แสดงอัตรา jank ในสภาพจริง สำหรับการวัดที่แม่นยำ ให้ใช้ Macrobenchmark กับสถานการณ์การเลื่อน
อุปกรณ์เก่ามีคอร์ CPU น้อยกว่า RAM น้อยกว่า และหน่วยความจำช้ากว่า การดำเนินการที่ใช้เวลา 5 ms บนเรือธงอาจใช้เวลา 50 ms บนอุปกรณ์ราคาประหยัด ทดสอบประสิทธิภาพบนอุปกรณ์ระดับล่างและตั้งค่า Baseline Profiles สำหรับการคอมไพล์ AOT
ใช่ นี่เป็นหนึ่งในวิธีที่มีประสิทธิภาพที่สุด รูปภาพความละเอียดสูงใช้หน่วยความจำและ เวลา CPU จำนวนมากในการถอดรหัส ใช้การลดขนาดให้พอดีกับขนาด View รูปแบบ WebP (Android) และ HEIC (iOS) และแคชผ่าน Coil หรือ Kingfisher
SwiftUI ปรับการอัปเดตให้เหมาะสมโดยอัตโนมัติผ่านการ diffing ซึ่งลดความเสี่ยงของแล็กเมื่อข้อมูลเปลี่ยนแปลง อย่างไรก็ตาม ลำดับชั้นที่ซับซ้อนและการสร้าง body บ่อยครั้งอาจทำให้ FPS ลดลง UIKit ควบคุมประสิทธิภาพได้มากกว่าแต่ต้องการการปรับแต่งด้วยตนเอง
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ