Đằng sau bức ảnh preview của Facebook

Lời mở đầu

Nếu bạn đi phỏng vấn, mua đồ hay hẹn hò đi chơi, thì những ấn tượng đầu tiên rất quan trọng.

Sử dụng Facebook cũng vậy, một trong những điều đầu tiên đập vào mắt khi bạn xem profile của người khác chính là ảnh cover.

Những hình ảnh đó có ý nghĩa đặc biệt quan trọng khi người dùng trải nghiệm sản phẩm.

Nhưng mỗi bức ảnh thường có dung lượng từ vài trăm KB đến vài MB nên có thể làm download và hiển thị chậm.

Điều này được minh chứng rõ rệt khi ta hết tiền 3G hoặc hôm nào cá mập cắn cáp - ngồi nhìn vào cái box màu xám trong khi chờ ảnh được download (khoc2).

Với Facebook, đối tượng hướng tới là tất cả mọi quốc gia trên thế giới, vậy nên trường hợp người dùng có kết nối chậm là khá phổ biến (giả dụ như Ấn Độ - nơi mà nhiều người dùng Facebook vẫn sử dụng mạng 2G).

Từ đó, đội ngũ kỹ sư của Facebook đã phải gánh 1 nhiệm vụ:

Phải thiết kế và xây dựng thế nào để người dùng có ấn tượng ban đầu tốt hơn ?

Ban đầu chúng tôi tập trung vào ảnh Cover - nằm ở trên profiles, ảnh phải có chất lượng đẹp, độ phân giải cao.

Ảnh Cover cũng là thứ dễ nhìn thấy nhất khi vào Facebook, nhưng đồng thời nó cũng là thứ load chậm nhất.

Có 2 nguyên nhân chính gây ra điều đó:

  • Thứ nhất, ảnh Cover thường có dung lượng tới 100KB, kể cả sau khi đã được nén ở định dạng JPEG. Đó là lượng data rất lớn, nếu như bạn nhận ra 2G chỉ cung cấp truyền tải khoảng 32KB/s

  • Nguyên nhân thứ hai liên quan đến API. Trước khi download ảnh, application sẽ request URL của ảnh từ GraphQL server. Sau đó, để thực sự lấy được ảnh, app sử dụng URL đó, tạo ra request thứ hai tới CDN server tải ảnh về. Và độ trễ của request thứ hai thường lâu hơn request đầu tiên.

Vậy nên, ta phải giải quyết cả 2 nguyên nhân trên.

200 bytes

Để giải quyết vấn đề này, chúng tôi tự hỏi: liệu có thể tạo ra ấn tượng thị giác của ảnh chỉ với 200 bytes.

Tại sao lại là 200 bytes?

Để loại bỏ request thứ 2, chúng ta cần đính kèm cả ảnh cover nháp trong request đầu tiên.

Điều này có nghĩa là ảnh cũng là 1 phần bên trong response của request đầu tiên.

Nhưng GraphQL không được thiết kế để xử lý Full-size image.

Nếu chúng ta có thể nén ảnh Cover xuống mức dưới 200 bytes, thì nó có thể được transfer thông qua GraphQL response.

Ưu điểm của giải pháp này là, nếu thành công, ta sẽ giải quyết gọn cả 2 nguyên nhân bên trên chỉ trong 1 request duy nhất.

Chúng tôi ước tính rằng, điều này sẽ cho phép app hiển thị ảnh preview trong lần render đầu tiên, giảm tổng thời gian chờ để hiển thị ProfilePage Header đáng kể.

Cuối cùng, chúng tôi vẫn muốn download và hiển thị ảnh full-size từ CDN, nhưng tiến trình này chạy ngầm nên vẫn đảm bảo rằng user sẽ có ấn tượng không tồi, thoải mái hơn.

Thử thách bây giờ là làm thế nào để nén 1 bức ảnh xuống dưới 200 bytes 😦

Ảnh preview

Chúng tôi cảm thấy, một tấm ảnh mờ sẽ đem lại cho bạn một cái gì đó thú vị và "ấn tượng" hơn so với hình ảnh ban đầu.

Sau khi quyết định trải nghiệm người dùng mong muốn, giờ ta cần tìm kỹ thuật nào để thực hiện điều đó.

  • Độ phân giải thấp nhất mà ta có thể sử dụng là bao nhiêu?
  • Làm thể nào để nén ảnh?
  • Làm thế nào để hiển thị ảnh đó dưới Client?

Hiển thị ảnh là phần đơn giản nhất: Hiệu ứng kính mờ có thể dễ dàng đạt được thông qua bộ lọc Gaussian blur. Điều thú vị của bộ lọc làm mờ này, bên cạnh việc nhìn trông hay hay, đó là Band-limits. Band-limiting có nghĩa là bỏ qua toàn bộ chi tiết và thay đổi nhanh thông tin ảnh nguồn ban đầu.

Ảnh hiển thị càng mờ, thì ảnh nguồn cũng càng nhỏ.

Độ phân giải và nén

Rõ ràng rằng, càng làm hình ảnh mờ, hạ độ phân giải xuống, thì dung lượng hình ảnh sẽ càng nhỏ đi.

Với bộ lọc Gaussian, ta cung cấp radius cho nó cao tới mức tối đa mà hình ảnh xuất ra có thể chập nhận được. Về size ảnh, chúng tôi thâý rằng độ phân giải này là khoảng 42 pixels.

Nếu thấp hơn tỷ lệ 42 x 42 pixel, ảnh xuất ra sẽ không còn độ chân thực nữa (yaoming).

Nhưng giả sử mỗi pixel đáng giá 3 bytes (đối với RBG), thì dung lượng vẫn là 42 x 42 x 3 = 5292 bytes - cao hơn nhiều so với mục tiêu 200 bytes.

Chúng tôi bắt đầu nghiên cứu kỹ thuật nén để tìm phương pháp tốt nhất cho nó xuống 200 bytes.

Nhưng không may, chỉ đơn thuần Entropy của encode ảnh như with, say, zlib đã có hệ số là 2. Vẫn quá lớn.

Vì vậy chúng tôi nghiên cứu cách mà JPEG encode - một image codec rất phổ biến. Đặc biệt là khi ảnh đã được làm mờ rất nhiều, cùng với band-limiting image data, JPEG sẽ nén ảnh này rất hiệu quả.

Nhưng không may, JPEG header thôi đã chiếm tới vài trăm bytes, vượt quá 200 bytes budget rồi.

Tuy nhiên, ngoại trừ header, dữ liệu mã hóa đã gần đạt tới mức 200 bytes rồi. Giờ ta cần xử lý cái JPEG header đó là được thôi.

JPEG

Bài toán giờ trở thành: Liệu có khả thi khi generate ra 1 header cố định, được lưu trữ trên client, nó sẽ ghép nối với phần nội dung ảnh lúc truyền về để hoàn tất ? Cuộc điều tra bắt đầu!

Bên trong JPEG header là vài bảng dữ liệu, trong đó có lưu trữ kích thước của ảnh.

Với 1 giá trị Q cố định, thì bảng định lượng (quantization table) trong header cũng cố định.

Tuy nhiên ảnh của ta không có size cố định, nó chỉ có giới hạn cao nhất là 42 x 42 thôi. Vậy nên ta phải mất 2 bytes để xác định header có size là A là phù hợp với ảnh có nội dung là A.

Nhìn tiếp vào các bảng còn lại trong JPEG header, bảng duy nhất có giá trị khác nhau giữa các ảnh là bảng Huffman.

Xử lý bảng này khá nhiều việc, vì việc thay đổi các tham số Q, image data, image size sẽ làm giá trị trong Huffman thay đổi, dẫn đến thay đổi số byte count. Nhưng cuối cùng, chúng tôi cũng đã một bảng Huffman được coi là tiêu chuẩn, phù hợp với hầu hết các hình ảnh.

Nói là hầu hết, tất nhiên sẽ có một số trường hợp đặc biệt mà nó không cover được. Nếu chúng tôi tìm ra được case đặc biệt, hoặc có table ngon hơn trong tương lai, chúng tôi có thể update version của images và bảng mới trên client.

Cuối cùng tính lại thì ta mất 1 byte cho version, 2 byte cho width x height, và xấp xỉ 200 byte cho nội dung ảnh.

Server sẽ gửi format này qua GraphQL response, và Client có thể dễ dàng ghép nối với JPEG header sẵn trên app.

Sau khi decode JPEG, client có thể chạy Gaussian blur và scale ảnh cho vừa màn hình.

Sau khi áp dụng các giải pháp trên, tốc độ đã có 1 bước tiến đáng kể.

Đối với những người dùng có kết nối chậm, điều này đã giúp tăng tốc load profile và page lên 30%.

Ngay cả với đường truyền nhanh cũng áp dụng kỹ thuật này, nó đảm bảo người dùng sẽ luôn luôn nhìn thấy ảnh cover preview ngay lập tức, làm cho trải nghiệm của họ tốt hơn.

Nguồn:

All Rights Reserved