FPS (Frames Per Second) là một chỉ số cho biết hệ thống đồ họa hiển thị bao nhiêu khung hình riêng lẻ trong một giây. Trong phát triển di động, FPS là chỉ số hiệu suất giao diện người dùng tiêu chuẩn: FPS càng cao, hoạt ảnh càng mượt và giao diện càng phản hồi nhanh. Theo Google Android Performance, 2025, giá trị mục tiêu FPS cho các ứng dụng di động là 60 khung hình mỗi giây — ngưỡng mà mắt người cảm nhận chuyển động là liên tục và mượt mà.
Những điểm chính
FPS (Frames Per Second) là đơn vị đo tốc độ khung hình được sử dụng trong đồ họa máy tính, video và giao diện di động. Mỗi khung hình là một hình ảnh tĩnh được hiển thị trên màn hình trong một khoảng thời gian ngắn. Với sự thay đổi khung hình nhanh chóng, não bộ cảm nhận chúng như chuyển động liên tục — hiệu ứng này được gọi là sự bền bỉ của thị giác. Đối với ứng dụng di động, FPS là một chỉ số quan trọng vì bất kỳ khung hình nào bị bỏ lỡ đều biến hoạt ảnh mượt mà thành hiện tượng giật đáng chú ý. Ứng dụng phải hiển thị mỗi khung hình trong ngân sách thời gian nghiêm ngặt: 16,6 ms cho 60 FPS, 11,1 ms cho 90 FPS, 8,3 ms cho 120 FPS.
FPS không chỉ được đo cho giao diện người dùng mà còn cho trò chơi, video và camera. Trong trò chơi, FPS phụ thuộc vào độ phức tạp của cảnh, chất lượng kết cấu và sức mạnh GPU. Trong video, FPS là cố định (24, 30, 60 fps) và được xác định bởi nội dung. Trong ứng dụng di động, FPS phụ thuộc vào hiệu quả của mã giao diện người dùng: độ phức tạp của bố cục, số lượng View, tần suất vẽ lại và hoạt động của GC (Garbage Collection). Theo Apple WWDC 2022, FPS trung bình trong ứng dụng có thể giảm 10–15% do cập nhật bộ sưu tập không hiệu quả (reloadData thay vì insert/delete/dequeueReusableCell). Đo FPS theo thời gian thực là thông lệ tiêu chuẩn cho các kỹ sư QA và nhà phát triển làm việc về hiệu suất.
Việc tính FPS trong ứng dụng di động dựa trên việc đo thời gian giữa các khung hình liên tiếp. Công thức đơn giản nhất: FPS = 1000 / deltaTimeMs, trong đó deltaTimeMs là khoảng thời gian giữa khi hoàn thành khung hình trước và hoàn thành khung hình hiện tại. Nếu khung hình hiện tại được hiển thị trong 20 ms, FPS = 1000 / 20 = 50. Tuy nhiên, trong thực tế, FPS hiếm khi ổn định ngay cả trong một giây: một hồ sơ điển hình bao gồm các khung hình 12–16 ms xen kẽ với các khung hình bị bỏ qua (jank) hoặc chậm (40–60 ms). Do đó, FPS được đo dưới dạng trung bình động trong 1–5 giây hoặc dưới dạng phần trăm phân phối thời gian khung hình.
Trên Android, FPS được tính qua Choreographer, nhận lệnh gọi lại từ VSync (xung đồng bộ màn hình). Mỗi lệnh gọi lại tương ứng với một khung hình. Nếu lệnh gọi lại không đến — khung hình bị bỏ qua. Choreographer cho phép đo số lượng khung hình chính xác mỗi giây và số lượng khung hình bị bỏ qua. Trên iOS, CADisplayLink hoạt động tương tự — nó được gọi mỗi khi màn hình sẵn sàng hiển thị một khung hình mới. Thuộc tính timestamp chứa thời gian chính xác của khung hình cuối cùng và targetTimestamp — thời gian dự kiến của khung hình tiếp theo. Sự khác biệt giữa chúng là ngân sách thời gian cho khung hình hiện tại.
Mã Swift minh họa cách theo dõi FPS đơn giản qua CADisplayLink. Bộ đếm frameCount tăng lên sau mỗi lần gọi và mỗi giây một lần, FPS thực tế được tính toán.
class FpsCounter {
private var displayLink: CADisplayLink?
private var frameCount = 0
private var lastTime = TimeInterval(0)
func start() {
displayLink = CADisplayLink(
target: self,
selector: #selector(countFrame)
)
displayLink?.add(to: .current,
forMode: .common)
}
@objc
private func countFrame() {
frameCount += 1
let now = Date().timeIntervalSince1970
if now - lastTime >= 1.0 {
print("FPS: \(frameCount)")
frameCount = 0
lastTime = now
}
}
}
Tiêu chuẩn 60 FPS (hoặc 60 Hz) đã được thiết lập trong ngành vì nhiều lý do. Đầu tiên là sinh lý: mắt người không phân biệt được các khung hình riêng lẻ ở tần số trên 50–60 Hz, cảm nhận chúng như chuyển động mượt mà. Ngưỡng này được gọi là Critical Flicker Fusion (CFF). Thứ hai là lịch sử: các ống tia âm cực (CRT) đầu tiên hoạt động ở 60 Hz tại Mỹ (NTSC) và 50 Hz tại Châu Âu (PAL). Màn hình LCD hiện đại kế thừa tần số này. Thứ ba là kỹ thuật: đối với hoạt ảnh giao diện người dùng, 60 FPS cung cấp độ trễ phản hồi chạm dưới mili giây, rất quan trọng cho nhập văn bản, cuộn và kéo.
Đối với nhà phát triển di động, 60 FPS không chỉ là khuyến nghị mà còn là ngân sách nghiêm ngặt 16,6 ms cho mỗi khung hình. Ngân sách này được phân chia giữa tất cả các giai đoạn hiển thị: Đầu vào (1–2 ms), Hoạt ảnh (2–3 ms), Bố cục (3–5 ms), Vẽ (3–5 ms) và Trao đổi (1–2 ms). Nếu bất kỳ giai đoạn nào vượt quá ngân sách phụ của nó, khung hình có thể không nằm gọn trong 16,6 ms. Google Android Performance khuyến nghị duy trì trong khoảng 12–14 ms cho việc chuẩn bị khung hình, để lại 2–4 ms dự phòng cho các gián đoạn hệ thống (GC, luồng nền). Theo Firebase Performance, các ứng dụng có FPS trung bình dưới 52 và P99 FPS dưới 30 nhận được nhiều hơn 35% khiếu nại về hiệu suất trong các đánh giá trên Google Play.
FPS và thời gian khung hình là hai mặt của cùng một chỉ số và điều quan trọng là không nhầm lẫn chúng. FPS là tốc độ, thời gian khung hình là độ trễ. Ở 60 FPS, mỗi khung hình mất 16,6 ms. Ở 30 FPS — 33,3 ms. Nhưng FPS là một chỉ số phi tuyến tính: giảm từ 60 xuống 30 FPS có nghĩa là thời gian khung hình tăng gấp đôi, trong khi giảm từ 30 xuống 20 có nghĩa là tăng 1,5 lần. Do đó, các trình phân tích hiển thị thời gian khung hình thay vì FPS — điều này cho phép nhìn thấy các khung hình có vấn đề thay vì tần số trung bình. Ví dụ, trung bình 55 FPS có thể che giấu thực tế rằng 5% khung hình có thời gian khung hình 50–100 ms — những khung hình này gây ra Jank nhưng không ảnh hưởng đáng kể đến FPS trung bình.
Khi phân tích hiệu suất, nên xem biểu đồ thời gian khung hình thay vì FPS trung bình. Trong Android Studio Profiler và iOS Instruments, thời gian khung hình được hiển thị dưới dạng thang đo trong đó vùng màu xanh lá cây lên đến 16,6 ms (60 FPS), màu vàng từ 16,6 đến 33,3 ms (30–60 FPS) và màu đỏ trên 33,3 ms (dưới 30 FPS). Mỗi cột màu đỏ là độ trễ đáng chú ý đối với người dùng. Một quy tắc thực tế: P95 Frame Time (95% khung hình nằm trong X ms) là chỉ số đáng tin cậy hơn FPS trung bình. Nếu P95 Frame Time vượt quá 32 ms (30 FPS), ứng dụng được cảm nhận là chậm ngay cả với FPS trung bình là 50.
Một hàm Kotlin để chuyển đổi mảng thời gian khung hình thành FPS với phần trăm. Nó trả về không chỉ FPS trung bình mà còn P50, P90 và P99 để phân tích chi tiết.
data class FpsReport(
val average: Float,
val p50: Float,
val p90: Float,
val p99: Float
)
fun List<Long>.toFpsReport(): FpsReport {
val fpsValues = this.map { ms ->
if (ms > 0) 1000f / ms else 0f
}.sorted()
return FpsReport(
average = fpsValues.average().toFloat(),
p50 = fpsValues[fpsValues.size / 2],
p90 = fpsValues[(fpsValues.size * 90 / 100)],
p99 = fpsValues[(fpsValues.size * 99 / 100)]
)
}
Các thiết bị di động hiện đại với màn hình 90, 120 và 144 Hz đặt ra các yêu cầu mới đối với FPS. Nếu ứng dụng cung cấp 60 FPS trên màn hình 120 Hz, người dùng thấy hiện tượng giật vi mô vì mỗi chu kỳ làm mới màn hình thứ hai nhận cùng một khung hình. Để duy trì 120 FPS, ngân sách mỗi khung hình thu hẹp từ 16,6 xuống 8,3 ms — đòi hỏi mã hiển thị hiệu quả gấp đôi. Theo các nhà phát triển Android (Google I/O 2023), để đạt được 120 FPS ổn định cần: tránh cấp phát trong chu kỳ vẽ, giảm thiểu số lượng View trong hệ thống phân cấp (dưới 80), loại bỏ drawable nặng để chuyển sang VectorDrawable và sử dụng surfaceView cho đồ họa phức tạp.
Tình hình tương tự trên iOS: iPhone Pro với ProMotion (120 Hz) yêu cầu gấp đôi số khung hình, nhưng thời gian mỗi khung hình giảm một nửa. Apple lưu ý rằng không phải tất cả hoạt ảnh đều cần chạy ở 120 FPS — Core Animation tự động giảm tốc độ khung hình cho các phần tử tĩnh hoặc thay đổi chậm. Tuy nhiên, cuộn, hoạt ảnh cử chỉ và chuyển tiếp phải cung cấp 120 FPS để có cảm giác “mượt như lụa”. Các vấn đề chính khi chuyển từ 60 lên 120 FPS: tăng tiêu thụ điện năng (25–40% cho GPU), nóng thiết bị và điều tiết nhiệt — khi tốc độ khung hình giảm do quá nhiệt. Nên triển khai cơ chế dự phòng: nếu thời gian khung hình liên tục vượt quá 8,3 ms, hãy giảm tốc độ khung hình mục tiêu xuống 60 FPS theo chương trình thay vì chờ hệ thống điều tiết.
Mã Java cho Android xác định xem thiết bị có thể hỗ trợ 120 FPS hay không và chuyển đổi chế độ hiển thị. Display.getMode được sử dụng để xác định tần số quét được hỗ trợ.
class FpsModeSwitcher {
static boolean canDo120Fps(Activity activity) {
Display display = activity.getWindowManager()
.getDefaultDisplay();
for (Display.Mode mode : display.getSupportedModes()) {
if (mode.getRefreshRate() >= 120f) {
return true;
}
}
return false;
}
}
Tối ưu hóa FPS đòi hỏi một cách tiếp cận có hệ thống, bắt đầu bằng việc phân tích hồ sơ và kết thúc bằng việc tái cấu trúc các khu vực có vấn đề. Bước đầu tiên là đo FPS hiện tại bằng trình phân tích hồ sơ. Bước thứ hai là tìm các khung hình vượt quá ngân sách. Trên Android, điều này có thể thực hiện qua GPU Profiling hoặc Perfetto. Trên iOS — Instruments với mẫu Core Animation. Bước thứ ba là loại bỏ nguyên nhân: giảm overdraw, giảm độ sâu phân cấp View, thay thế giai đoạn bố cục bằng ConstraintLayout, thêm ViewHolder Recycling, chuyển các tính toán nặng sang luồng nền.
Các tối ưu hóa cụ thể cho FPS bao gồm: Frame Pacing — cơ chế phân phối thời gian đồng đều giữa các khung hình để tránh “các đợt bùng phát” khung hình nhanh và chậm. Trên Android, Choreographer.FrameCallback với khoảng thời gian cố định cho phép triển khai Frame Pacing. Trên iOS, CADisplayLink.preferredFrameRateRange thực hiện tương tự. Phương pháp thứ hai — Triple Buffering: hệ thống sử dụng ba bộ đệm thay vì hai, cho phép GPU bắt đầu hiển thị khung hình tiếp theo mà không cần đợi khung hình trước được giải phóng. Android tự động bật Triple Buffering khi cần, nhưng trên iOS, nhà phát triển có thể yêu cầu rõ ràng qua CAMetalLayer. Thứ ba — Texture Caching: lưu trữ bitmap trong bộ nhớ GPU để tránh tải lại chúng mỗi khung hình.
Ví dụ Kotlin minh họa việc triển khai Frame Pacing với khoảng thời gian cố định 16,6 ms. Tất cả các lệnh gọi lại đến với khoảng thời gian đồng đều, ngay cả khi hệ thống bị trễ.
class PacedFrameRenderer {
private val targetDelta = 16_666_666L // 16.6 ms (60 FPS)
private var lastFrameTime = 0L
private val frameCallback =
Choreographer.FrameCallback { frameTimeNanos ->
val delta = frameTimeNanos - lastFrameTime
if (delta >= targetDelta) {
onFrame(delta)
lastFrameTime = frameTimeNanos
}
Choreographer.getInstance()
.postFrameCallback(this)
}
private fun onFrame(delta: Long) {
// kết xuất khung hình
}
}
Câu hỏi thường gặp
60 FPS là mức thoải mái cho các ứng dụng di động. Sự khác biệt giữa 60 và 120 FPS chỉ đáng chú ý trên màn hình tần số quét cao khi có hoạt ảnh nhanh (cuộn, kéo). Dưới 30 FPS — khó chịu.
FPS = 1000 / FrameTime (ms). Nếu thời gian khung hình = 16,6 ms, FPS = 60. Nếu thời gian khung hình = 33,3 ms, FPS = 30. Nên theo dõi thời gian khung hình thay vì FPS, vì nó hiển thị các khung hình có vấn đề.
Khi cuộn, hệ thống gọi Layout và Draw cho mỗi mục mới trong danh sách. Nếu các View phức tạp, bố cục không được lưu trong bộ nhớ đệm hoặc sử dụng drawable nặng — thời gian khung hình tăng và FPS giảm. Giải pháp là tái chế ViewHolder và hệ thống phân cấp phẳng.
Sử dụng Instruments với mẫu Core Animation (hiển thị FPS theo thời gian thực). Để đo theo chương trình — CADisplayLink với đếm khung hình mỗi giây. Cho môi trường sản xuất — MetricKit với chỉ số MXAnimatoryMetric.
Triple Buffering sử dụng ba bộ đệm thay vì hai, cho phép GPU bắt đầu hiển thị khung hình tiếp theo trước khi VSync hiện tại hoàn tất. Điều này làm mượt tải đỉnh và cải thiện độ ổn định FPS, nhưng thêm một khung hình độ trễ.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm