公司上个月办了个读者投稿活动。收上来的照片有一小半是 .heic,审核后台在 Chrome 里一张都预览不出来。同事问我能不能在网页里直接转 JPG。我先得搞清楚两件事:浏览器自己能读到什么程度?读不了的时候塞一个 WASM 解码器进去又要付出多少?测试用的是 Playwright 1.61.1 自带的三个内核构建。全部 headless,机器是 Apple M4 / 16 GB。样本是我用 sips 从合成图编出来的 HEIC,分 3MP、12MP 和 48MP 三档。它们不是 iPhone 拍的原片。成品对照我用的是图映 ImgIng(https://imging.cn/)的压缩转换,样本和机器都跟前面一样。
浏览器能直接打开 HEIC 吗
只有一家能。Chromium 149 和 Firefox 151 构建上四条原生路全部走不通。<img> 直接走进 onerror。img.decode() 报的是 EncodingError。createImageBitmap 则报 InvalidStateError。本地起的 Python 静态服务把 .heic 当成 application/octet-stream 发出去,我一开始怀疑是 MIME 不对。于是把 Blob 的 type 改成 image/heic 再喂一遍,照样失败。WebCodecs 这条路我也试了:两家的 isTypeSupported('image/heic') 都是 false。换成 image/avif 倒是 true。WebKit 26.5 构建则是全部成功。方向 6 和方向 8 的竖拍样本出来都是正的,也没有转两次。它能读是因为调用了 macOS 的系统解码器。我测的只是 Mac 上的这个构建,Safari 正式版和 iOS 上会不会一样我没有环境验证。

原生解码 HEIC 省在哪、差在哪
原生这条路最省的是下载量。图映在 WebKit 构建上直接显示「浏览器可读」,整个过程没有下载任何 HEIC 解码文件。上传到可读的时间 12MP 是 176 ms、48MP 是 439 ms。它的短板也很直接:能不能用全看用户用的是哪个内核,开发者一点都插不上手。WebKit 这个构建还没有 ImageDecoder,只能走 <img> 或 createImageBitmap 拿浏览器处理好的位图。
WASM 解码 HEIC 要下载多少
图映在 Chromium 和 Firefox 构建上先用 <img> 试一下原生。失败了才在界面上放一个「加载解码器」按钮,不点就什么都不下。点下去以后只多了两个请求。大头是 libheif-bundle.mjs:解压后 1,461,926 字节,gzip 传输 520,705 字节。WASM 是用 base64 塞在这个 .mjs 里的。解出来有 1,034,305 字节,二进制里能看到 libheif 1.19.8 和 libde265 的字样。我第一轮在 Network 面板里找 .wasm 请求一条都没找到,差点以为它根本没用 WASM。后来翻了文件内容才发现是内联的。再次打开页面也便宜,它把用过的解码器记在 localStorage 里。刷新后自动发两个条件请求,两个都回 304 共 1,095 字节。之后再传 HEIC 就不出按钮了。

WASM 这条路多出来的活
能读只是开始。Chromium 149 上 12MP 解码 377 ms,48MP 要 1.56 s。编译和解码都在主线程上同步跑。我挂的探针只记到 new WebAssembly.Module 和 new WebAssembly.Instance 各一次。没见到 Worker。解码期间最长的长任务 12MP 是 393 ms、48MP 是 1.60 s。长任务的长度基本就是整段解码的时长。方向要自己心里有数:sips 编出来的文件同时带着 irot 270° 和 EXIF 方向 6。libheif 只认前者。图映导出时再把 Orientation 改成 1,我数了 25 个产物全是正的。颜色是另一个坑:libheif 吐出来的是没做色彩管理的 P3 数值。在 Chromium 和 Firefox 构建上图映把源文件的 ICC 原样写回。12 个色块的 ΔE2000 都在 0.31 以内。同一批像素要是丢了 ICC 按 sRGB 看,域内颜色就偏到 2.36--3.33。
审核后台最后怎么接
我给同事的方案是两段。先用 createImageBitmap 试原生,成功就直接预览。失败再按需加载 WASM 解码器,导出时把 ICC 带上。48MP 那档在 M4 上都要卡一秒半,我在按钮旁边先加了一行「大图解码中」的提示。还有一块我心里没底:投稿里真正的 iPhone 原片可能带 HDR 增益图或 10 bit。这组合成样本一张都没覆盖。等拿到几张真原片我再跑一遍。