File API实战指南:读取、处理与上传文件的前端技巧
优化前:卡得不行
上个月我接了个上传大文件的需求,用户要能上传几百 MB 的视频。一开始没当回事,直接用 FileReader 读整个文件,再转成 base64 丢给后端。结果本地测试时,100MB 的文件一上传,浏览器直接卡死——页面无响应,风扇狂转,连 DevTools 都打不开。用户反馈更狠:“点上传后电脑像被施了定身法”。
这哪行啊?我赶紧回滚代码,但需求又不能砍。只能硬着头皮优化。说实话,那几天做梦都是文件分片和内存溢出的报错。
找到瓶颈了!
先用 Performance 面板录了个操作过程。一看吓一跳:主线程被一个长达 4.8 秒的 FileReader.readAsDataURL 调用占满了,期间完全没空处理 UI 事件。再切到 Memory 面板,堆内存直接从 50MB 飙到 800MB+,全是那个 base64 字符串占的——100MB 的文件转成 base64 后膨胀到 133MB,还全在内存里堆着。
问题很明显:一次性读大文件 + base64 编码 = 主线程阻塞 + 内存爆炸。得拆!
核心思路:别让主线程干重活
试了几种方案:
- 方案一:用 Web Worker 处理 FileReader。但 FileReader 本身不能直接扔进 Worker(它依赖 DOM),得靠 postMessage 传文件,大文件传过去又卡。
- 方案二:分片读取 + 流式上传。这个靠谱,但实现起来有点绕。
最后选了方案二,核心就两点:1)用 slice 切文件;2)用 FormData 直接传二进制,别碰 base64。base64 纯属自找麻烦,体积大还耗 CPU。
关键优化:分片 + 直接传二进制
优化前的代码是这样的(别笑,真有人这么写):
// 优化前:灾难现场
function uploadFile(file) {
const reader = new FileReader();
reader.onload = (e) => {
const base64 = e.target.result;
// 发送 base64 字符串,体积巨大
fetch('/upload', {
method: 'POST',
body: JSON.stringify({ data: base64 })
});
};
reader.readAsDataURL(file); // 阻塞主线程!
}
改成这样:
// 优化后:分片 + 直接传二进制
const CHUNK_SIZE = 1024 * 1024; // 1MB 分片
async function uploadFile(file) {
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
for (let i = 0; i < totalChunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end); // 切片,不复制数据
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', i);
formData.append('total', totalChunks);
// 直接传二进制,不经过 base64
await fetch('https://jztheme.com/api/upload', {
method: 'POST',
body: formData
});
// 这里可以加个进度更新
updateProgress((i + 1) / totalChunks);
}
}
这里注意我踩过好几次坑:1)file.slice() 是轻量级的,只是创建文件句柄,不会复制数据;2)千万别用 readAsArrayBuffer 再手动拼接,那又回到内存爆炸的老路;3)用 FormData 传 Blob,浏览器会自动处理 multipart 编码,后端也省事。
再榨干一点性能:并发控制
分片后发现新问题:如果同时发 100 个请求(比如 100MB 文件分 100 片),浏览器会卡在“排队”状态,反而更慢。于是加了并发限制:
// 控制并发数,避免请求爆炸
async function uploadWithConcurrency(file, concurrency = 3) {
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
const chunks = Array.from({ length: totalChunks }, (_, i) => i);
async function uploadChunk(index) {
const start = index * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', index);
return fetch('https://jztheme.com/api/upload', {
method: 'POST',
body: formData
});
}
// 并发执行,最多同时 3 个
const results = [];
for (let i = 0; i < chunks.length; i += concurrency) {
const batch = chunks.slice(i, i + concurrency);
const batchPromises = batch.map(uploadChunk);
results.push(...await Promise.all(batchPromises));
updateProgress(i / chunks.length);
}
return results;
}
实测并发数设为 3-5 最稳,再高网络反而抖动。这个值得根据实际带宽调,别盲目设 10。
性能数据对比
拿 150MB 的视频文件实测(Chrome 120,MacBook Pro M1):
- 优化前:内存峰值 1.2GB,上传耗时 12.3s(含卡死等待),CPU 占用 95%+
- 优化后(分片+并发=3):内存峰值稳定在 150MB,上传耗时 2.1s,CPU 占用 40% 左右
最直观的是体验:以前上传时页面完全冻住,现在进度条丝滑,还能随时取消。用户反馈从“卡死了”变成“挺快啊”。
还有个小尾巴
其实分片上传后端也得配合,要能合并分片。不过这是后话了。前端这边还剩个小问题:超大文件(比如 2GB)分片太多,进度条更新太频繁会轻微卡顿。后来我改成每 10 个分片更新一次进度,基本无感了。不是完美方案,但够用。
另外,如果项目支持,直接上 ReadableStream + fetch 流式上传会更优雅,但兼容性差点(Safari 16.4 才支持)。我这项目要兼容老 Chrome,所以还是用分片稳妥。
总结
File API 性能优化的核心就一句:别让主线程碰大文件,别用 base64。分片、二进制直传、控制并发,三招搞定。折腾完这波,我对“用户上传大文件”再也不慌了。
以上是我踩坑后的实战总结,有更优的实现方式(比如用 Web Worker + Transferable 对象传文件句柄)欢迎评论区交流。这个技巧的拓展用法还有很多,比如结合压缩、校验,后续会继续分享这类博客。

暂无评论