糟糕的开头
就在刚才,我试图在 daima.life 的测试环境里转换一张 150MB 的超高清 8K 壁纸到 Base64。结果按钮一按,整个人直接傻掉:MacBook 的风扇开始疯狂起飞,Chrome 标签页直接变成了一片惨白,DevTools 里赫然躺着一句冰冷的 Total canvas memory use exceeds the maximum limit。没错,在 2026 年,如果你还用 FileReader.readAsDataURL() 去撸百兆级的大图,浏览器依然会秒变砖头给你看。
我的思考
为什么市面上 90% 的在线转换工具遇到大图就直接假死?核心逻辑就在于,它们都在“一把梭”。传统的 readAsDataURL 会试图把整个文件内容一次性吞进内存,然后在内存里构造一个长得离谱的字符串。对于 100MB 的图片,生成的 Base64 字符串大约会占用 133MB 的内存。再加上浏览器渲染这些字符串的开销,主线程不卡死才怪。
在 daima.life,我坚持“纯前端计算”,但纯前端不代表无脑。为了搞定这个“内存炸弹”,我决定借鉴流式渲染(Streaming)的思想,把这个过程切碎了喂给浏览器。与其等它爆炸,不如分餐而食。
技术硬核区
思路很简单:不要等整个文件读完,而是利用 Blob.stream() 开启一个 ReadableStream。我们分块读取二进制数据(比如每次 64KB),在 Web Worker 里异步转换成 Base64 片段,最后实时输出。核心避坑点在于:Base64 每 3 个字节映射为 4 个字符,如果分块切断了这就麻烦了,所以需要处理“遗留字节”。
// 核心伪代码:在此处处理边缘对齐
async function streamToBase64(file: File) {
const reader = file.stream().getReader();
let remainder = new Uint8Array(0);
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 合并之前的遗留字节,确保按 3 的倍数切割
const chunk = concatUint8(remainder, value);
const safeLen = Math.floor(chunk.length / 3) * 3;
const toProcess = chunk.subarray(0, safeLen);
remainder = chunk.subarray(safeLen);
// 在 Worker 线程转换,绝不占用主线程显存
const b64Part = btoa(uint8ToString(toProcess));
postMessage({ type: 'CHUNK', data: b64Part });
}
}
通过这种方式,内存占用被死死地控制在了几百 KB 的量级。不管你的图片是 100MB 还是 1GB,我的解析器都能像吃蚕丝一样把它一点点吐出来,主线程甚至还能丝滑地跑一个 60FPS 的进度条动画。这就是真香定律。
FAQ 模块
Q1: 既然是流式,为什么非要折腾 Web Worker?
A: 兄弟,虽然内存降下来了,但 btoa 的计算量和大数组的操作依然会吃 CPU 周期。跑在 Worker 里,主线程才能去刷 UI 和响应用户的“取消”操作。用户体验要是卡了,那这工具做得就太挫了。
Q2: 处理边缘字节对齐太麻烦,有没有偷懒的方案?
A: 别在这儿偷懒!这是 Base64 的灵魂。如果你随便切,边缘字符就会在拼接处产生无效编码。我手搓的这套方案保留了 1-2 个剩余字节带入下轮计算,这才是最稳的“边缘控制”。
Q3: Cloudflare Pages 部署这种大规模计算会有额外计费吗?
A: 完全没有!这正是 daima.life 架构的精髓:所有计算都发生在你自己的浏览器里。Cloudflare 只负责把压缩后的静态 JS 发过去。这种“白嫖”用户算力的架构,不仅隐私安全,还省了这一大笔后端服务器开销。
结尾
重构完这个逻辑后,看着 200MB 的大图在 2 秒内无声无息地转完,我只想说:别再迷信那些臃肿的后端方案了。浏览器就是个未被完全发掘的超算中心。不过话说回来,我最近在看用 WebAssembly 处理更复杂的图像编解码,也许这套流式方案很快又会被我迭代掉。要不要来试试下一代更硬核的降维打击?...