本篇要解決的問題
很多網站功能會需要處理使用者上傳的圖片,比方讓使用者上傳會員照片。
但隨著手機相機愈做愈好,拍出來的照片隨便都是幾 MB,直接上傳的話,耗時也佔空間。
雖然網路上搜尋有許多圖片壓縮工具,但大多只能設定固定的壓縮的品質,無法保證壓縮後的檔案大小符合需求。
本筆記文將使用 Compressor.js 套件,實作一個圖片壓縮功能,符合以下需求:
- 自動嘗試不同的壓縮品質,直到檔案小於指定大小(ex: 600KB)為止。
- 將圖片轉換為 WebP 格式。
- 長、寬限制最大尺寸。
這樣就可以確保壓縮後的圖片既符合檔案大小限制,又保有良好的質感。
核心概念:遞迴壓縮
一般網路上的圖片壓縮只能一次性設定品質參數,本篇用了 comporess.js 後,採用 遞迴嘗試 的方式,執行方式如下:
- 從最高品質(
quality = 1.0)開始壓縮。 - 檢查壓縮後的檔案大小。
- 如果超過目標大小 600KB,則降低品質(減少 0.05)後重新壓縮。
- 重複步驟 2-3,直到檔案符合大小要求,或品質降到下限(0.40)。
這種方法能夠在保證檔案大小的前提下,盡可能保留圖片品質。
引入 Compressor.js
首先,我們需要引入 Compressor.js 這個圖片壓縮套件。
它可以在瀏覽器端直接處理圖片:無需後端支援:
<script src="https://cdn.jsdelivr.net/npm/compressorjs@1.2.1/dist/compressor.min.js"></script> 上面是直接引用 CDN 的方式,官方文件 也有其他引用方式,可以自己的需求引用。
建立 HTML
我們需要一個檔案上傳的 file input,以及一個用來預覽壓縮結果的 div:
<input type="file" id="upload" accept="image/*" /> <img id="preview" style="max-width:300px" /> 實作智慧壓縮函式
這是整個方案的核心函式,接受四個參數:
-
file:要壓縮的原始檔案。 -
targetSize:目標檔案大小,預設 600KB。 -
floor:品質下限,預設 0.40。 -
step:每次品質遞減幅度,預設 0.05。
function compressWithFloor( file, targetSize = 600 * 1024, floor = 0.4, step = 0.05 ) { return new Promise((resolve, reject) => { let q = 1.0; const attempt = () => { new Compressor(file, { quality: q, mimeType: "image/webp", // 強制轉換為 WebP 格式 maxWidth: 2560, // 最大寬度限制 maxHeight: 1440, // 最大高度限制 success(blob) { console.log( `q=${q.toFixed(2)} size=${Math.round(blob.size / 1024)}KB` ); if (blob.size > targetSize && q - step >= floor) { q = +(q - step).toFixed(2); attempt(); // 繼續壓縮 } else { resolve(blob); // 符合大小或到達品質下限 } }, error(err) { reject(err); }, }); }; attempt(); }); } 參數說明
1. 強制輸出 WebP 格式
mimeType: 'image/webp' 會將所有圖片(包括 PNG、JPEG)都轉換為 WebP。
WebP 是 Google 開發的現代圖片格式。在相同品質下。檔案大小通常比 JPEG 小 25-35%。
2. 尺寸限制
maxWidth: 2560 和 maxHeight: 1440 確保圖片不會超過 2K 解析度。
如果原圖尺寸較小,不會被放大;
如果較大,會等比例縮小。
這對於手機拍攝的 4K、8K 照片特別有用。
3. 遞迴壓縮邏輯
if (blob.size > targetSize && q - step >= floor) { q = +(q - step).toFixed(2); attempt(); // 繼續壓縮 } 這段程式碼檢查兩個條件:
- 檔案是否仍大於目標大小?
- 品質是否還有下降空間?
只要 1、2 都符合,就會降低品質並重新嘗試壓縮。
Blob 轉 Base64 輔助函式
為了在瀏覽器中預覽和下載圖片,或是調用 API 傳到後端,我們需要將壓縮後的 Blob 物件轉換為 Base64 格式:
function blobToBase64(blob) { return new Promise((resolve) => { const r = new FileReader(); r.onloadend = () => resolve(r.result); r.readAsDataURL(blob); }); } 整合上傳與下載功能
最後,我們監聽檔案上傳事件,執行壓縮流程,並自動觸發下載:
document.getElementById("upload").addEventListener("change", async (e) => { const file = e.target.files[0]; if (!file) return; try { const compressed = await compressWithFloor(file, 600 * 1024, 0.4, 0.05); console.log("final size:", Math.round(compressed.size / 1024), "KB"); const base64 = await blobToBase64(compressed); document.getElementById("preview").src = base64; // 取出原始檔名(去掉副檔名) const originalName = file.name.replace(/\.[^/.]+$/, ""); const newFileName = `${originalName}-compress.webp`; // 建立下載連結並自動觸發 const link = document.createElement("a"); link.href = base64; link.download = newFileName; link.click(); } catch (err) { console.error("壓縮失敗:", err); } }); 檔名處理邏輯
程式會自動處理檔名:
- 移除原始副檔名:
file.name.replace(/\.[^/.]+$/, "")。 - 加上
-compress.webp後綴。
例如:vacation.jpg → vacation-compress.webp。
壓縮流程示例
假設上傳一張 5MB 的照片,壓縮過程可能如下:
q=1.00 size=1200KB → 超過 600KB,繼續 q=0.95 size=950KB → 超過 600KB,繼續 q=0.90 size=750KB → 超過 600KB,繼續 q=0.85 size=580KB → 符合要求,完成! final size: 580 KB 如果原圖品質很差,即使降到最低品質仍超過 600KB,程式也會在 q=0.40 時停止,避免過度壓縮導致圖片難以辨識。
完整程式碼
以下是完整的可執行範例:
<!DOCTYPE html> <html lang="zh-TW"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Let's write - 圖片壓縮</title> </head> <body> <h1>Let's write - 圖片壓縮工具</h1> <p>筆記文:<a href="https://www.letswrite.tw/compressjs/" target="_blank"></a></p> <input type="file" id="upload" accept="image/*" /> <img id="preview" style="max-width:300px; margin-top:20px;" /> <script src="https://cdn.jsdelivr.net/npm/compressorjs@1.2.1/dist/compressor.min.js"></script> <script> function compressWithFloor( file, targetSize = 600 * 1024, floor = 0.4, step = 0.05 ) { return new Promise((resolve, reject) => { let q = 1.0; const attempt = () => { new Compressor(file, { quality: q, mimeType: "image/webp", maxWidth: 2560, maxHeight: 1440, success(blob) { console.log( `q=${q.toFixed(2)} size=${Math.round(blob.size / 1024)}KB` ); if (blob.size > targetSize && q - step >= floor) { q = +(q - step).toFixed(2); attempt(); } else { resolve(blob); } }, error(err) { reject(err); }, }); }; attempt(); }); } function blobToBase64(blob) { return new Promise((resolve) => { const r = new FileReader(); r.onloadend = () => resolve(r.result); r.readAsDataURL(blob); }); } // 依序嘗試 q=1.00, 0.95, 0.90, ... ,直到 <=600KB 或 q<0.40 function compressWithFloor( file, targetSize = 600 * 1024, floor = 0.4, step = 0.05, ) { return new Promise((resolve, reject) => { let q = 1.0; const attempt = () => { new Compressor(file, { quality: q, mimeType: "image/webp", // 強制轉 WebP maxWidth: 2560, // 最大寬度限制 maxHeight: 1440, // 最大高度限制 success(blob) { console.log( `q=${q.toFixed(2)} size=${Math.round(blob.size / 1024)}KB`, ); if (blob.size > targetSize && q - step >= floor) { q = +(q - step).toFixed(2); attempt(); // 繼續壓縮 } else { resolve(blob); // 符合大小或到達品質下限 } }, error(err) { reject(err); }, }); }; attempt(); }); } // Blob -> base64 function blobToBase64(blob) { return new Promise((resolve) => { const r = new FileReader(); r.onloadend = () => resolve(r.result); r.readAsDataURL(blob); }); } document.getElementById("upload").addEventListener("change", async (e) => { const file = e.target.files[0]; if (!file) return; const targetSize = 600 * 1024; // 檢查原始檔案大小 if (file.size <= targetSize) { console.log( `原始檔案大小 ${Math.round(file.size / 1024)}KB,已小於目標大小 ${Math.round(targetSize / 1024)}KB,不需壓縮`, ); alert(`圖片大小已符合要求 (${Math.round(file.size / 1024)}KB),無需壓縮`); // 仍然顯示預覽 const base64 = await blobToBase64(file); document.getElementById("preview").src = base64; return; } try { const compressed = await compressWithFloor(file, targetSize, 0.4, 0.05); console.log("final size:", Math.round(compressed.size / 1024), "KB"); const base64 = await blobToBase64(compressed); document.getElementById("preview").src = base64; const originalName = file.name.replace(/\.[^/.]+$/, ""); const newFileName = `${originalName}-compress.webp`; const link = document.createElement("a"); link.href = base64; link.download = newFileName; link.click(); } catch (err) { console.error("壓縮失敗:", err); } }); </script> </body> </html> Demo 與原始碼
想要實際體驗圖片壓縮工具的話,以下是完整的 Demo 頁面和原始碼供大家使用。
使用前,如果這個工具對你有幫助,歡迎到 GitHub 給我一顆星星 ⭐,你的小小動作,對本站都是大大的鼓勵。
Demo:Demo
GitHub 原始碼:GitHub Repository
Top comments (0)