Product Management
Đăng nhập
ESC

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

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

Bài 45 — Image & Media Loading Performance

Đã đọc 0/15 mục · còn khoảng 12 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

Hãy thử một phép thử nhỏ: mở app thương mại điện tử bất kỳ trên điện thoại của bạn, cuộn nhanh qua trang danh sách sản phẩm. Bạn có thấy những ô ảnh xám nhấp nháy, ảnh bung ra chậm rãi từng cái một, hay tệ hơn là một biểu tượng "ảnh vỡ" (broken image icon) xuất hiện? Với người dùng, đó là cảm giác "app này lag". Với QA, đó là cả một chuỗi vấn đề đo lường được: bộ nhớ phình to, băng thông lãng phí, định dạng ảnh không tương thích, và trải nghiệm giật cục.

Trong khóa học này bạn đã học về FPS và frozen frames (Bài 14), về memory leak (Bài 15), và về performance của việc load media nói chung. Bài 45 này zoom hẳn vào một nguồn gây chậm cực kỳ phổ biến nhưng hay bị bỏ qua: ảnh và media. Trên mobile, ảnh thường chiếm 60-80% dung lượng tải về của một màn hình. Một app tin tức, một app bán hàng, một app mạng xã hội — tất cả sống bằng ảnh. Nếu bạn kiểm thử được vòng đời của một tấm ảnh từ lúc request đến lúc hiển thị, bạn đã giải quyết được phần lớn cảm giác "nặng" của app.

Điều khiến chủ đề này thú vị với QA là: bug về ảnh hiếm khi làm app crash, nên chúng dễ lọt qua smoke test. Chúng biểu hiện dưới dạng "hơi chậm", "hơi tốn pin", "thỉnh thoảng vỡ ảnh" — những thứ mà chỉ khi bạn biết cách đo mới bắt được. Đó chính là giá trị bạn mang lại.

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

Vòng đời của một tấm ảnh trên mobile

Để test hiệu quả, bạn cần hình dung mỗi tấm ảnh đi qua các bước: request (app gọi URL ảnh) → download (tải bytes về) → decode (giải mã từ JPEG/PNG/WebP thành bitmap trong bộ nhớ) → transform (resize, crop, bo góc) → render (vẽ lên màn hình) → cache (lưu lại để lần sau khỏi tải). Mỗi bước là một điểm có thể hỏng hoặc chậm.

Điểm mấu chốt mà nhiều người bỏ qua là bước decode. Một file JPEG nén 200 KB nghe rất nhẹ, nhưng khi giải mã ra bitmap thô, nó chiếm bộ nhớ theo công thức: chiều rộng × chiều cao × 4 byte (4 byte cho mỗi pixel ở định dạng ARGB_8888). Một ảnh 4000×3000 pixel sẽ ngốn khoảng 4000 × 3000 × 4 = 48 MB RAM — dù file gốc chỉ 2 MB. Đây là gốc rễ của hầu hết vấn đề memory bloat liên quan tới ảnh.

Vấn đề #1: Load ảnh full-res vào ô thumbnail

Đây là lỗi kinh điển ghi trong dàn ý: giao diện chỉ hiển thị ảnh thumbnail 150×150 pixel, nhưng lập trình viên lại tải nguyên ảnh gốc 4000×3000 rồi để hệ thống co lại. Kết quả: tốn băng thông tải file lớn, tốn CPU decode, và tốn RAM khủng khiếp cho bitmap. Một list 20 sản phẩm với ảnh full-res có thể ngốn cả trăm MB RAM và khiến app bị hệ điều hành "kill" khi chuyển sang app khác rồi quay lại.

Giải pháp đúng phía dev là dùng downsampling (giải mã ảnh ở kích thước gần với kích thước hiển thị) hoặc yêu cầu server trả về đúng biến thể ảnh (thumbnail có sẵn). Vai trò QA là phát hiện ra rằng ảnh đang bị tải ở kích thước sai.

Vấn đề #2: Định dạng không được hỗ trợ

Dàn ý nêu ví dụ rất hay: HEIC vào webview → broken icon. HEIC/HEIF là định dạng ảnh mặc định của iPhone (chụp ảnh ra file .heic). Native iOS hiển thị được, nhưng nếu app đẩy ảnh HEIC đó vào một WebView (Bài 35) hoặc gửi lên server để hiển thị lại trên Android/web, nhiều môi trường không giải mã được HEIC → hiện icon ảnh vỡ. Tương tự là AVIF (mới, chưa phổ biến đủ), hay WebP trên các thiết bị Android quá cũ. QA cần có ma trận định dạng × nền tảng để kiểm.

Vấn đề #3: Không có placeholder và trạng thái loading

"No placeholder" trong dàn ý ám chỉ: khi ảnh đang tải, ô ảnh trống trơn hoặc nhảy layout (layout shift) khi ảnh về. Trải nghiệm tốt cần có placeholder (ảnh mờ blur-up, màu chủ đạo, hoặc skeleton xám) giữ đúng kích thước ô, cộng với xử lý 3 trạng thái: loading, success, error (khi tải thất bại thì hiện ảnh fallback chứ không để trống hay crash).

Cache: con dao hai lưỡi

Cache ảnh có 2 tầng: memory cache (nhanh, mất khi app đóng) và disk cache (chậm hơn nhưng bền). Cache giúp cuộn lại mượt và tiết kiệm data — cực kỳ quan trọng ở thị trường Việt Nam nơi nhiều người dùng gói 3G/4G tính theo dung lượng. Nhưng cache sai cũng gây bug: ảnh cũ không cập nhật khi server đổi ảnh (cache không invalidate theo URL/version), hoặc disk cache phình vô hạn chiếm hết bộ nhớ máy.

Tình huống thực tế

Tình huống 1 — App bán hàng "Chợ Xanh" bị OutOfMemory trên máy tầm trung

Một startup bán nông sản, tạm gọi "Chợ Xanh", ra mắt app Android. Trên iPhone 14 của team dev thì mượt, nhưng review trên Play Store toàn 1 sao: "app tự thoát", "cuộn một lúc là văng". QA vào cuộc, chạy trên một chiếc Samsung Galaxy A14 (RAM 4 GB, rất phổ biến ở phân khúc phổ thông Việt Nam).

Bật Android Studio Profiler theo dõi Memory, QA thấy: mỗi khi cuộn qua trang danh sách 30 sản phẩm, memory nhảy vọt lên 380 MB rồi crash với lỗi OutOfMemoryError: Failed to allocate a 48000012 byte allocation. Đào tiếp, hóa ra server trả về ảnh gốc do người bán tự chụp bằng điện thoại — kích thước 4000×3000, nhưng ô hiển thị chỉ 160×160 dp. App load full-res rồi mới co lại.

Bài học: con số 48 MB mỗi bitmap khớp đúng công thức w × h × 4. Sau khi dev thêm downsampling (giải mã ảnh xuống ~320px) và server sinh thumbnail, memory đỉnh giảm còn ~90 MB, hết crash. Điều QA rút ra: luôn test trên thiết bị RAM thấp, phổ thông của thị trường mục tiêu, đừng chỉ test trên máy flagship của team.

Tình huống 2 — Ảnh HEIC vỡ trong tính năng chat của một super-app Đông Nam Á

Một super-app kiểu Grab/Gojek có tính năng chat giữa tài xế và khách. Khách dùng iPhone gửi ảnh món ăn để tài xế xác nhận. Ảnh iPhone chụp ra định dạng HEIC. Phần hiển thị lịch sử chat lại là một WebView render HTML. Kết quả: tài xế (thường dùng máy Android phổ thông) thấy icon ảnh vỡ, không xem được ảnh khách gửi, dẫn tới giao nhầm.

QA phát hiện lỗi này nhờ ma trận định dạng: cố tình gửi ảnh từ iPhone (HEIC), từ Android (JPEG), ảnh chụp màn hình (PNG), ảnh động (GIF), và ảnh WebP. Chỉ HEIC hỏng, và chỉ hỏng ở phía WebView. Nguyên nhân: WebView engine trên các bản Android WebView cũ không giải mã HEIC.

Bài học: bug chỉ xuất hiện ở giao điểm định dạng (HEIC) × nền tảng render (WebView) × thiết bị (Android cũ). Giải pháp là convert ảnh sang JPEG/WebP ngay khi upload từ client. Với QA, đây là lời nhắc: định dạng ảnh phải được test xuyên nền tảng, đặc biệt khi ảnh do người dùng tạo ra và hiển thị lại ở nơi khác.

Tình huống 3 — Layout nhảy và lãng phí 3G của một app tin tức

Một tòa soạn báo điện tử làm app tin tức. Người dùng phản ánh: khi đọc bài, chữ đang đọc dở thì "nhảy tụt xuống" vì ảnh vừa load về đẩy layout. Ngoài ra, người dùng ở quê dùng 3G phàn nàn "vào app tốn cả trăm MB data mỗi ngày".

QA đo bằng Charles Proxy (bắt network): mỗi bài viết tải về 8-12 ảnh, mỗi ảnh 1-2 MB, tổng ~15 MB/bài. App không dùng lazy-loading — tải hết ảnh ngay cả những ảnh nằm dưới cuối bài mà người dùng chưa cuộn tới. Về layout shift, QA dùng chức năng quay video màn hình chậm và thấy rõ ô ảnh ban đầu cao 0 px rồi phình lên khi ảnh về.

Bài học rút ra kép: (1) cần reserve không gian cho ảnh bằng tỉ lệ khung hình biết trước để tránh layout shift; (2) cần lazy-load ảnh ngoài viewport và phục vụ ảnh nén WebP đúng kích thước để tiết kiệm data. Sau tối ưu, data mỗi bài giảm còn ~3 MB và không còn nhảy chữ. Với thị trường Việt Nam, tiết kiệm data là một tính năng chứ không phải "nice to have".

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

Đây là quy trình kiểm thử image & media performance bạn có thể áp dụng cho bất kỳ màn hình nhiều ảnh nào.

Bước 1 — Lập bản đồ ảnh của màn hình. Xác định màn hình nào nhiều ảnh nhất (list sản phẩm, feed, gallery, chat). Ghi lại: ô hiển thị bao nhiêu dp, ảnh đến từ đâu (server có sẵn thumbnail không, hay user upload), định dạng gì.

Bước 2 — Bắt network để xem kích thước và định dạng thật. Dùng Charles Proxy hoặc Proxyman (hoặc Chrome DevTools cho phần WebView). Với mỗi ảnh, kiểm tra: kích thước file (KB), độ phân giải (pixel), định dạng (JPEG/WebP/HEIC/PNG). So sánh độ phân giải ảnh với kích thước ô hiển thị. Nếu ảnh 4000px vào ô 160dp → cờ đỏ ngay.

Bước 3 — Đo memory khi cuộn. Mở Android Studio Profiler (Memory) hoặc Xcode Instruments (Allocations / VM Tracker). Cuộn lên xuống danh sách nhiều lần. Quan sát đỉnh memory và xem nó có được giải phóng khi rời màn hình không. Chú ý các bitmap lớn bất thường.

Bước 4 — Test ma trận định dạng. Chuẩn bị bộ ảnh mẫu: JPEG, PNG, WebP, HEIC, GIF động, ảnh rất lớn, ảnh 0 byte (hỏng), ảnh sai đuôi. Đẩy từng loại qua luồng hiển thị (native, WebView, sau khi upload lên server). Ghi lại loại nào vỡ, ở đâu.

Bước 5 — Kiểm tra placeholder và 3 trạng thái. Bật chế độ mạng chậm (Network Link Conditioner trên iOS, hoặc bóp băng thông qua proxy). Quan sát: khi loading có placeholder giữ đúng kích thước không? Khi tải lỗi (tắt mạng giữa chừng) có ảnh fallback hay để trống/crash? Có layout shift khi ảnh về không?

Bước 6 — Kiểm cache. Load màn hình lần 1 (nên thấy tải mạng), thoát vào lại lần 2 (nên đọc từ cache, không tải lại — xác nhận bằng proxy). Sau đó đổi ảnh trên server nhưng giữ nguyên URL, xem app có hiện ảnh cũ mãi không (bug invalidation). Kiểm tra disk cache có giới hạn dung lượng không.

Bước 7 — Ghi nhận với số liệu. Bug report tốt phải có con số: "memory đỉnh 380 MB", "ảnh 4000×3000 vào ô 160dp", "tải 15 MB/bài trên 3G". Số liệu là thứ khiến dev sửa nhanh.

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

Chỉ test trên máy flagship của team. Đây là cái bẫy lớn nhất. iPhone 15 Pro hay Galaxy S24 có RAM khủng, che giấu mọi vấn đề bitmap. Luôn có ít nhất một máy phổ thông RAM 3-4 GB trong bộ thiết bị test — đó mới là máy đa số người dùng Việt Nam dùng.

Nhầm dung lượng file với dung lượng bộ nhớ. File 200 KB không có nghĩa tốn 200 KB RAM. Luôn nhớ công thức bitmap w × h × 4. Khi thấy memory tăng bất thường, hãy nghĩ tới ảnh decode ra bitmap thô.

Bỏ qua trạng thái error. Nhiều QA chỉ test khi mạng tốt và ảnh về đủ. Hãy cố tình tắt mạng giữa chừng, đưa URL ảnh sai, ảnh 0 byte — app phải xử lý duyên dáng, không crash.

Quên test xoay màn hình và tái sử dụng ô list. Khi list tái sử dụng view (RecyclerView/UITableView), ảnh của item cũ có thể "nhấp nháy" trên item mới trước khi ảnh đúng về (image flicker do bất đồng bộ). Cuộn nhanh là cách bắt lỗi này.

Không kiểm cache invalidation. Ảnh avatar user đổi mà app vẫn hiện ảnh cũ là bug rất hay gặp và rất khó chịu. Test bằng cách đổi ảnh server, giữ URL.

Mẹo hiệu quả: Dùng ảnh có màu/nội dung dễ nhận biết cho từng kích thước để bằng mắt thường phát hiện ảnh sai kích thước. Mẹo thứ hai: quay video màn hình tốc độ cao rồi xem lại chậm để bắt layout shift và flicker mà mắt thường bỏ lỡ. Mẹo thứ ba: so sánh data usage trước/sau một thao tác bằng thống kê data của hệ điều hành để định lượng lãng phí băng thông.

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

  • Đo bitmap memory. Chọn một app có trang danh sách nhiều ảnh. Dùng proxy ghi lại độ phân giải thật của 5 ảnh thumbnail và kích thước ô hiển thị. Tính lượng RAM lý thuyết mỗi ảnh chiếm theo w × h × 4. Kết luận app có đang load ảnh quá khổ không.
  • Ma trận định dạng. Chuẩn bị 6 ảnh: JPEG, PNG, WebP, HEIC, GIF động, ảnh hỏng 0 byte. Đẩy chúng qua một luồng upload-rồi-hiển-thị (ví dụ ảnh đại diện hoặc chat). Lập bảng: định dạng × nơi hiển thị (native / WebView) × kết quả (ok / vỡ / crash). Viết bug report cho mọi ô "vỡ".
  • Test 3 trạng thái + layout shift. Với một màn hình có ảnh, bật mạng chậm và quay video màn hình. Xác minh: có placeholder giữ kích thước, có fallback khi lỗi, không nhảy layout. Đính kèm video minh họa trong bug report nếu phát hiện lỗi.
  • Kiểm cache. Load một màn hình ảnh lần 1, thoát vào lại lần 2, dùng proxy xác nhận lần 2 không tải lại. Bonus: nếu có quyền, đổi ảnh trên server giữ nguyên URL và kiểm tra invalidation.

Tóm tắt

Ảnh và media là nguồn gây "nặng" lớn nhất nhưng âm thầm nhất trên mobile. Chìa khóa của bài này là hình dung được vòng đời một tấm ảnh — request, download, decode, transform, render, cache — và biết mỗi bước hỏng ở đâu. Ba lỗi cốt lõi cần thuộc nằm lòng: (1) load ảnh full-res vào ô thumbnail gây memory bloat theo công thức w × h × 4; (2) định dạng không được hỗ trợ như HEIC trong WebView gây ảnh vỡ; (3) thiếu placeholder dẫn tới layout shift và trải nghiệm giật cục.

Quy trình QA gói gọn: lập bản đồ ảnh → bắt network để soi kích thước và định dạng → đo memory khi cuộn → chạy ma trận định dạng → kiểm placeholder và 3 trạng thái loading/success/error → kiểm cache và invalidation → báo cáo bằng số liệu cụ thể. Và đừng bao giờ quên thông điệp lớn nhất từ ba tình huống thực tế: hãy test trên thiết bị phổ thông của đúng thị trường mục tiêu, vì đó là nơi những vấn đề về ảnh lộ ra rõ nhất và cũng là nơi có nhiều người dùng thật nhất.

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