去年接手一個營運後台,列表頁打開要九秒多。抓包看下去,介面本身只跑了 300 毫秒,回應內容卻有 22MB——每條記錄都把商品主圖整個塞進了 JSON,欄位長這樣:"pic": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."。單張原圖 850KB,編碼完 1.13MB,一頁 20 條就爆到這個量級。當時寫這段程式碼的人理由很正當:「省一次圖片請求」。要理解為什麼這個理由只在很小的圖上成立,得先把 Base64 這件事講清楚。
一、它解決的是「搬運」,從來不是「保護」
Base64 幹的活很窄:把任意位元組流換成只用 64 個可列印字元寫成的文字,讓它在只認文字的通道裡安全通過——JSON、XML、郵件標頭、URL 查詢字串都算這類通道。早期郵件協定只能傳 7 位 ASCII,附件裡的二進位必須換個說法;JSON 欄位也一樣,塞進原始位元組會直接把解析器打斷。
但「換個說法」不等於加密。它沒有金鑰,沒有口令,任何人都能把結果原樣還原回去。我見過真實的例子:有專案把使用者手機號碼 Base64 之後寫進記錄,安全評審時被一句話問死——這跟沒處理有什麼區別。JWT 也是同一個道理,它的 Header 和 Payload 只是 Base64Url 編碼,把字串貼進主控台就能讀出裡面的手機號碼、角色和到期時間;真正的安全來自第三段簽章,不是那層編碼。
二、為什麼會變大,末尾的 = 是怎麼來的
編碼表是 A-Z、a-z、0-9、+、/。規則是把 3 個位元組(24 位)切成 4 段、每段 6 位,每段落進 0 到 63 的索引取一個字元。湊不滿 3 位元組的部分用 = 補齊,所以結尾常出現一到兩個等號。代價寫死在數學裡:3 位元組換 4 字元,體積變成原來的 4/3。
| 原文 | UTF-8 位元組數 | 編碼結果 | 說明 |
|---|---|---|---|
| Man | 3 | TWFu | 剛好一組,無填充 |
| hi | 2 | aGk= | 補 1 個 = |
| hello | 5 | aGVsbG8= | 5 不是 3 的倍數 |
| 張三 | 6 | 5byg5LiJ | 中文字約 3 位元組,正好兩組 |
用這張表能順手解釋兩個日常疑問:為什麼中文 Base64 後長度總是 4 的倍數(UTF-8 下一個漢字通常 3 位元組),以及為什麼看到 = 結尾不代表出錯,只是補位。
三、圖片內聯到底什麼時候划算
Data URI 的寫法是 data:[MIME類型];base64,[內容],可以直接進 <img src>,也能當 CSS 背景。它省掉的是一次 TCP 連線和請求往返,代價是這塊內容跟著 HTML 一起走,無法被瀏覽器單獨快取——首頁 HTML 一改,內聯圖片就得整個重傳。
我的經驗值卡在 2KB:以內的小圖示、箭頭、佔位圖形內聯,收益明確;再大就換成獨立檔案,並且給它加指紋和長快取。上面那次故障裡的 850KB 商品圖,編碼後 1.13MB,gzip 也救不回來——原始二進位裡的冗餘已經被 6 位分組打散,壓縮率明顯低於直接壓原檔案。同類需求我會先把大圖留在 CDN 上,只把 16×16 的圖示內聯,那部分體積膨脹三成就完全可以接受。
不適合內聯的還有:任何使用者可上傳的圖、需要被搜尋或重用的圖、以及需要單獨設 Cache-Control 的圖。
四、我現在實際的用法
- 介面除錯要還原一段可疑字串時,我直接在瀏覽器裡做,圖省事也少一層顧慮——文字、圖片、檔案都在Base64 編解碼裡跑,本機完成、不外傳;解碼中文時不用自己處理編碼,頁面對 UTF-8 是認得的。
- 要嵌 CSS 的小圖示就在同一頁把圖片轉成帶
data:image/...;base64,前綴的結果,直接貼進樣式表;心裡再拿原檔案大小乘 4/3 估一遍,決定要不要真的內聯。 - 音訊、PDF 這類任意檔案轉字串,用二進位轉 Base64,它會同時列出原始大小和編碼後大小,超大檔案受瀏覽器記憶體限制會失敗,這時該走分片上傳而不是硬編。
- 要拼進 URL 的參數,先用URL 編解碼過一遍再發,別指望 Base64 結果能原樣躺在查詢字串裡。
五、踩過才知道的幾個坑
- 加號被吃掉:Base64 字元表裡的
+在查詢字串裡按舊約定表示空格。一段合法編碼傳過去解出來內容全變,多半就是這個。要嘛先 urlencode,要嘛改用把+、/換成-、_且不補=的 Base64Url(JWT 用的就是它)。 atob解中文亂碼:atob()回傳的是按位元組拼出來的字串,中文會碎成ö¼¡這類東西。要嘛decodeURIComponent(escape(atob(s))),要嘛用TextDecoder;Node 裡Buffer.from(s,'base64').toString('utf8')才是對的做法。- 換行導致解碼失敗:從郵件、文件或記錄裡複製出來的長串經常帶
\r\n或空格,Python 的b64decode會直接拋binascii.Error: Invalid base64-encoded string。先把空白清掉再解。 - 長度不對齊:Base64 結果長度必是 4 的倍數。如果拿到 43 個字元,說明開頭或結尾被截了——這在抓包工具的截斷顯示裡特別常見。
- MIME 寫錯:PNG 的前綴寫成
data:image/jpeg;base64,,瀏覽器不一定報錯,但會顯示成破圖;自己手拼時最好整段由工具產生。
六、常見疑問
Q:Base64 能當加密用嗎?
不能。它是無金鑰的可逆編碼,任何拿到密文的人都能還原,需要保密請用真正的加密演算法,需要存口令請用帶鹽的雜湊。
Q:為什麼我編碼完體積大了三分之一?
因為每 3 個位元組必須用 4 個字元表達。這是「用可列印字元承載二進位」的固定開銷,跟內容是不是圖片無關。
Q:小圖片內聯真的能提速嗎?
在兩三 KB 的量級上,省掉一次請求往返的收益大於體積膨脹;再往上就是淨虧損,改成獨立檔案加快取更快。
Q:末尾的一兩個 = 要去掉嗎?
標準 Base64 裡它們是補位符,解碼時留著最穩。有些語言允許嚴格模式校驗,去掉反而會報長度錯誤。
Q:能不能用 Base64 傳大檔案?
不建議。體積漲三成、要整塊讀進記憶體、也斷了串流處理的可能,幾 MB 就該走物件儲存或分片上傳。
回看那個九秒的列表頁,改動其實只有一行:把圖片欄位換成 URL,再給圖示留內聯。判斷標準也簡單了——問這段內容是要「搬過去」還是要「藏起來」,問它是不是小於 2KB,兩個問題答完,用不用 Base64 基本就不會錯了。