OSS上传实战技巧与前端优化避坑指南

莆泽 Dev 交互 阅读 1,784
赞 17 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

上周上线了个图片上传功能,用户反馈说“传个图要等半天”,一开始没当回事,觉得可能是网络问题。直到我自己在测试机上点了一下——好家伙,选完照片页面直接卡死两秒,进度条还没动。再传个大点的视频?直接白屏,控制台报错内存溢出。

OSS上传实战技巧与前端优化避坑指南

这哪行啊,赶紧看下数据。用 DevTools 录了下性能面板,发现主线程被一个叫 FileReader.readAsArrayBuffer 的操作占了整整 2.8 秒,期间 UI 完全无响应。而且是同步阻塞的那种卡,滑都滑不动。

问题很明显了:我们把整个文件读取 + 分片上传的逻辑全塞在主线程里跑,大文件一来,浏览器直接跪了。

找到瓶颈了!

我先上了 Performance 面板跑了一遍记录,重点关注 Tasks 列表。果然看到一大段红色长条,持续时间超过 3 秒,点击进去一看调用栈:

  • handleFileSelect → readFile → new FileReader() → readAsArrayBuffer
  • 后面还跟着一堆 base64 编码和分片处理逻辑

这里注意我踩过好几次坑:很多人以为 FileReader 是异步的就不会卡 UI,但其实它虽然回调是异步的,中间读取过程还是会占用主线程资源,尤其是大文件。再加上我们之前为了兼容老逻辑,先把文件转成 base64 再分片,等于多了一倍的数据复制开销。

我还用 performance.mark() 手动打了几个点测耗时,结果更吓人:一个 50MB 的视频文件,光读取+转码就花了 3.2s,上传才用了 1.8s。也就是说,处理比传输还慢。

核心优化:扔到 Worker 里去

第一反应就是:这种纯计算型任务必须挪出主线程。于是上了 Web Worker。不过 FileReader 在 Worker 里不能用,得换方案——改用 file.arrayBuffer(),这个可以在 Worker 中安全调用。

下面是关键改造代码:

// worker.js
self.onmessage = async function(e) {
  const { file, chunkSize } = e.data;
  const arrayBuffer = await file.arrayBuffer();
  const chunks = [];
  for (let start = 0; start < arrayBuffer.byteLength; start += chunkSize) {
    const end = Math.min(start + chunkSize, arrayBuffer.byteLength);
    const chunk = arrayBuffer.slice(start, end);
    chunks.push({
      index: start / chunkSize,
      data: chunk,
      total: Math.ceil(arrayBuffer.byteLength / chunkSize)
    });
    // 每处理完一块发个进度
    self.postMessage({ type: 'progress', loaded: end, total: arrayBuffer.byteLength });
  }
  // 发送所有分片(可以用 postMessage 传输 ArrayBuffer 零拷贝)
  self.postMessage({ type: 'done', chunks }, [chunks.map(c => c.data).reduce((a,b)=>[...a,b],[])]);
};

主线程这边创建 Worker 并通信:

// main.js
function uploadFile(file) {
  const worker = new Worker('/worker-upload.js');
  const chunkSize = 5 * 1024 * 1024; // 5MB 分片

  worker.postMessage({ file, chunkSize }, [file]);

  worker.onmessage = function(e) {
    if (e.data.type === 'progress') {
      updateProgress(e.data.loaded, e.data.total);
    } else if (e.data.type === 'done') {
      // 开始并发上传分片
      e.data.chunks.forEach(chunk => uploadChunk(chunk));
      worker.terminate();
    }
  };
}

这里有个重点:传递 file 给 Worker 时要用 postMessage(file, [file]) 启用 Transferable Objects,避免结构化克隆带来的性能损耗。不然大文件照样卡。

顺手优化了几处细节

除了主流程移到 Worker,还有几个小地方也改了:

  • 取消了中间 base64 转码步骤,直接传 ArrayBuffer 给 OSS SDK
  • OSS 分片上传启用了并发(最多 3 个 concurrent request),原来是一个传完再传下一个
  • 加了个简单的节流,progress 回调每 200ms 更新一次 UI,防止频繁 render

其中并发上传这块改得比较简单:

async function uploadChunk(chunk) {
  const { index, data, total } = chunk;
  const blob = new Blob([data]);
  const formData = new FormData();
  formData.append('file', blob, chunk-${index});
  
  return fetch('https://jztheme.com/api/oss/upload-chunk', {
    method: 'POST',
    body: formData
  }).then(res => res.json());
}

// 并发控制
async function uploadChunks(chunks) {
  const results = [];
  const concurrency = 3;
  const executing = [];

  for (const chunk of chunks) {
    const p = uploadChunk(chunk).then(res => {
      executing.splice(executing.indexOf(p), 1);
      return res;
    });
    executing.push(p);
    
    if (executing.length >= concurrency) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}

优化后:流畅多了

改完重新压测一遍。这次用的是 80MB 视频文件,在中端安卓机上测试:

  • 优化前:UI 卡死 3.5s,总上传时间约 6.2s
  • 优化后:UI 响应正常,可滚动、可点击取消;上传总时间降到 2.4s

最关键的是用户体验变了——不再是“点了没反应”,而是立刻看到进度条动起来,哪怕上传慢点也能接受。

后来我又试了 Service Worker 和 Comlink 封装 Worker 通信,但提升不大,反而增加了复杂度。最后决定就用原生 Worker + Transferable Objects 这套最稳的组合。

性能数据对比

这是我在三台不同设备上测的平均值(80MB 文件):

指标 优化前 优化后 提升
主线程阻塞时长 3480ms 120ms 96.5%
文件处理耗时 3200ms 800ms 75%
总上传时间 6200ms 2400ms 61.3%
最大内存占用 ~400MB ~180MB 55%

内存下降明显是因为避免了 base64 编码产生的额外字符串副本,加上 Transferable Objects 实现零拷贝传输。

还有点小问题

当然也不是完美。现在的问题是 Safari 对 Worker 中使用 file.arrayBuffer() 支持不太好,偶尔会报 DOMException: The operation was aborted。目前临时方案是在不支持的环境降级回主线程处理,加个 loading 提示。

另外 Worker 调试确实麻烦,console.log 得靠 onmessage 转发,source map 也不太准。折腾了半天才发现是 chunk slice 时边界算错了。

以上是我的优化经验

这次优化核心就一条:别让文件处理挤占主线程。Worker + 分片 + 并发上传,这套组合拳打下来效果立竿见影。虽然方案不算新颖,但在真实项目中依然经常被忽略。

代码我已经简化抽离了一份可运行 demo 放在 GitHub,有兴趣可以搜关键词“oss-upload-worker”找到。如果有更好的并发控制或断点续传实现方式,欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论