雜記 › 技術筆記 › WebP 轉 PNG 後體積漲了幾倍:什麼時候必須轉,怎麼把漲幅壓住

WebP 轉 PNG 後體積漲了幾倍:什麼時候必須轉,怎麼把漲幅壓住

設計師傳來一疊 WebP,接收端只認位圖這類老牌格式,只能轉 PNG。轉完尺寸沒變、透明還在,唯獨檔案從幾百位元組變成幾萬,最兇的一張 358 B 變成 28946 B,漲了 80.8547 倍。我以為是工具把圖弄壞了,比過像素才知道,壞掉的是我對「無損」的理解:PNG 承諾像素不改,從不承諾位元組數。

先看 13 組實測:1.4194 倍到 80.8547 倍

同一批素材逐張跑過 WebP 轉 PNG,挑 4 張覆蓋不同類型:

樣本輸入輸出倍數
flat_ui_1440_lossless.webp358 B28946 B80.8547×
gradient_256_q80.webp826 B64408 B77.9758×
lossy_photo_400_q80.webp1996 B107853 B54.0346×
one_px.webp62 B88 B1.4194×

1×1 的 one_px 只多 26 B 容器開銷,1440×900 純色 UI 裡,無損 WebP 花 358 B 記下 6 種顏色,交給 RGBA 容器要 28946 B。批次 8 張的匯總條原樣是「已轉換 8 / 8 張 · 合計 17.5 KB → 499.8 KB(體積增加 2764%)」,來自輸入 17872 B 與輸出 511807 B 的位元組加權 28.6373 倍,不是單張倍數的平均。

漲的是編碼方式,不是畫質

5 組來源解碼與輸出解碼的對比裡 diffPixels = 0、maxChannelDelta = 0、totalAbsDelta = 0,顏色數一字不差:23511 → 23511、6 → 6。漲的只能是描述像素的方式:輸出固定 colour type 6 的 RGBA,chunk 只有 IHDR、IDAT、IEND,沒有 pHYs 和 tRNS,也不建調色盤,每像素 4 位元組——1440×900 攤開 5184000 位元組原始 RGBA,壓完仍要 28946 B。

反差和直覺相反:無損 WebP 漲得最溫和,12938 B → 43232 B 只有 3.3415 倍;q80 照片爆得最兇,1996 B → 107853 B 是 54.0346 倍;同一張文字截圖,無損 15.4036 倍、q80 只有 4.6521 倍。WebP 先把高頻扔掉、碼流短到幾百位元組,PNG 卻照樣按每像素 4 位元組重鋪。diffPixels = 0 也只保證不新增損失:轉完顏色數仍是 28504,色帶被原樣釘死,第 234 行只有一次 drawImage,沒有抖動。

flat_ui_1440_lossless 80.8547× gradient_256_q80 77.9758× lossy_photo_400_q80 54.0346× large_1600_q80 23.8811× lossless_photo_400 3.3415× one_px 1.4194×

圖 1|長條依輸出位元組除以輸入位元組計算:同樣叫漲了幾倍,純色 UI、漸層和有損照片根本不是一個量級。

什麼時候確實必須轉

  • 接收端解不了 WebP。轉出來的是標準 IHDR、IDAT、IEND 的 PNG,13/13 張寬高等於輸入尺寸,從 1×1 到 1600×1200 共 12 種尺寸零縮放。
  • 透明還要讓下游繼續編輯,「保留透明通道」強制勾選並鎖定,48635 個全透明、4641 個半透明、36724 個不透明像素一字不差進 PNG;同一張轉 JPG 是 9633 B,透明全被 #ffffff 填掉。
  • 要把圖當成演算法輸入逐像素取值:interlace 0、RGBA 每像素 4 位元組,讀回來的值與 WebP 解碼 diffPixels = 0。
  • 只想讓網頁變小就別轉,13/13 張全部變大,最小 1.4194 倍;真要省位元組只能反向,PNG 轉 WebP 那一側才有品質滑桿。

怎麼把漲幅壓住

先劃掉一條路:PNG 頁簽下品質整行 display=none,toBlob 收到的 quality 是 undefined,滑桿拖到 10 只改顯示文字,重轉位元組數不變;也沒有位元組上限和壓縮開關,靜態掃描 MAX、slice(0, N) 命中 0 次,10718 B 的 large_1600_q80 照樣轉成 255958 B。

能動的是內容和容器:同一批像素換個編碼器再編一次,RGBA 與參考輸出是 64408 對 571、28946 對 5524、4459 對 1421、50000 對 39338。

112.8× 工具 64408 B 參考 571 B 5.24× 工具 28946 B 參考 5524 B 3.14× 工具 4459 B 參考 1421 B 1.27× 工具 50000 B 參考 39338 B

圖 2|深淺兩條用同一把尺;差距來自編碼器不建調色盤,不是可調項目。

64408 對 571 差 112.8 倍,漲出來的大半是 RGBA 平鋪的冗餘,換容器比換參數有效;50000 對 39338 只差 1.27 倍,說明大頭是真資訊,壓不動。非得交付 PNG 的,轉完再走一道只動過濾器的 PNG 壓縮;接收端沒說不能收 WebP 的,別繞這一圈。

幾個靜默發生的坑

  • 動態圖只出一幀:animated_2frame.webp 338 B → 1045 B、同素材 GIF 597 B → 1028 B,都是 200×120 單幀,chunk 裡無 acTL,幀率與迴圈全丟。
  • alpha = 0 的像素 RGB 被清零:輸入 (255,0,0,0) 輸出 (0,0,0,0),57600 個像素顏色數 1,580 B 漲到 2072 B。
  • 沒有格式白名單,accept 只寫 image/*,39587 B 的 JPEG 資料取名 fake_webp_is_jpeg.webp 照樣解成 1200×900 出 504277 B;只有 notes.txt 觸發「已跳過非圖片檔案:notes.txt」。
  • 打包依賴第 57 行 cdnjs 的 jszip 3.10.1:連網時 8 張得到 512759 B 的 ZIP,比成員總和 511807 B 多 952 B;抓不到腳本時退化成只下載第 1 張 88 B 的 one_px.png,也沒有錯誤提示。

Q:漲這麼多是不是轉壞了?

不是。diffPixels = 0、maxChannelDelta = 0,顏色數一張都沒變,13/13 張 IHDR 寬高也與輸入相等。漲的全在編碼:每像素 4 位元組、不建調色盤,1.4194 倍和 80.8547 倍是同一條規則的兩端。

Q:能讓工具少漲一點嗎?

不能。品質那一行被隱藏、quality 傳的是 undefined,拖到 10 輸出也不變;也沒有位元組上限和「變大就退回原格式」的分支,第 224 到 236 行只有 drawImage 加 toBlob。能擰的只有轉之前定格式、轉之後補一道壓縮。

Q:這次到底該轉還是該壓?

先看是接收端不支援 WebP,還是位元組數不滿意。前者必須轉,13 組全部變大也得認;後者轉了只會更糟。拿不準就按內容估:純色 UI 80.8547×、q80 照片 54.0346×、無損照片 3.3415×。

這批素材最後照樣轉了,只是我把 17872 B 變 511807 B、增加 2764% 那一行貼進需求單。真正改掉的是協作順序:交付格式在開工前由接收端定,而不是轉完才發現裝不進去。

📎 相關文章

PNG 壓了三遍只減 426 位元組:無損壓縮的真實邊界 2026-10-22 截圖縮到四分之一,小字糊成一團的三種原因 2026-10-22 同樣裁 300×300,頭像為什麼總偏右下角半格 2026-10-22 按每月 1.7 倍預估 18 個月,腳本和表格為什麼給出兩個數 2026-10-22 介面 JSON 貼成 PHP 陣列,哪些值會悄悄變形 2026-10-22