Jank เป็นคำที่ใช้เรียกอาการกระตุกหรือ “สะดุด” ที่เห็นได้ชัดในแอนิเมชันอินเทอร์เฟซซึ่งเกิดจากการข้ามเฟรมแต่ละเฟรม ในแอปมือถือ Jank เกิดขึ้นเมื่อเวลาเรนเดอร์เฟรมเกินงบประมาณที่จัดสรรตามอัตรารีเฟรชของหน้าจอ ตามข้อมูลจาก Android Developers, 2025 Jank เป็นสาเหตุหลักของความรู้สึก “ช้า” — แอปอาจทำงานได้สมบูรณ์แบบ แต่ผู้ใช้รับรู้ว่าแอปช้าเนื่องจาก FPS ไม่เสถียร
ประเด็นสำคัญ
Jank เป็นศัพท์ในคอมพิวเตอร์กราฟิกที่หมายถึงข้อบกพร่องทางการมองเห็นซึ่งแอนิเมชันเคลื่อนที่แบบกระตุกแทนที่จะเลื่อนอย่างราบรื่น ในการพัฒนามือถือ Jank วัดจากจำนวนเฟรมที่ถูกข้าม (skipped frames) ต่อหน่วยเวลา หากระบบไม่สามารถเตรียมเฟรมทันเวลา VSync หน้าจอจะแสดงเฟรมก่อนหน้าซ้ำ — เกิดการหยั่งชั่วคราว 16.6 มิลลิวินาทีที่ 60 Hz เฟรมที่ถูกข้ามหนึ่งเฟรมอาจไม่มีใครสังเกต แต่การข้ามเฟรม 3–5 เฟรมติดต่อกันสร้างความรู้สึก “กระตุก” นาน 50–80 มิลลิวินาที ซึ่งผู้ใช้สังเกตได้อย่างชัดเจน
Jank มีความสำคัญเป็นพิเศษสำหรับแอนิเมชันที่ต้องทำงานด้วยความเร็วคงที่: การเลื่อนฟีด, แอนิเมชันเปิดเมนู, เอฟเฟกต์พาราแลกซ์, การเปลี่ยนระหว่างหน้าจอ จากการศึกษา UX ของ Google (2024) แอปที่มีอัตรา Jank เกิน 3% ของเซสชันการเลื่อนได้รับรีวิวหนึ่งดาวมากกว่าแอปที่มีอัตราต่ำกว่า 0.5% ถึง 22% เครื่องมือ Android Vitals จะติดตาม Jank โดยอัตโนมัติและจำแนกตามความรุนแรง: ปานกลาง, รุนแรง และวิกฤต
สาเหตุของ Jank แบ่งออกเป็นหลายประเภท ประเภทแรก — Layout Jank: เกิดจากการเรียก requestLayout() บ่อยครั้งเนื่องจากการเปลี่ยนแปลงขนาด View, แอนิเมชัน LayoutTransition หรือการโหลดเนื้อหาแบบไดนามิก การเรียก requestLayout แต่ละครั้งจะเริ่ม Measure + Layout สำหรับทรีย่อย View ทั้งหมด ซึ่งอาจใช้เวลา 5–30 มิลลิวินาที ประเภทที่สอง — Draw Jank: เกี่ยวข้องกับ overdraw และการใช้ drawable ที่หนัก ประเภทที่สาม — Thread Jank: การบล็อกเธรดหลักเนื่องจากการดำเนินการแบบซิงโครนัส — การโหลดไฟล์, การทำงานกับฐานข้อมูลบนเธรดหลัก, การถอดรหัส Bitmap
ประเภทที่สี่ — GC Jank: การเก็บขยะ (Garbage Collection) ใน ART/Dalvik หรือ Swift ARC เมื่อวัตถุจำนวนมากสะสมในฮีป GC จะเริ่มการหยุด Stop-The-World นาน 5–15 มิลลิวินาที บน Android การหยุด GC มักเกิดขึ้นบ่อยที่สุดระหว่างการจัดสรรบ่อยครั้งในลูป: การสร้างวัตถุใน onDraw(), การจัดสรรในอะแดปเตอร์, นิพจน์ lambda ที่ไม่ได้ใช้ ประเภทที่ห้า — IPC Jank: การสื่อสารระหว่างกระบวนการ (ContentProvider, Binder) บนเธรดหลัก ประเภทที่หก — Rendering Jank: การเรนเดอร์ GPU ช้าเนื่องจากเชเดอร์ที่ไม่เหมาะสมหรือพื้นผิวขนาดใหญ่
| ประเภท Jank | สาเหตุ | ระยะเวลาทั่วไป | เครื่องมือตรวจจับ |
|---|---|---|---|
| Layout | requestLayout, relayout | 5–30 ms | Perfetto, Systrace |
| Draw | Overdraw, drawable หนัก | 3–20 ms | GPU Profiling |
| Thread | การบล็อกเธรดหลัก | 10–200 ms | Android Studio Profiler |
| GC | Garbage Collection | 5–15 ms | Memory Profiler |
| Rendering | โหลด GPU | 10–50 ms | GPU Tracer, Xcode GPU |
ใน Android การวินิจฉัย Jank เริ่มต้นด้วยการติดตามระบบ Perfetto Perfetto บันทึกกิจกรรมของเธรดทั้งหมด, CPU, GPU และตัวจัดตารางเวลา ตัวบ่งชี้ที่ชัดเจนของ Jank คือบรรทัด Choreographer.doFrame และ Choreographer.doCallbacks: หากช่วงเวลาระหว่างการเรียก doFrame สองครั้งติดต่อกันเกิน 16.6 มิลลิวินาที แสดงว่าเฟรมถูกข้าม Perfetto แสดงสาเหตุที่แน่ชัด — ว่าการเรียกระบบ, ล็อก หรือ GC ใดทำให้เกิดความล่าช้า ใน Android Studio Profiler ฟังก์ชันที่คล้ายกันสามารถใช้ได้ผ่าน CPU Profiler
สำหรับการตรวจจับ Jank อัตโนมัติในโปรดักชัน ใช้ FrameMetricsAggregator — API ที่รวบรวมสถิติสำหรับแต่ละเฟรมและรวมตามเซสชัน ใน Android 12+ มี PerformanceHintManager — API สำหรับบอกใบ้ระบบเกี่ยวกับอัตราเฟรมเป้าหมาย หากแอประบุว่าทำงานในสถานการณ์ 120 FPS ระบบสามารถเพิ่มความถี่ CPU/GPU เพื่อป้องกัน Jank สำหรับการบันทึกเฟรมที่ถูกข้ามทั้งหมดแบบง่าย เพียงสมัครสมาชิก Choreographer.FrameCallback ก็เพียงพอ
โค้ด Kotlin สมัครสมาชิก Choreographer.FrameCallback และบันทึกแต่ละเฟรมที่ถูกข้ามพร้อมระยะเวลาที่ล่าช้า เรียกกลับถูกเรียกในแต่ละ VSync
class JankDetector {
private val frameBudget = 16_666_666L
private var previousFrameTime = 0L
private val callback =
Choreographer.FrameCallback { currentTime ->
if (previousFrameTime != 0L) {
val frameDuration =
currentTime - previousFrameTime
val skippedFrames =
(frameDuration / frameBudget) - 1
if (skippedFrames > 0) {
Log.w("Jank",
"Skipped $skippedFrames frames")
}
}
previousFrameTime = currentTime
Choreographer.getInstance()
.postFrameCallback(this)
}
fun start() {
Choreographer.getInstance()
.postFrameCallback(callback)
}
}
ใน iOS การวินิจฉัย Jank ทำผ่าน Instruments ด้วยเทมเพลต Core Animation Instruments แสดง FPS แบบเรียลไทม์, จำนวนการเรนเดอร์นอกหน้าจอ และการทดสอบ hit ตัวบ่งชี้หลักของ Jank ใน iOS: แถบสีแดงบนไทม์ไลน์ Core Animation (เกินงบประมาณเฟรม), ค่า Renderer สูง (บ่งชี้การเรนเดอร์นอกหน้าจอ) และ FPS ต่ำ สำหรับการตรวจสอบในโปรดักชัน MetricKit รวบรวมรายงานด้วยเมตริก MXAnimatoryMetric ซึ่งรวมถึง FPS เฉลี่ย, เวลาเฟรม P50 และ P95
การวินิจฉัย Jank ดั้งเดิมใน iOS รวมถึง CADisplayLink พร้อมการตรวจสอบ timestamp และ targetTimestamp หาก timestamp ปัจจุบันล่าช้ากว่า targetTimestamp อย่างมีนัยสำคัญ แสดงว่ามีการข้ามหนึ่งเฟรมหรือมากกว่า Apple ยังแนะนำให้ใช้ os_signpost สำหรับการโปรไฟล์แบบกำหนดเอง: วาง signpost-interval ที่จุดเริ่มต้นและจุดสิ้นสุดของการเรนเดอร์เฟรม และดูใน Instruments ว่าระยะใดเกิน 16.6 มิลลิวินาที ใน SwiftUI สำหรับการวินิจฉัย Jank ใช้ UIView.invalidateIntrinsicContentSize — การเรียกเมธอดนี้บ่อยครั้งบ่งชี้ Layout ที่ไม่เสถียร
โค้ด Swift ตรวจจับเฟรมที่ถูกข้ามผ่าน CADisplayLink หากความแตกต่างระหว่าง timestamp และ targetTimestamp เกิน 16.6 มิลลิวินาที จะบันทึก Jank
class JankMonitor {
private var displayLink: CADisplayLink?
private var totalJank = 0
func start() {
displayLink = CADisplayLink(
target: self,
selector: #selector(detectJank)
)
displayLink?.add(to: .current,
forMode: .common)
}
@objc
private func detectJank() {
guard let link = displayLink else { return }
let delay = link.targetTimestamp
- link.timestamp
if delay > 0.0167 {
totalJank += 1
}
}
}
สำหรับการโปรไฟล์ Jank ใช้ทั้งเครื่องมือในตัวของระบบปฏิบัติการและ SDK ของบริษัทอื่น บน Android เครื่องมือหลักคือ Perfetto (แทนที่ Systrace) Perfetto สามารถบันทึกการติดตามได้นานถึง 30 วินาทีและวิเคราะห์ผ่านอินเทอร์เฟซเว็บ ui.perfetto.dev มันแสดงไทม์ไลน์ที่แม่นยำพร้อมกิจกรรมของ Choreographer, เธรดการเรนเดอร์ (RenderThread) และ GPU สำหรับการวิเคราะห์ปัญหา GPU โดยละเอียด ใช้ AGI (Android GPU Inspector) ซึ่งแสดงไม่เพียงเวลาเฟรมแต่ยังแสดงโหลดของบล็อก GPU เฉพาะ — เชเดอร์, ราสเทอไรเซอร์, หน่วยพื้นผิว
บน iOS สิ่งที่เทียบเท่าคือ Instruments พร้อมเทมเพลต Core Animation, Metal System Trace และ GPU Driver Core Animation แสดง FPS และเวลาเฟรม Metal System Trace แสดงงาน GPU ลงไปจนถึงแต่ละ draw call สำหรับการโปรไฟล์บนอุปกรณ์จริงภายใต้โหลด ใช้ Firebase Performance (รวบรวมเมตริก Screen Rendering) และ Sentry (จับภาพ stack trace เมื่อเกิด Jank) API ใหม่ Android 15 Performance Hint ช่วยให้นักพัฒนาสามารถบอกระบบว่าเฟรมใดสำคัญและรับคำเตือนจากระบบเมื่อ Jank ใกล้เข้ามา
โค้ด Kotlin ใช้ FrameMetricsAggregator เพื่อรวบรวมสถิติเฟรมระหว่างเซสชัน หลังจากหยุดตัวรวบรวม จำนวนเฟรมที่ถูกข้ามจะแสดงผล
class JankAggregator(private val activity: Activity) {
private val aggregator = FrameMetricsAggregator()
fun startCollection() {
aggregator.add(activity.window)
}
fun stopAndReport() {
aggregator.remove()
val result = aggregator.getMetrics()
val totalFrames = result
?.get(FrameMetrics.TOTAL_DURATION)
?.size ?: 0
val jankFrames = result
?.get(FrameMetrics.TOTAL_DURATION)
?.count { it > 16_666_666L} ?: 0
Log.d("JankReport",
"Jank ratio: \${jankFrames * 100 / totalFrames}%")
}
}
การกำจัด Jank ต้องใช้เทคนิคร่วมกันตามประเภทของมัน สำหรับ Layout Jank: แทนที่ลำดับชั้นที่ลึกด้วย ConstraintLayout/Compose/SwiftUI, ใช้แท็ก merge, หลีกเลี่ยง requestLayout ในแอนิเมชัน สำหรับ Draw Jank: ใช้ Debug GPU Overdraw เพื่อค้นหา overdraw 4x+, แทนที่ drawable หนักด้วยกราฟิกเวกเตอร์ (VectorDrawable/PDF), ใช้ hardware layers ด้วยความระมัดระวัง — มันเร่งการเรนเดอร์แต่กินหน่วยความจำ GPU มากขึ้น สำหรับ Thread Jank: ย้ายการดำเนินการ I/O ทั้งหมด, งานฐานข้อมูล และการถอดรหัส Bitmap ไปยังเธรดพื้นหลัง, ใช้ Kotlin Coroutines พร้อม Dispatcher ที่ถูกต้อง หรือ RxJava พร้อม Schedulers.io()
สำหรับ GC Jank: ลดการจัดสรรใน onDraw() และ getView(), ใช้ ObjectPool, แทนที่ for-each ด้วย for แบบมีดัชนี, ใช้ immutable data class ใน Kotlin ด้วย copy() อย่างระมัดระวัง — copy สร้างวัตถุใหม่ สำหรับ IPC Jank: เริ่มต้น ContentProvider แบบขี้เกียจผ่าน App Startup, ย้ายการเรียก Binder ไปยังเธรดพื้นหลัง สำหรับ Rendering Jank: ลดขนาดพื้นผิวให้เหลือความละเอียดหน้าจอสูงสุด, ใช้การบีบอัด ASTC หรือ ETC2, หลีกเลี่ยงการคอมไพล์เชเดอร์ที่มากเกินไป (คอมไพล์เชเดอร์ล่วงหน้า) วิธีแก้ปัญหาที่ครอบคลุมคือการเรียกใช้การโปรไฟล์ Perfetto/Instruments เป็นประจำใน CI และติดตามการถดถอยของ Jank
โค้ด Kotlin แสดงการโหลดข้อมูลแบบอะซิงโครนัสบนหน้าจอหลังจาก reportFullyDrawn เพื่อให้งานหนักไม่บล็อกเฟรมแรก เรียกกลับถูกเรียกหลังจากผู้ใช้เห็นอินเทอร์เฟซ
class JankSafeLoader {
suspend fun loadAfterFirstFrame(
activity: Activity
) {
// รับประกันว่าเฟรมแรกได้รับการเรนเดอร์แล้ว
if (Build.VERSION.SDK_INT >= 29) {
activity.reportFullyDrawn()
}
// การโหลดหนัก — หลังจากเฟรมแรก
withContext(Dispatchers.IO) {
val data = fetchHeavyData()
withContext(Dispatchers.Main) {
updateUI(data)
}
}
}
}
คำถามที่พบบ่อย
Jank คือเฟรมเรนเดอร์ที่ถูกข้ามซึ่งปรากฏเป็นอาการกระตุกหรือสะดุดที่เห็นได้ชัดในแอนิเมชัน เกิดขึ้นเมื่อเวลาเตรียมเฟรมเกินงบประมาณเวลา (16.6 มิลลิวินาทีสำหรับ 60 FPS)
Layout Jank (requestLayout บ่อย), Draw Jank (overdraw), Thread Jank (การบล็อกเธรดหลัก), GC Jank (การเก็บขยะ), IPC Jank (การเรียก Binder) และ Rendering Jank (เชเดอร์หนัก)
ใช้ Perfetto สำหรับการติดตามระบบ, GPU Profiling สำหรับการวิเคราะห์เฟสเฟรม และ FrameMetricsAggregator สำหรับการตรวจสอบในโปรดักชัน ใน Android Studio — CPU Profiler พร้อม Deep Java Trace
ผ่าน Instruments ด้วยเทมเพลต Core Animation หรือ Metal System Trace สำหรับโปรดักชัน — MetricKit ด้วย MXAnimatoryMetric โดยการเขียนโปรแกรม — CADisplayLink ตรวจสอบความแตกต่างระหว่าง timestamp และ targetTimestamp
ตามข้อมูลของ Google อัตรา Jank ที่เกิน 3% ของเซสชันการเลื่อน (3 ในการเลื่อน 100 ครั้งมีการกระตุก) นำไปสู่การเพิ่มขึ้นของรีวิวเชิงลบ 22% อัตราเป้าหมายคือต่ำกว่า 0.5% ของเซสชันการเลื่อน
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม