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
相关产品推荐
相关产品推荐

