jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

How Browsers Work · Part 19 — Image & Media Decoding Pipeline

Ảnh đi từ byte tới pixel: fetch, decode đồng bộ/bất đồng bộ, raster/composite; cùng decoding="async", Image.decode(), createImageBitmap, OffscreenCanvas và video frames.

Ảnh thường chiếm nhiều byte và bộ nhớ của một trang. Nhiều người nghĩ “tải xong là xong”, nhưng giữa byte nén và pixel còn có decode, resize/color conversion, raster và composite. Browser hiện đại đẩy nhiều việc sang helper/raster threads, song synchronous decode hoặc lượng decode quá lớn vẫn có thể làm rớt frame.


1. Mental model: fetch → decode → raster/composite

Byte nén (JPEG/PNG/WebP/AVIF)
   │  ① fetch (mạng/cache — Phần 9)

Decoded image data (RGBA/YUV/tile tuỳ codec và engine)
   │  ② decode + scale/color conversion (CPU/hardware, có cache)

Raster tile / software bitmap / GPU texture
   │  ③ composite bằng đường software hoặc GPU

Màn hình

Điểm bất ngờ: một estimate phổ biến cho bitmap RGBA là width × height × 4 byte, nên file JPEG vài trăm KB có thể cần hàng chục MB khi decode. Engine có thể giữ định dạng/tile khác và evict cache, nên đây không phải công thức đo RAM chính xác. Decode vẫn tốn CPU/băng thông bộ nhớ; synchronous decode trên đường paint có thể chặn luồng chính.


2. Decode đồng bộ vs bất đồng bộ

Thuộc tính decodinghint về cách phối hợp ảnh với nội dung khác, không phải lệnh chọn thread:

<!-- async: ưu tiên không trì hoãn việc present nội dung khác -->
<img src="hero.avif" decoding="async" alt="...">

<!-- sync: ưu tiên present ảnh cùng nội dung khác (hiếm khi cần) -->
<img src="icon.png" decoding="sync" alt="...">

Để chủ động decode rồi mới chèn (tránh nhấp nháy/jank khi swap ảnh lớn):

const img = new Image();
img.src = 'big.avif';
try {
  await img.decode();    // resolve khi dữ liệu sẵn sàng để paint
  container.append(img); // browser cố giữ decoded data ít nhất cho frame kế
} catch {
  container.textContent = 'Không thể giải mã ảnh';
}

img.decode() trả promise và có thể reject (EncodingError). Nó giúp tránh chèn ảnh rồi mới gặp synchronous decode khi paint; không hứa “zero jank” nếu layout, upload/raster hay memory pressure vẫn đắt.


3. createImageBitmap — bitmap bất đồng bộ, dùng được trong worker

createImageBitmap() trả promise cho một ImageBitmap, hỗ trợ crop/resize và là transferable. API có trong worker; gọi pipeline từ worker giữ fetch/orchestration JS khỏi main thread, nhưng spec không bảo đảm decode vật lý luôn chạy trên một OS thread cụ thể.

// Trong worker: fetch + tạo bitmap mà không chạy JS orchestration trên main
const res = await fetch(url);
const blob = await res.blob();
const bitmap = await createImageBitmap(blob, {
  resizeWidth: 400, resizeQuality: 'high', // resize ngay khi decode
});
postMessage({ bitmap }, [bitmap]); // chuyển ownership về main

Khác HTMLImageElement, ImageBitmap không gắn DOM và có vòng đời tài nguyên rõ hơn. Sau khi vẽ xong ở bên nhận, gọi bitmap.close() để nhả backing resource sớm thay vì chờ GC.


4. OffscreenCanvas — vẽ ngoài luồng chính

OffscreenCanvas cung cấp drawing context không phụ thuộc DOM và có thể transfer control từ <canvas> sang dedicated worker. JS dựng frame chạy ở worker; browser vẫn phải composite kết quả lên trang.

// main.js — gọi trước khi lấy context trên HTMLCanvasElement
const canvas = document.querySelector('canvas');
if (!canvas) throw new Error('Missing <canvas>');
const worker = new Worker('./worker.js', { type: 'module' });
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);

// worker.js
onmessage = ({ data }) => {
  const ctx = data.canvas.getContext('2d');
  function draw() {
    ctx.clearRect(0, 0, data.canvas.width, data.canvas.height);
    // ...draw next frame...
    requestAnimationFrame(draw);
  }
  requestAnimationFrame(draw);
};

Dedicated-worker requestAnimationFrame giúp đồng bộ drawing loop với rendering của owner window. Dù vậy, worker không bảo đảm 60fps: chi phí vẽ, transfer, raster/GPU và composite vẫn phải nằm trong deadline.


5. Lazy loading, priority & responsive

Trình duyệt cũng quyết định khi nàocái nào tải, ảnh hưởng pipeline:

<img
  src="photo-800.avif"
  srcset="photo-400.avif 400w, photo-800.avif 800w, photo-1600.avif 1600w"
  sizes="(max-width: 600px) 100vw, 800px"
  loading="lazy"
  decoding="async"
  fetchpriority="low"
  width="800" height="600"
  alt="...">
  • loading="lazy": cho user agent hoãn fetch ảnh ngoài màn hình theo heuristic riêng; đừng dùng cho ảnh LCP.
  • srcset/sizes: giúp browser chọn candidate theo kích thước layout, DPR và điều kiện mạng → tránh decode ảnh lớn quá nhu cầu.
  • fetchpriority: hint nâng ảnh LCP (high) hoặc hạ ảnh phụ (low), không ghi đè mọi quyết định scheduler.
  • width/height: cho trình duyệt biết tỉ lệ → tránh layout shift (CLS).

Codec đổi trade-off byte mạng, chất lượng, decode và hardware support. Đừng kết luận AVIF/WebP/JPEG chỉ từ kích thước file; đo cả LCP, decode time và memory trên thiết bị mục tiêu.


6. Video — pipeline thời gian thực

Video không decode một lần mà là dòng khung liên tục. Browser ưu tiên hardware decoder khi codec/thiết bị/driver hỗ trợ và rơi về software khi cần:

Container (mp4/webm) ─► demux ─► luồng video nén (H.264/VP9/AV1)

   ▼ media pipeline → hardware decoder hoặc software decoder
Khung đã decode ─► đồng bộ với audio clock ─► compositor ─► màn hình

API nâng cao: WebCodecs (VideoDecoder/VideoEncoder) cho truy cập khung ở mức thấp; requestVideoFrameCallback lên lịch callback khi một video frame mới được đưa tới compositor (hữu ích cho overlay/analysis). <video> thường tận dụng media threads và đường compositing hiệu quả, nhưng hardware decode/layer riêng không được bảo đảm; JS callback nặng vẫn chặn main thread.


7. Tóm tắt

  • Ảnh đi qua fetch → decode → raster/composite; RGBA w × h × 4 là estimate hữu ích, không phải layout bộ nhớ bắt buộc.
  • decoding chỉ là hint; img.decode() cho phép chờ decoded data trước khi chèn và phải xử lý rejection.
  • createImageBitmap tạo bitmap bất đồng bộ, hỗ trợ worker/transfer; không có bảo đảm spec về OS thread thực thi decode.
  • OffscreenCanvas chuyển drawing JS sang worker, còn raster/composite và deadline vẫn thuộc pipeline browser.
  • loading, srcset/sizes, fetchpriority, width/height là input cho scheduler/layout; phần lớn là hint, không phải mệnh lệnh tuyệt đối.
  • Video dùng media pipeline với hardware/software fallback; WebCodecs và requestVideoFrameCallback cho quyền điều khiển thấp hơn nhưng đòi quản lý frame/resource cẩn thận.

Phần tiếp theo: Threading deep — Web Workers, SharedArrayBuffer + Atomics, OffscreenCanvas ở quy mô lớn, và cross-origin isolation (COOP/COEP) mở khoá bộ nhớ chia sẻ + timer độ phân giải cao.