แอปที่ช้าเป็นสาเหตุหลักที่ทำให้ผู้ใช้ลบโปรแกรม ความล่าช้าเพียงเสี้ยววินาทีเมื่อเริ่มต้นหรือเลื่อนรายการจะลดอัตราการคงอยู่ (retention) ลงหลายสิบเปอร์เซ็นต์ ประสิทธิภาพ (performance) ไม่ใช่แค่ความเร็ว แต่ยังรวมถึงความเสถียรด้วย: ไม่มี ANR การขัดข้อง และหน่วยความจำรั่วไหล บทความนี้ครอบคลุมทุกแง่มุมของประสิทธิภาพ ตั้งแต่การจัดการหน่วยความจำ (GC, ARC) ไปจนถึงการสร้างโปรไฟล์ด้วยเครื่องมือ ดูเพิ่มเติมได้ใน คู่มือ Android Performance อย่างเป็นทางการ
ประเด็นสำคัญ
ประสิทธิภาพของแอปเกี่ยวข้องโดยตรงกับ jank — ความล่าช้าที่สังเกตได้ระหว่างการกระทำของผู้ใช้และการตอบสนองของอินเทอร์เฟซ สาเหตุหลัก: การบล็อกเธรดหลัก (การทำงานหนักบนเธรด UI), การวาดเค้าโครงซ้ำบ่อยครั้ง (overdraw), หน่วยความจำรั่วไหล (GC บ่อยครั้ง), อัลกอริทึมที่ไม่เหมาะสม (O(n²) บนข้อมูลขนาดใหญ่) อัตราเฟรม (FPS) — จำนวนเฟรมต่อวินาที สำหรับประสบการณ์ที่สะดวกสบาย จำเป็นต้องมี 60 FPS ที่เสถียร (Android) หรือ 120 FPS (iPhone Pro, iPad Pro) VSync ซิงโครไนซ์การเรนเดอร์กับอัตรารีเฟรชหน้าจอ
Jank เกิดขึ้นเมื่อการเรนเดอร์เฟรมเดียวเกิน 16.6 มิลลิวินาที (สำหรับ 60 FPS) หรือ 8.3 มิลลิวินาที (สำหรับ 120 FPS) การสร้างโปรไฟล์ GPU (Profile GPU Rendering บน Android, Core Animation บน iOS) แสดงให้เห็นว่าขั้นตอนการเรนเดอร์ใดใช้เวลามากที่สุด ขั้นตอนหลัก: Layout (การจัดวางองค์ประกอบ), Draw (การวาด), Display (การส่งไปยังบัฟเฟอร์เฟรม) ปัญหาที่พบบ่อยที่สุดคือการพองตัวของเค้าโครงใน XML โดยเฉพาะอย่างยิ่งเมื่อใช้ ConstraintLayout ที่ซ้อนกันซับซ้อน
Time-to-Interactive (TTI) — เวลาที่แอปพร้อมสำหรับการโต้ตอบอย่างเต็มที่ TTI รวมถึง Cold Start, การโหลดข้อมูล และการเริ่มต้นไลบรารี Google แนะนำ TTI ต่ำกว่า 5 วินาที, Apple — ต่ำกว่า 2 วินาทีสำหรับหน้าจอหลัก Lazy Loading — เทคนิคการโหลดเนื้อหาและไลบรารีแบบล่าช้า ซึ่งสำคัญต่อการปรับปรุง TTI ที่ IT Sectr เราใช้การเริ่มต้นแบบขี้เกียจเป็นค่าเริ่มต้นในทุกโครงการ
ANR และ Crash เป็นศัตรูหลักของประสิทธิภาพแอปมือถือ ANR (Application Not Responding) — กล่องโต้ตอบที่ปรากฏบน Android หากเธรดหลักถูกบล็อกนานกว่า 5 วินาที สาเหตุ: คำขอเครือข่ายแบบซิงโครนัสบนเธรด UI, การทำงานกับฐานข้อมูลโดยไม่มี coroutine, การถอดรหัสบิตแมปขนาดใหญ่โดยไม่ลดตัวอย่าง, deadlock บนเธรดหลัก สแต็กการเรียก ANR จะถูกบันทึกใน /data/anr/traces.txt และช่วยระบุตำแหน่งที่ถูกบล็อกได้อย่างแม่นยำ
Crash — การสิ้นสุดแอปโดยไม่คาดคิด บน Android — Exception (Java/Kotlin) หรือ Signal (โค้ดเนทิฟ) บน iOS — NSException หรือสัญญาณ (EXC_BAD_ACCESS — การเข้าถึงหน่วยความจำที่ถูกปล่อย) เครื่องมือรายงาน Crash: Firebase Crashlytics, Sentry, BugSnag เครื่องมือเหล่านี้รวบรวม stacktrace, ข้อมูลอุปกรณ์ และขั้นตอนการจำลองสถานการณ์ Stack Overflow — ล้นสแต็กการเรียกจากการเรียกซ้ำไม่สิ้นสุด OutOfMemoryError — เมื่อ heap เต็ม
StrictMode — เครื่องมือ Android สำหรับตรวจจับการละเมิดความปลอดภัยของเธรด ช่วยให้กำหนดกฎ: ThreadPolicy (ห้ามดิสก์/เครือข่ายบนเธรดหลัก), VmPolicy (ตรวจจับการรั่วไหลของ Activity, SQLite, CloseGuard) ควรเปิดใช้ StrictMode เฉพาะในบิลด์ดีบักเท่านั้น — ในรุ่นเผยแพร่ไม่ควรทำงาน บน iOS สิ่งที่เทียบเท่าคือ Main Thread Checker (Xcode) ซึ่งตรวจจับการเรียก UIKit ที่ไม่ได้อยู่ในเธรดหลักโดยอัตโนมัติ
หน่วยความจำรั่วไหล (Memory Leak) — สถานการณ์ที่อ็อบเจกต์ยังคงอยู่ในหน่วยความจำแม้ว่าแอปจะไม่ได้ใช้งานแล้ว สิ่งนี้ลดประสิทธิภาพของแอปโดยตรง บน Android GC (Garbage Collection) ไม่สามารถเก็บอ็อบเจกต์ได้หากมีการอ้างอิงแบบแข็ง (strong reference) ไปยังมัน สาเหตุทั่วไป: การอ้างอิงแบบคงที่ไปยัง Activity, callback/observer ที่ไม่ได้ยกเลิก, คลาสภายในที่มีการอ้างอิงโดยนัยไปยังคลาสภายนอก, Handler ที่มีข้อความที่ไม่ได้ล้าง LeakCanary — ไลบรารีสำหรับตรวจจับการรั่วไหลอัตโนมัติ
ARC (Automatic Reference Counting) — โมเดลการจัดการหน่วยความจำใน iOS แต่ละอ็อบเจกต์มีตัวนับการอ้างอิง (retain count) เมื่อตัวนับถึงศูนย์ หน่วยความจำจะถูกปล่อย Retain Cycle เกิดขึ้นเมื่ออ็อบเจกต์สองตัวมีการอ้างอิงแบบแข็งซึ่งกันและกัน (A → B และ B → A) ARC จะไม่ทำให้ตัวนับเป็นศูนย์ วิธีแก้: การอ้างอิงแบบอ่อน (weak) หรือแบบไม่มีเจ้าของ (unowned) Weak จะกลายเป็น nil โดยอัตโนมัติเมื่ออ็อบเจกต์ถูกปล่อย Unowned จะไม่กลายเป็น nil แต่รับประกันว่าอ็อบเจกต์ยังมีชีวิตอยู่
GC (Garbage Collection) ทำงานบน Android (Java/Kotlin) GC หยุดการทำงานเป็นระยะ (การหยุด Stop-the-World) เพื่อค้นหาและปล่อยอ็อบเจกต์ที่ไม่สามารถเข้าถึงได้ ตัวกระตุ้น GC: เมื่อ heap เต็มถึงเปอร์เซ็นต์ที่กำหนด ARC ทำงานบน iOS (Swift/Objective-C) และไม่มีการหยุด — ตัวนับจะถูกอัปเดตแบบอะตอมมิกทุกครั้งที่มีการกำหนดค่า ARC คาดเดาได้ง่ายกว่า แต่สามารถสะสมการดำเนินการ retain/release ที่มากเกินไปเมื่อความถี่ในการกำหนดค่าสูง
การอ้างอิงแบบอ่อน (Weak Reference) และการอ้างอิงแบบแข็ง (Strong Reference) — ชนิดของการอ้างอิงกำหนดว่า GC/ARC สามารถปล่อยอ็อบเจกต์ได้หรือไม่ Strong Reference — อ็อบเจกต์จะไม่ถูกรวบรวมตราบใดที่การอ้างอิงนี้ยังคงอยู่ Weak Reference — GC/ARC สามารถรวบรวมอ็อบเจกต์ได้; การอ้างอิงแบบอ่อนจะกลายเป็น nil (ใน Swift/Java WeakReference) Unowned Reference (Swift) — ไม่กลายเป็น nil เมื่อปล่อย; การเข้าถึงหลังจากอ็อบเจกต์ตายทำให้เกิดการขัดข้อง บน Android ใช้ java.lang.ref.WeakReference สำหรับการอ้างอิงแบบอ่อน
ตัวอย่างการตรวจจับการรั่วไหลบน Android ด้วย LeakCanary:
// Утечка: анонимный класс держит ссылку на Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
// Используем `this@MainActivity`, сохраняя ссылку на Activity
Log.d("TAG", "Handler received message")
}
}
handler.sendEmptyMessageDelayed(0, 60000)
}
}
// Исправление: статический Handler + WeakReference
class SafeHandler(activity: MainActivity) : Handler() {
private val weakActivity =
WeakReference(activity)
override fun handleMessage(msg: Message) {
weakActivity.get() ?: return
Log.d("TAG", "Handler received message")
}
}
การสร้างโปรไฟล์คือกระบวนการวัดประสิทธิภาพของแอป: CPU, หน่วยความจำ, เครือข่าย, การใช้พลังงาน หากไม่มีการสร้างโปรไฟล์ การปรับแต่งแบบไม่รู้ทิศทางก็ไร้ประโยชน์ — คุณจะไม่รู้ว่าส่วนใดของโค้ดที่ช้าจริงๆ
| เครื่องมือ | แพลตฟอร์ม | วัด | เมื่อใดควรใช้ |
|---|---|---|---|
| Instruments (Time Profiler) | iOS | CPU, การเรียกฟังก์ชัน, เวลาดำเนินการ | การปรับแต่งอัลกอริทึม, การค้นหาคอขวด |
| Instruments (Allocations) | iOS | หน่วยความจำ, จำนวนอ็อบเจกต์, retain counts | การค้นหาการรั่วไหลและการใช้หน่วยความจำมากเกินไป |
| Instruments (Leaks) | iOS | Retain cycles, หน่วยความจำรั่วไหล | การตรวจสอบเป็นประจำก่อนเผยแพร่ |
| Android Profiler (CPU) | Android | การใช้ CPU, กิจกรรมเธรด, traces | การค้นหาการบล็อกเธรดหลัก |
| Android Profiler (Memory) | Android | Heap dump, การติดตามการจัดสรร | การค้นหาการรั่วไหล, การวิเคราะห์อ็อบเจกต์ |
| Android Profiler (Network) | Android | ปริมาณการรับส่ง, ความเร็ว, ช่วงเวลาคำขอ | การปรับแต่งการเรียกเครือข่าย |
| LeakCanary | Android | การตรวจจับหน่วยความจำรั่วไหลอัตโนมัติ | ในทุกขั้นตอนการพัฒนา |
| StrictMode | Android | ดิสก์/เครือข่ายบนเธรดหลัก, การรั่วไหล | บิลด์ดีบัก |
| Traceview / Systrace | Android | การติดตามเมธอด, เหตุการณ์ระบบ | การวิเคราะห์ความหน่วงเชิงลึก |
Instruments (Xcode) — เครื่องมือที่ทรงพลังที่สุดสำหรับ iOS Time Profiler แสดงฟังก์ชันที่ใช้ CPU มากที่สุด Allocations ติดตามการสร้างและการปล่อยอ็อบเจกต์ Leaks ค้นหา retain cycles โดยอัตโนมัติ ขั้นตอนการสร้างโปรไฟล์: (1) เปิด Instruments; (2) เลือกแม่แบบ (Time Profiler สำหรับ CPU); (3) เรียกใช้สถานการณ์ที่มีปัญหา; (4) วิเคราะห์สแต็กการเรียก — คอลัมน์ที่กว้างที่สุดคือฟังก์ชันที่ "ร้อนแรง" ที่สุด
Android Profiler ถูกรวมอยู่ใน Android Studio (View → Tool Windows → Profiler) CPU Profiler แสดงโหลดของแต่ละเธรด Memory Profiler — heap dump และการติดตามการจัดสรร Network Profiler — คำขอ HTTP ทั้งหมดพร้อมช่วงเวลา Energy Profiler — การใช้พลังงาน: WakeLock, Location, Network สำหรับการติดตามโดยละเอียด ใช้ Systrace (Android 10+) หรือ Perfetto — การติดตามระบบด้วยความแม่นยำระดับไมโครวินาที
การเริ่มต้นแอปเป็นหนึ่งในตัวบ่งชี้ประสิทธิภาพหลัก แบ่งออกเป็นสามประเภท: Cold Start — แอปเริ่มต้นจากศูนย์: สร้างกระบวนการ, Application.onCreate (Android) / AppDelegate.applicationDidFinishLaunching (iOS), โหลดคลาส, เริ่มต้นไลบรารี Warm Start — กระบวนการมีอยู่ แต่ Activity/ViewController ถูกทำลาย (เช่น เมื่อหมุนหน้าจอหรือกลับมาจากหน่วยความจำ) Hot Start — Activity/ViewController อยู่ในหน่วยความจำ แอปถูกแสดงเพียงอย่างเดียว (สลับจากแอปอื่น)
Cold Start เป็นตัวชี้วัดที่สำคัญที่สุด บน Android รวมถึง: (1) launch Activity — โหลด XML, เริ่มต้น View; (2) เฟรมแรก — เวลาจนถึงการเรนเดอร์ครั้งแรก Google แนะนำ: launch Activity < 200 ms, เฟรมแรก < 500 ms, TTI < 5 วินาที การปรับแต่ง Cold Start: ลด Application.onCreate (coroutine สำหรับการเริ่มต้นแบบขี้เกียจ), ใช้ SplashScreen API (Android 12+), เลื่อนการเริ่มต้นไลบรารี (WorkManager, DI), ลบ ContentProviders ที่ไม่จำเป็น
บน iOS, Cold Start รวมถึง: โหลดไบนารี Mach-O, dyld (ลิงเกอร์ไดนามิก), เริ่มต้น Objective-C runtime, application delegate, ตัวควบคุมแรก Chrome Custom Tabs (Android) และ Universal Links (iOS) — เทคโนโลยีสำหรับเปิดเนื้อหาภายนอกในแอปอย่างรวดเร็วโดยไม่ต้อง Cold Start เต็มรูปแบบ แนะนำให้ทดสอบ Cold Start บนอุปกรณ์จริงระดับกลาง
ขนาดแอปเป็นปัจจัยประสิทธิภาพสำหรับการติดตั้งและการอัปเดต มันส่งผลต่ออัตราการแปลง: ทุก 10 MB ลดอัตราการแปลง 1% Google Play แนะนำ ขนาด APK ต่ำกว่า 150 MB; App Store — ต่ำกว่า 200 MB (เครือข่ายเซลลูลาร์ — 100 MB) วิธีการปรับแต่งหลัก: การบีบอัดภาพ (WebP แทน PNG ประหยัด 25-35%), การทำให้เป็นเวกเตอร์ (VectorDrawable บน Android, SF Symbols บน iOS), การลบโค้ดที่ไม่ได้ใช้ (R8/ProGuard), การลบทรัพยากรที่ไม่ได้ใช้ (lint → unused resources)
App Bundle (Android) — รูปแบบการเผยแพร่ที่ Google Play สร้าง APK ที่ปรับแต่งสำหรับแต่ละอุปกรณ์ App Bundle ลดขนาดดาวน์โหลดลง 20-40% Dynamic Delivery — โมดูลที่ดาวน์โหลดตามความต้องการ (on-demand feature modules) บน iOS สิ่งที่เทียบเท่าคือ On-Demand Resources (ODR): ทรัพยากรที่ดาวน์โหลดหลังจากเริ่มต้นครั้งแรก (ระดับเกม, วิดีโอ)
Lazy Loading — เทคนิคที่โมดูลและไลบรารีไม่ถูกโหลดเมื่อเริ่มต้น แต่ถูกโหลดตามความจำเป็น Split APK (Android) และ App Slicing (iOS) — การแบ่งแอปเป็นช่องสถาปัตยกรรม: arm64-v8a, x86_64 การปรับแต่งขนาดแอป — กระบวนการต่อเนื่อง: วิเคราะห์องค์ประกอบ APK (Analyze APK ใน Android Studio), ลบไอคอนที่ซ้ำกัน, ใช้ SVG แทนความหนาแน่น PNG หลายแบบ ที่ IT Sectr เรารวมการตรวจสอบขนาดบิลด์ใน CI/CD สำหรับทุก MR
คำถามที่พบบ่อย
ANR (Application Not Responding) — กล่องโต้ตอบที่ปรากฏบน Android หากเธรดหลักถูกบล็อกนานกว่า 5 วินาที เพื่อหลีกเลี่ยง ANR ให้ย้ายการทำงานหนักทั้งหมด (เครือข่าย, ฐานข้อมูล, การประมวลผลไฟล์) ไปยังเธรดพื้นหลัง สิ่งที่เทียบเท่าบน iOS คือ frozen UI เมื่อแอปหยุดตอบสนองต่อการสัมผัส
หน่วยความจำรั่วไหล — เมื่ออ็อบเจกต์ไม่สามารถถูกปล่อยได้เนื่องจากการอ้างอิงยังคงมีอยู่ Retain Cycle — สถานการณ์ใน iOS/Objective-C ที่อ็อบเจกต์สองตัวอ้างอิงซึ่งกันและกัน (A → B → A) และ ARC ไม่สามารถปล่อยอ็อบเจกต์ใดเลย วิธีแก้: การอ้างอิงแบบ weak/unowned และการล้าง callback อย่างทันท่วงที
สำหรับ iOS: Instruments (Time Profiler, Allocations, Leaks) สำหรับ Android: Android Profiler (CPU, Memory, Network), LeakCanary (หน่วยความจำรั่วไหล), StrictMode (การละเมิดเธรด) แนะนำให้รวมการสร้างโปรไฟล์ ในระหว่างการพัฒนาและการรวมระบบ
Cold Start — แอปเริ่มต้นจากศูนย์: สร้างกระบวนการ, โหลดคลาส, เรียกใช้ Application.onCreate Warm Start — กระบวนการมีอยู่ แต่ Activity/ViewController ถูกสร้างใหม่ Hot Start — Activity/ViewController อยู่ในหน่วยความจำแล้ว แค่แสดง Cold Start ช้าที่สุด (1-5 วินาที) และสำคัญต่อประสบการณ์ผู้ใช้
วิธีการหลัก: ลบทรัพยากรและโค้ดที่ไม่ได้ใช้ (ใช้ R8/ProGuard), ทำให้ภาพเป็นเวกเตอร์ (VectorDrawable, SF Symbols), บีบอัด PNG/WebP (Android), ใช้ App Bundle แทน APK, ลบไลบรารีที่ไม่จำเป็น, ใช้ Lazy Loading สำหรับโมดูล การปรับแต่งขนาด สามารถลด APK ได้ 40-60%
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ