Ba bước: preview ngay, nén trước khi gửi, giải phóng tài nguyên.
Preview dùng URL.createObjectURL(file) — nhanh hơn FileReader.readAsDataURL vì không phải mã hoá base64 toàn bộ file vào chuỗi. Bắt buộc revokeObjectURL khi bỏ ảnh hoặc unmount, nếu không blob sẽ nằm lại trong bộ nhớ.
Nén bằng cách vẽ ảnh xuống canvas ở kích thước tối đa rồi xuất lại qua canvas.toBlob(cb, 'image/webp', 0.8):
js
async function compress(file, maxSide = 1600, quality = 0.8) {
const bitmap = await createImageBitmap(file)
const scale = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height))
const canvas = document.createElement('canvas')
canvas.width = Math.round(bitmap.width * scale)
canvas.height = Math.round(bitmap.height * scale)
canvas.getContext('2d').drawImage(bitmap, 0, 0, canvas.width, canvas.height)
bitmap.close()
return new Promise((r) => canvas.toBlob(r, 'image/webp', quality))
}Lưu ý thực tế:
- Chặn trước bằng
accept="image/*"và kiểm trafile.size/file.type, hiển thị lỗi rõ ràng thay vì để server trả 413. - Canvas xoá metadata EXIF, bao gồm cả thông tin hướng ảnh — ảnh chụp bằng điện thoại có thể bị xoay.
createImageBitmap(file, { imageOrientation: 'from-image' })xử lý được việc này. - Nén trên main thread làm giật giao diện với ảnh lớn; đẩy vào Web Worker nếu người dùng chọn nhiều ảnh một lúc.
- Client nén chỉ để tiết kiệm băng thông — server vẫn phải kiểm tra lại kiểu và kích thước file.