← 返回技术札记
IMAGE PIPELINE · 2026.08.24

把 RAW 解码器塞进浏览器之后,我们先撞上了内存墙

CR3、NEF、ARW 为什么能直接拖进网页?这是图派用 LibRaw、WebAssembly 和 Web Worker 构建浏览器端 RAW 预处理链路时,真正遇到的问题。

摄影师的硬盘里很少只有 JPEG。Canon 的 CR3、Nikon 的 NEF、Sony 的 ARW、Fujifilm 的 RAF,以及越来越常见的 DNG,才是许多工作流真正的起点。

但浏览器不这样看。

把一张 JPEG 交给 <img>,浏览器知道怎样解码、怎样按屏幕尺寸绘制;把一张 CR3 交给它,多数时候得到的只是一份无法预览的二进制文件。如果线上选片系统只接受 JPEG,摄影师就要在上传前额外导出一套图片——哪怕这些图片唯一的用途,只是让客户辨认并选中它们。

我们想去掉这一步。于是问题从“上传照片”变成了:能不能让浏览器在本地读懂 RAW,并把它变成适合选片的预览图?

浏览器为什么不认识 RAW

RAW 不是一种统一的图片格式。它更像一组厂商各自定义的容器,里面可能包括传感器原始数据、内嵌 JPEG、EXIF 信息、色彩矩阵和相机私有字段。同一个扩展名,在不同相机型号甚至不同固件版本中,也可能存在差异。

更重要的是,RAW 不是“尚未压缩的 JPEG”。从 Bayer 或 X-Trans 传感器数据得到一张可见图片,需要经过黑电平校正、坏点处理、去马赛克、白平衡、色彩转换和曲线映射。浏览器内置的图片解码器通常不会承担这些工作。

我们没有重新实现一套 RAW 解析器,而是选择了 LibRaw:一个被许多桌面图像工具使用的 C/C++ RAW 解码库。再通过 Emscripten 把需要的能力编译成 WebAssembly,让它能在浏览器的沙箱中运行。

这里的目标必须先说清楚:图派生成的是用于客户辨认与选择的预览 JPEG,不是代替 Lightroom 或 Capture One 完成专业显影。色彩足够可靠地识别照片即可,最终成片仍然来自摄影师自己的后期流程。

一条发生在本地的处理链路

用户选择 RAW 文件后,当前处理链路没有立刻把整份文件发送给服务器,而是先在浏览器完成预处理:

01FileCR3 / NEF / ARW
02ArrayBuffer转移所有权
03LibRaw · WASMWorker 中解码
04RGB → Canvas缩放与编码
05JPEG上传选片预览

主线程先通过 File.arrayBuffer() 读取文件,再把 ArrayBuffer 发送给 RAW Worker。发送时使用 Transferable,把这块内存的所有权交给 Worker,而不是复制一份同样大的数据:

worker.postMessage(
  { rawData, fileName: file.name, type: 'decode' },
  [rawData]
);

Worker 初始化 LibRaw WASM 模块,把文件数据写进 WASM 线性内存,调用解码函数取得图像尺寸和 RGB 数据。主线程收到像素后补齐 Alpha 通道,绘制到 Canvas,再按账户允许的最长边缩放并编码为质量 0.9 的 JPEG。

对 JPG、PNG、WebP 和浏览器能够读取的 HEIC/HEIF,我们走一条更短的路径:在另一个 Worker 中使用 createImageBitmap()OffscreenCanvas 完成尺寸读取、缩放和 JPEG 编码。RAW 需要 LibRaw,普通图片则尽量使用浏览器自己的解码器。

两条路径最后汇合为同一种上传对象:标准化 JPEG、原始文件名、原始文件大小、输出尺寸和源图片尺寸。上传本身使用 OSS Post Policy 直传,服务端只负责创建任务、签发表单并在结束后确认对象存在。

这减少了应用服务器转发大文件的带宽压力,但也把一个更棘手的问题带到了用户设备上。

真正的限制不是 CPU,而是内存

假设一台相机拍摄 40MP 照片,解码后的 RGBA 像素缓冲大小约为:

40,000,000 pixels × 4 bytes ≈ 152.6 MiB

这只是其中一份缓冲。一次完整处理还可能同时存在:

  • RAW 原文件对应的 ArrayBuffer;
  • LibRaw / WASM 内部的输入与解码缓冲;
  • 从 RGB 补齐为 RGBA 后的 Uint8ClampedArray;
  • 源尺寸 Canvas 的像素缓冲;
  • 缩放后 Canvas 的像素缓冲;
  • 最终生成的 JPEG Blob。

因此,“同时解码四张会不会更快”不是一个只看 CPU 核心数的问题。四个任务并行,很可能在吞吐量提升之前,先让浏览器标签页占用数百 MB 甚至更多内存。在手机和配置较低的电脑上,结果通常不是慢一点,而是页面直接被系统回收。

我们的处理器因此刻意采用串行队列:

const task = processingQueue.then(() => preprocessImageNow(file, options));
processingQueue = task.catch(() => {});
return task;

每次只允许一张图片进入解码和 Canvas 分配阶段。上传可以独立重试,但高峰内存占用必须受控。这牺牲了批量任务的理论速度,换来更可预测的稳定性。

这也是整条链路里最反直觉的决定:并行并不总是性能优化;当单个任务的工作集足够大时,限制并发本身就是优化。

在解码前拒绝危险输入

仅依靠串行还不够。图片尺寸来自不可信的本地文件,如果直接用宽高创建像素数组,异常文件或解析错误可能造成巨大的内存申请。

目前上传策略同时限制:

  • 单边不得超过 30,000 像素;
  • 总像素不得超过 2.5 亿;
  • 宽高必须是安全、正数整数;
  • 文件大小必须有效且扩展名在允许列表内。

这不是为了限制正常相机,而是避免在真正分配 width × height × 4 字节之前失去控制。对于 2.5 亿像素的 RGBA 数据,仅一份像素缓冲就接近 1GB,所以这个上限更像最后一道保险,而不是推荐工作尺寸。

Worker 不是兼容性的终点

把工作放进 Worker 能避免解码时冻结按钮和滚动,但 Worker 不能自动抹平浏览器差异。

createImageBitmapOffscreenCanvas、HEIC 解码与 EXIF 方向处理,在不同 Safari 和 Chromium 版本中的组合支持并不一致。例如,某些 Safari 版本可以通过页面中的 <img> 读取图片,却不能在 Worker 中用 createImageBitmap 解码同一文件。

所以普通图片预处理有两层路径:

  1. 优先在 Worker 中使用 createImageBitmap + OffscreenCanvas
  2. 如果失败,回到主线程,用 Object URL 加载 <img>,再通过普通 Canvas 编码。

回退不是免费的。主线程 Canvas 可能造成短暂卡顿,也意味着我们必须更谨慎地撤销 Object URL、释放 Bitmap,并继续保持串行。但比起把“浏览器支持该格式”误判成“Worker 也支持该格式”,显式降级更可靠。

RAW Worker 还需要处理另一类失败:WASM 文件下载失败、模块初始化失败、LibRaw 不认识某个新相机、文件内容损坏,或解码结果长度与尺寸不一致。每一步都需要独立的错误信息,否则用户最终只会看到一个没有解释的“上传失败”。

上传也不是一次请求

预处理完成后,浏览器先向服务端创建图片记录并取得 OSS 上传表单,再直接向对象存储发送 JPEG,最后调用完成接口进行校验。

上传凭证可能过期或网络可能在中途断开。第一次直传失败时,客户端会重新创建上传任务、获取新表单并重试一次。如果仍然失败,则清理未完成的图片记录,避免相册中长期留下一个永远无法加载的占位项。

我们不进行无限重试。用户主动重新选择文件,通常比浏览器在弱网环境里悄悄循环更可控。

文件名比预览图更重要

RAW 经过浏览器处理后,上传对象已经是 JPEG,但它不能只剩下一个新的 .jpg 名字。

客户最终选择的是画面,摄影师回到本地后需要找的是原片。因此我们同时保留 originalFileName,例如 DSC_4821.NEF。选片完成后,系统可以把客户的选择重新映射回摄影师硬盘和 Lightroom 中的文件。

对于 PNG、WebP 等普通图片,内部 JPEG 名称会保留原扩展名作为一部分,例如 photo.webp.jpg,避免 photo.pngphoto.webp 在标准化之后都坍缩成同一个 photo.jpg,从而触发错误的重复文件判断。

这看起来只是命名细节,但选片系统的真正产物并不是 JPEG 本身,而是客户的视觉选择与摄影师原始文件之间的稳定对应关系

我们最终接受的取舍

这套方案没有让浏览器变成完整的 RAW 工作站,也没有消除所有上传等待。它只是把“为了选片先批量导出 JPEG”这一步,移动到了用户选择文件之后,并尽量安静地完成。

我们接受了这些边界:

  • RAW 解码串行执行,以稳定性换取峰值内存可控;
  • 输出是限定尺寸的选片 JPEG,不是专业显影结果;
  • 新相机格式的支持取决于 LibRaw 能力和我们的兼容验证;
  • Worker 路径失败时允许回退,而不是追求架构上的绝对纯粹;
  • 当前上传的是浏览器生成的选片预览,并非把完整 RAW 原件保存到云端。

最后一条尤其重要。“支持选择 RAW 文件”与“提供 RAW 云端存储”是两件不同的事。我们更愿意把能力边界写清楚,因为摄影师有权知道原片究竟去了哪里。

WebAssembly 让一个原本属于桌面软件的能力进入了网页,但真正决定体验的,不是 WASM 这个名词,而是围绕它做出的内存预算、降级路径、错误恢复和文件映射。

技术最终应该退到照片后面。客户看到的是一张可以顺畅选择的预览,摄影师拿到的是能够回到原始工作流的文件名;中间那段复杂的解码过程,最好不需要任何人留意。

FIELD NOTES

更多工程记录,都在技术札记里。

只写已经发生的问题、验证过的方案,以及仍然存在的边界。

查看全部札记