Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 14 — Performance Testing Mobile — FPS, Frozen Frames

Đã đọc 0/14 mục · còn khoảng 13 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Bạn đã bao giờ mở một app, kéo danh sách sản phẩm và cảm thấy nó "giật giật", "khựng khựng" một cái rồi mới trôi tiếp chưa? Đó không phải cảm giác chủ quan. Đó là một hiện tượng đo được, có tên gọi, có ngưỡng số cụ thể, và trong ngành gọi là jank. Một app có thể chạy đúng logic 100%, không crash, không bug chức năng — nhưng nếu nó giật, người dùng vẫn đánh giá là "app rẻ tiền", "làm ẩu", và rời đi.

Với QA mobile, đây là một mảng dễ bị bỏ quên nhất. Chúng ta quen kiểm tra "nút bấm có hoạt động không", "màn hình có hiển thị đúng dữ liệu không". Nhưng chất lượng cảm nhận (perceived quality) của một app di động phần lớn nằm ở độ mượt của khung hình — thứ mà mắt thường cảm nhận được nhưng lại rất khó mô tả trong một bug report nếu bạn không có công cụ và con số.

Bài này sẽ dạy bạn cách "nhìn thấy" cái giật đó bằng số liệu: FPS, thời gian dựng frame, jank, và frozen frame. Đây là những chỉ số mà một QA mobile chuyên nghiệp phải biết đọc, biết đo, và biết viết vào bug report một cách thuyết phục để dev không thể cãi "máy em chạy mượt mà".

Lưu ý phạm vi: bài này tập trung riêng vào hiệu năng render — FPS và jank/frozen frame. Các khía cạnh hiệu năng khác như rò rỉ bộ nhớ, hao pin, tải ảnh/media, stress/soak test sẽ được bàn ở các bài riêng (Bài 15, 16, 45, 52). Ở đây, chúng ta chỉ quan tâm một câu hỏi: app có dựng đủ khung hình đúng hạn để mắt người thấy mượt hay không?

Khái niệm cốt lõi

FPS và ngân sách 16.67ms mỗi frame

FPS (frames per second) là số khung hình mà màn hình hiển thị trong một giây. Đa số màn hình điện thoại phổ thông làm mới ở 60Hz, nghĩa là màn hình vẽ lại 60 lần mỗi giây. Để app trông mượt, hệ thống phải chuẩn bị xong một khung hình mới cho mỗi lần làm mới đó.

Chia ra: 1 giây = 1000ms, chia cho 60 = 16.67ms. Đây là "ngân sách" (frame budget) cho mỗi khung hình. Trong 16.67ms đó, app phải làm xong toàn bộ: xử lý input, tính toán layout, đo đạc view, vẽ (draw), và đẩy sang GPU để hiển thị. Nếu làm xong trong ngân sách → frame kịp giờ → mượt. Nếu vượt quá → frame trễ → jank.

Ngày nay nhiều máy cao cấp có màn hình 90Hz hoặc 120Hz. Với 120Hz, ngân sách chỉ còn 8.33ms mỗi frame — khắt khe gấp đôi. Điều này quan trọng cho QA: một app "mượt" trên iPhone 8 (60Hz) có thể lộ jank rõ rệt trên Samsung Galaxy 120Hz, vì cùng một khối lượng công việc render nhưng deadline chặt hơn.

Jank là gì

Jank là hiện tượng một hoặc nhiều frame bị dựng trễ hơn ngân sách cho phép. Khi frame N mất 30ms để dựng thay vì 16.67ms, màn hình không có gì mới để hiển thị ở nhịp làm mới đó → nó lặp lại frame cũ (gọi là dropped frame / frame bị bỏ). Mắt người nhạy cảm với sự đứt gãy chuyển động này, nên ta thấy "giật một cái".

Điểm quan trọng: không phải cứ FPS trung bình cao là mượt. Một app có thể đạt trung bình 58 FPS nhưng vẫn cảm giác giật, vì cái giật nằm ở những frame cá biệt bị trễ nặng (spike). Vì vậy dân chuyên nghiệp không chỉ nhìn FPS trung bình mà nhìn phân phối thời gian dựng frame, đặc biệt là các percentile cao (p90, p99) và số lượng frame vượt ngân sách.

Frozen frame — cấp độ nghiêm trọng hơn jank

Google định nghĩa frozen frame là frame mất hơn 700ms để dựng. Đây không còn là "giật nhẹ" nữa — 700ms là gần một giây màn hình đứng hình hoàn toàn, người dùng nghĩ app bị treo và có thể bấm loạn hoặc thoát app. Frozen frame thường xảy ra khi có công việc nặng chạy nhầm trên main thread / UI thread: đọc file lớn, parse JSON khổng lồ, query database đồng bộ, hoặc xử lý ảnh ngay lúc đang cuộn.

Google Play Console dùng hai chỉ số chính để chấm điểm hiệu năng render của app Android:

  • Slow rendering: tỉ lệ phần trăm frame chậm hơn 16ms (một số ngưỡng dùng frame chậm hơn thời gian khung). Play Console cảnh báo nếu quá 25% frame bị chậm.
  • Frozen frames: tỉ lệ frame chậm hơn 700ms. Cảnh báo nếu quá 0.1% phiên có frozen frame.
Đây là hai con số bạn nên thuộc lòng vì chúng là ngưỡng chuẩn để phân loại mức độ nghiêm trọng của bug hiệu năng.

Vì sao frame bị trễ — nguồn gốc kỹ thuật

Hiểu nguyên nhân giúp bạn viết bug report có giá trị cho dev thay vì chỉ nói "app giật". Các thủ phạm phổ biến:

  • Công việc nặng trên main thread: main thread (Android) hay main queue (iOS) là nơi duy nhất được phép chạm vào UI. Nếu bạn chạy một vòng lặp nặng, giải mã ảnh, hoặc chờ đọc/ghi database đồng bộ ở đây, toàn bộ việc dựng frame bị chặn.
  • Overdraw: cùng một pixel bị vẽ đè nhiều lần (nhiều lớp background chồng lên nhau). GPU tốn công vô ích.
  • Layout phức tạp / lồng sâu: cây view lồng nhiều tầng khiến bước measure và layout tốn thời gian mỗi lần cuộn.
  • RecyclerView/List tái sử dụng kém: bind dữ liệu nặng trong lúc cuộn, tạo view mới liên tục thay vì tái dùng.
  • Garbage collection dồn dập: tạo quá nhiều object tạm trong vòng cuộn khiến bộ dọn rác nhảy vào, gây khựng.

Tình huống thực tế

Tình huống 1 — Sàn TMĐT: màn hình danh sách sản phẩm giật khi cuộn

Một team QA tại một sàn thương mại điện tử lớn ở Việt Nam (giả định tên ChợViệt) nhận phản hồi từ user rằng "trang danh sách sản phẩm cuộn bị khựng, nhất là máy Android tầm trung". Ban đầu dev bác bỏ vì test trên Pixel 7 flagship thấy mượt.

QA bật GPU rendering profiler trên một máy Samsung A14 (phổ thông, rất phổ biến ở Việt Nam) và bật chế độ Profile GPU Rendering → On screen as bars. Kết quả: khi cuộn nhanh, các thanh bar vọt qua vạch xanh (ngưỡng 16ms) liên tục, nhiều frame chạm ngưỡng 33–40ms. Đo bằng adb shell dumpsys gfxinfo <package>, họ thấy p90 = 28ms, số frame trên 16ms chiếm 34% — vượt xa ngưỡng 25% của Play Console.

Truy tiếp bằng systrace, nguyên nhân lộ ra: mỗi item sản phẩm giải mã ảnh thumbnail ngay trên main thread trong onBindViewHolder, và một số item còn tính lại giá khuyến mãi bằng một hàm nặng. Sau khi dev đẩy giải mã ảnh sang thư viện tải ảnh bất đồng bộ và cache giá tính sẵn, tỉ lệ slow frame giảm còn 6%, p90 còn 14ms.

Bài học: FPS trung bình (58) đã che giấu vấn đề. Chỉ khi nhìn phân phối và test trên máy phổ thông đúng với tập người dùng thật (không phải flagship của dev), bug mới hiện ra. QA phải luôn hỏi: "user của mình dùng máy gì?" — ở Việt Nam phần lớn là máy Android tầm trung.

Tình huống 2 — App ngân hàng số: frozen frame khi mở màn hình lịch sử giao dịch

Một ngân hàng số (giả định VBank) có màn hình "Lịch sử giao dịch". Khi user có nhiều giao dịch, mở màn hình này bị đứng hình khoảng 1 giây rồi mới hiện. QA đo trên Android bằng gfxinfo và thấy có frame ~900ms — vượt ngưỡng frozen frame 700ms.

Phân tích cho thấy app đọc toàn bộ lịch sử từ SQLite đồng bộ trên main thread, rồi format lại từng dòng ngày tháng bằng SimpleDateFormat tạo mới trong vòng lặp. Với danh sách 500 giao dịch, việc này chặn main thread gần một giây → frozen frame. Trên iOS phiên bản tương tự, họ dùng Instruments → Time Profiler và thấy main thread bị chiếm bởi truy vấn Core Data đồng bộ.

Sau khi dev chuyển truy vấn sang thread nền, dùng phân trang (pagination) và tái dùng formatter, màn hình mở gần như tức thì.

Bài học: frozen frame gần như luôn đến từ I/O hoặc tính toán nặng trên UI thread. Là QA, khi thấy màn hình "đứng một nhịp rồi mới hiện", đừng chỉ ghi "load chậm" — hãy đo con số ms cụ thể và chỉ ra đây là frozen frame vượt 700ms. Con số biến bug từ "cảm giác" thành "sự thật không cãi được".

Tình huống 3 — App gọi xe: jank khi mở/đóng bottom sheet bản đồ

Một app gọi xe (giả định GoDi) có animation kéo bottom sheet thông tin chuyến đi lên xuống trên nền bản đồ. Trên máy 120Hz, animation này giật rõ. QA nghi ngờ vì trên 60Hz thì tạm ổn.

Họ nhận ra ngân sách frame ở 120Hz chỉ còn 8.33ms, và animation đang làm bố cục lại (relayout) toàn bộ view mỗi frame thay vì chỉ dịch chuyển (translate). Trên iOS, họ dùng Instruments → Core Animation FPS gauge thấy tụt xuống ~70 FPS trong lúc kéo (đáng lẽ phải 120). Sau khi dev chuyển sang animate bằng transform/translation thay vì đổi layout, FPS ổn định lại sát trần thiết bị.

Bài học: luôn test hiệu năng trên máy có màn hình tần số cao nếu tập user có dùng. Ngân sách khắt khe hơn sẽ lộ ra những vấn đề mà máy 60Hz giấu đi.

Hướng dẫn từng bước

Đây là quy trình đo hiệu năng render mà bạn có thể áp dụng ngay.

Bước 1 — Xác định kịch bản và thiết bị đại diện. Chọn đúng luồng dễ giật: cuộn danh sách dài, chuyển màn hình, chạy animation, mở màn hình nặng dữ liệu. Chọn thiết bị theo tập user thật — ưu tiên máy tầm trung, không chỉ flagship. Test cả một máy 60Hz và một máy 90/120Hz nếu có.

Bước 2 — Đo trực quan nhanh trên Android bằng GPU bars. Vào Developer Options → Profile GPU Rendering → On screen as bars. Cuộn màn hình cần test. Vạch ngang màu là ngưỡng 16ms; bất kỳ thanh nào vượt vạch là frame trễ. Đây là cách "nhìn thấy" jank trong 30 giây, tốt để khoanh vùng nhanh.

Bước 3 — Lấy số liệu định lượng trên Android bằng gfxinfo. Reset bộ đếm rồi chạy kịch bản:

  • adb shell dumpsys gfxinfo <package> reset (nếu cần), thực hiện thao tác cuộn, sau đó
  • adb shell dumpsys gfxinfo <package>
Đọc phần thống kê: tổng số frame, số Janky frames và tỉ lệ %, các percentile 50th/90th/95th/99th (đơn vị ms), và số frame trong nhóm >700ms (frozen). Ghi lại các con số này làm bằng chứng.

Bước 4 — Đo trên iOS bằng Instruments. Mở Xcode → Instruments, chọn template Core Animation (hoặc Time Profiler để tìm hàm nào chặn main thread). Chạy app trên máy thật (không phải Simulator, vì hiệu năng render trên Simulator không phản ánh thiết bị). Xem FPS gauge trong lúc thao tác; nếu FPS tụt, dùng Time Profiler xem stack nào chiếm main thread.

Bước 5 — Xác định ngưỡng đạt/không đạt. Áp chuẩn: slow rendering ≤ 25% frame chậm, frozen frames ≤ 0.1% phiên. Nội bộ nhiều team đặt gắt hơn: p90 ≤ 16ms, không có frozen frame trong luồng chính. So sánh số đo với ngưỡng để kết luận pass/fail.

Bước 6 — Viết bug report có số. Ghi: thiết bị + OS + tần số màn hình, kịch bản cụ thể, con số đo (p90, % janky, frame lớn nhất bằng ms), video quay chậm nếu có, và (nếu đã đào sâu) trace chỉ ra hàm/thao tác nghi vấn. Đính kèm output gfxinfo hoặc ảnh Instruments.

Bước 7 — Đưa vào regression và CI. Hiệu năng dễ tái xuất hiện sau mỗi thay đổi. Ghi lại baseline số liệu để lần sau so sánh, và cân nhắc đo tự động trong pipeline (chủ đề CI/CD sẽ bàn ở Bài 48).

Lỗi thường gặp & mẹo

Chỉ nhìn FPS trung bình. Đây là sai lầm phổ biến nhất. Trung bình 58 FPS vẫn có thể giật vì vài frame spike. Luôn nhìn p90/p99 và số frame vượt ngân sách, không chỉ trung bình.

Test trên máy flagship của dev. Máy mạnh giấu bug. Hãy test trên thiết bị đại diện đúng tập user — với thị trường Việt Nam thường là Android tầm trung 2–3 năm tuổi. Cân nhắc dùng device farm để phủ nhiều máy (Bài 49).

Đo hiệu năng trên Simulator/Emulator. Simulator dùng CPU/GPU của máy tính, cho FPS "ảo" không thật. Đo render luôn phải trên máy thật. (Xem Bài 5 về Simulator vs Real Device.)

Đo khi máy đang nóng hoặc pin yếu. Máy nóng sẽ throttle (giảm xung) làm kết quả tệ hơn thực tế; ngược lại nếu đo ngay sau khi cắm sạc mát thì lại đẹp giả tạo. Hãy đo trong điều kiện ổn định, lặp lại 3–5 lần lấy trung vị.

Bật GPU profiler rồi quên tắt. Chính profiler cũng tốn tài nguyên; đừng để nó bật khi đo các chỉ số khác.

Mẹo — quay video slow-motion. Điện thoại có sẵn quay 240fps. Quay chậm màn hình đang giật giúp bạn và dev thấy chính xác frame nào rớt, rất thuyết phục khi đính kèm bug.

Mẹo — phân biệt jank do render và jank do dữ liệu. Nếu giật chỉ khi có mạng chậm hoặc dữ liệu lớn, có thể gốc rễ là I/O/network chứ không phải render thuần. Đo cả hai điều kiện để khoanh đúng nguyên nhân.

Mẹo — luôn kèm ngưỡng vào kết luận. Đừng viết "hơi giật". Viết "34% frame > 16ms, vượt ngưỡng Play Console 25%, p90 = 28ms". Con số + ngưỡng = bug không thể chối.

Bài tập thực hành

  • Đo baseline một app thật. Chọn một app bạn hay dùng (app tin tức, TMĐT, mạng xã hội). Trên một máy Android, bật Profile GPU Rendering → bars, cuộn feed 15 giây và quan sát số thanh vượt vạch. Sau đó chạy adb shell dumpsys gfxinfo <package> và ghi lại: % janky frames, p50, p90, p99, số frame >700ms.
  • So sánh hai thiết bị. Lặp lại bài 1 trên một máy tầm trung và một máy mạnh hơn (hoặc một máy 60Hz và một máy 120Hz). Ghi lại chênh lệch p90 và % janky. Viết 3–4 câu giải thích vì sao kết quả khác nhau.
  • Tự tạo một frozen frame. Nếu bạn có môi trường dev (hoặc app demo), thêm một thao tác đọc file lớn/sleep đồng bộ trên main thread khi mở một màn hình, đo lại và xác nhận xuất hiện frame > 700ms. Mục tiêu: hiểu bằng tay vì sao frozen frame sinh ra.
  • Viết một bug report hoàn chỉnh. Dựa trên số liệu ở bài 1 hoặc 2, viết một bug report chuẩn: tiêu đề, thiết bị/OS/tần số màn hình, các bước tái hiện, số liệu đo kèm ngưỡng vi phạm, mức độ ưu tiên đề xuất, và (nếu có) giả thuyết nguyên nhân. Đây là kỹ năng thực chiến quan trọng nhất của bài này.

Tóm tắt

  • Màn hình mượt nghĩa là app dựng xong mỗi frame trong ngân sách 16.67ms (60Hz) hoặc 8.33ms (120Hz).
  • Jank là frame vượt ngân sách gây giật; frozen frame là frame > 700ms, mức nghiêm trọng khiến app như bị treo.
  • Đừng chỉ nhìn FPS trung bình — hãy nhìn phân phối và percentile (p90/p99) cùng % frame vượt ngưỡng. Chuẩn tham chiếu: slow rendering ≤ 25%, frozen ≤ 0.1%.
  • Công cụ: Android dùng Profile GPU Rendering bars và adb shell dumpsys gfxinfo; iOS dùng Instruments (Core Animation, Time Profiler) — luôn đo trên máy thật, ưu tiên thiết bị đại diện tập user (Android tầm trung cho thị trường Việt Nam).
  • Nguyên nhân gốc thường là công việc nặng trên main thread, overdraw, layout phức tạp, bind list kém. Bug report tốt luôn kèm con số + ngưỡng vi phạm, không phải cảm giác.
Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi