You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native中使用AWS SDK上传大文件如何避免UI阻塞?

React Native 大文件上传S3避免UI阻塞解决方案

问题分析

你遇到的UI冻结问题,核心原因是AWS SDK的Upload类在React Native环境中,会将分块处理、并行上传的逻辑放在主线程执行,大文件多并发场景下直接占用主线程资源,导致UI无法响应。而单纯读取分块或调用其他API时,逻辑没有持续占用主线程,所以不会阻塞。

解决方案

1. 用InteractionManager将上传逻辑移至后台队列

React Native的InteractionManager会延迟执行任务,直到所有UI交互完成,或把任务放到非UI优先级的队列,避免抢占主线程资源。

修改代码如下:

import { InteractionManager } from 'react-native';

const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB - AWS MultiPart Min Size

const fileStreaming = async (uri) => {
  try {
    const resp = await fetch(uri);
    const reader = resp.body;

    // 将上传逻辑包裹在runAfterInteractions中
    await InteractionManager.runAfterInteractions(async () => {
      const parallelUpload = new Upload({
        client: client,
        params: {
          Bucket: 'example',
          Key: 'file-stream-test-2.mp4',
          Body: reader,
          ContentType: 'video/mp4'
        },
        queueSize: 2, // 降低并行数,减少主线程负载(原4改为2)
        partSize: CHUNK_SIZE,
        leavePartsOnError: false
      });

      parallelUpload.on("httpUploadProgress", (progress) => {
        console.log(progress);
        // 若需更新UI,直接用setState即可(React会自动回到主线程)
      });

      const completeUpload = await parallelUpload.done();
      console.log(completeUpload);
    });
    
  } catch(err) {
    console.error(err);
  }
}

2. 自定义Multipart上传,手动控制并发

放弃SDK的Upload封装,手动实现分块上传,完全控制并发数和执行时机,进一步降低主线程压力。需要借助react-native-fs读取本地文件:

首先安装依赖:

npm install react-native-fs --save
# 或 yarn add react-native-fs

实现代码:

import RNFS from 'react-native-fs';
import { InteractionManager } from 'react-native';

const CHUNK_SIZE = 5 * 1024 * 1024;

const fileStreaming = async (uri) => {
  let uploadId = null;
  try {
    const fileStat = await RNFS.stat(uri);
    const totalParts = Math.ceil(fileStat.size / CHUNK_SIZE);

    await InteractionManager.runAfterInteractions(async () => {
      // 初始化Multipart上传
      const initResp = await client.createMultipartUpload({
        Bucket: 'example',
        Key: 'file-stream-test-2.mp4',
        ContentType: 'video/mp4'
      }).promise();
      uploadId = initResp.UploadId;

      const partPromises = [];
      const maxConcurrent = 2; // 控制同时上传的分块数

      for (let partNum = 1; partNum <= totalParts; partNum++) {
        const start = (partNum - 1) * CHUNK_SIZE;
        const end = Math.min(start + CHUNK_SIZE - 1, fileStat.size - 1);

        // 读取文件分块
        const chunkBase64 = await RNFS.read(uri, CHUNK_SIZE, start, 'base64');
        const chunkBuffer = Buffer.from(chunkBase64, 'base64');

        // 上传分块
        const uploadPromise = client.uploadPart({
          Bucket: 'example',
          Key: 'file-stream-test-2.mp4',
          PartNumber: partNum,
          UploadId: uploadId,
          Body: chunkBuffer
        }).promise().then(resp => ({
          PartNumber: partNum,
          ETag: resp.ETag
        }));

        partPromises.push(uploadPromise);

        // 达到并发上限时,等待当前批次完成
        if (partPromises.length >= maxConcurrent) {
          await Promise.all(partPromises.splice(0, maxConcurrent));
        }
      }

      // 处理剩余分块
      if (partPromises.length > 0) {
        await Promise.all(partPromises);
      }

      // 完成Multipart上传
      const completeResp = await client.completeMultipartUpload({
        Bucket: 'example',
        Key: 'file-stream-test-2.mp4',
        UploadId: uploadId,
        MultipartUpload: { Parts: partPromises }
      }).promise();

      console.log('上传完成:', completeResp);
    });
    
  } catch(err) {
    console.error('上传失败:', err);
    // 出错时中止未完成的Multipart上传,避免存储冗余分块
    if (uploadId) {
      await client.abortMultipartUpload({
        Bucket: 'example',
        Key: 'file-stream-test-2.mp4',
        UploadId: uploadId
      }).promise().catch(() => {});
    }
  }
}

3. 使用Worker线程彻底隔离上传逻辑(React Native 0.60+)

将上传逻辑放入独立的Worker线程,完全脱离主线程,彻底避免UI阻塞。

步骤1:创建Worker文件(upload.worker.js)

import { S3Client, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand } from "@aws-sdk/client-s3";

// 初始化S3客户端(注意:Worker中无法直接访问RN的环境变量,需传入配置)
const s3Client = new S3Client({
  region: '你的区域',
  credentials: {
    accessKeyId: '你的AccessKey',
    secretAccessKey: '你的SecretKey'
  }
});

self.onmessage = async (e) => {
  const { chunkSize, bucket, key, fileChunks } = e.data;
  let uploadId = null;

  try {
    // 初始化Multipart上传
    const initResp = await s3Client.send(new CreateMultipartUploadCommand({
      Bucket: bucket,
      Key: key,
      ContentType: 'video/mp4'
    }));
    uploadId = initResp.UploadId;

    const partResults = [];
    const maxConcurrent = 2;

    for (let i = 0; i < fileChunks.length; i++) {
      const partNum = i + 1;
      const chunkBuffer = fileChunks[i];

      const uploadPromise = s3Client.send(new UploadPartCommand({
        Bucket: bucket,
        Key: key,
        PartNumber: partNum,
        UploadId: uploadId,
        Body: chunkBuffer
      })).then(resp => ({
        PartNumber: partNum,
        ETag: resp.ETag
      }));

      partResults.push(uploadPromise);

      if (partResults.length >= maxConcurrent) {
        await Promise.all(partResults.splice(0, maxConcurrent));
      }

      // 发送进度给主线程
      self.postMessage({
        type: 'progress',
        progress: ((i + 1) / fileChunks.length) * 100
      });
    }

    if (partResults.length > 0) {
      await Promise.all(partResults);
    }

    // 完成上传
    const completeResp = await s3Client.send(new CompleteMultipartUploadCommand({
      Bucket: bucket,
      Key: key,
      UploadId: uploadId,
      MultipartUpload: { Parts: partResults }
    }));

    self.postMessage({ type: 'complete', data: completeResp });
  } catch (err) {
    self.postMessage({ type: 'error', message: err.message });
    if (uploadId) {
      await s3Client.send(new AbortMultipartUploadCommand({
        Bucket: bucket,
        Key: key,
        UploadId: uploadId
      })).catch(() => {});
    }
  } finally {
    self.close();
  }
};

步骤2:主线程调用Worker

import { Worker } from 'react-native';
import RNFS from 'react-native-fs';
import { InteractionManager } from 'react-native';

const CHUNK_SIZE = 5 * 1024 * 1024;

const fileStreaming = async (uri) => {
  try {
    const fileStat = await RNFS.stat(uri);
    const totalParts = Math.ceil(fileStat.size / CHUNK_SIZE);
    const fileChunks = [];

    // 先在主线程读取所有分块(或分批读取传给Worker)
    await InteractionManager.runAfterInteractions(async () => {
      for (let i = 0; i < totalParts; i++) {
        const start = i * CHUNK_SIZE;
        const end = Math.min(start + CHUNK_SIZE - 1, fileStat.size - 1);
        const chunkBase64 = await RNFS.read(uri, CHUNK_SIZE, start, 'base64');
        fileChunks.push(Buffer.from(chunkBase64, 'base64'));
      }

      // 创建Worker并传递数据
      const uploadWorker = new Worker('./upload.worker.js');
      uploadWorker.postMessage({
        chunkSize: CHUNK_SIZE,
        bucket: 'example',
        key: 'file-stream-test-2.mp4',
        fileChunks
      });

      uploadWorker.onmessage = (e) => {
        switch(e.data.type) {
          case 'progress':
            console.log(`上传进度: ${e.data.progress.toFixed(2)}%`);
            break;
          case 'complete':
            console.log('上传完成:', e.data.data);
            uploadWorker.terminate();
            break;
          case 'error':
            console.error('上传失败:', e.data.message);
            uploadWorker.terminate();
            break;
        }
      };
    });
  } catch (err) {
    console.error('读取文件失败:', err);
  }
}

方案选择建议

  • 优先尝试方案1,改动最小,能解决大部分场景的UI阻塞问题;
  • 若方案1效果不佳,使用方案2,手动控制并发更灵活;
  • 超大型文件(500MB+)或极端场景,用方案3彻底隔离主线程,但需注意Worker的资源开销和文件读取的内存占用。

内容的提问来源于stack exchange,提问作者VTLLY Coding

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 16:34:56