React.js中生成大体积PPT调用writeFile导致UI短暂卡顿及下载时机异常问题
问题根因
两个异常表现本质都是pptxgen.js的writeFile方法的执行逻辑导致的:
- 虽然
writeFile返回Promise,但它把所有幻灯片、资源打包成pptx二进制流的核心计算逻辑是同步运行在浏览器主线程的。120MB文件的打包计算量会直接阻塞主线程,所有UI渲染、状态更新都会被挂起,所以你会看到页面出现1秒左右的无响应卡顿。 - 加载动画和下载不同步是因为你把
setIsGenerating(false)写在了writeFile的then回调里:writeFile先执行完同步打包逻辑(这段时间主线程被阻塞,你之前调用的setIsGenerating(true)还没来得及被React渲染成加载动画就被卡住了),打包完成后立刻触发下载,同时才会执行then回调更新状态,等主线程空闲后React才会执行重渲染关闭加载动画,所以你会感知到下载比动画消失早1秒。
另外你说120MB体积不大但性能差,是因为浏览器主线程JS执行是单线程的,只要同步计算超过50ms用户就会感知到卡顿,打包120MB文件的计算量远超过这个阈值,和文件本身大小没有绝对关系。
解决方案
1. 快速修复(改少量代码即可见效)
首先调整状态更新时机,先保证加载动画渲染出来再执行计算逻辑,同时把无用的map调用换成forEach减少内存开销,另外不用等writeFile返回promise再关闭加载动画,打包完成触发下载后就可以直接关闭:
const createPPT = async () => { setIsGenerating(true) // 加setTimeout把后续逻辑放到下一个宏任务,等待React先渲染出加载动画再执行计算 setTimeout(async () => { let cart = JSON.parse(localStorage.getItem('cart')) || [] let pres = new pptxgen(); let newCart = JSON.parse(JSON.stringify(cart)) const imgList = [] await Promise.all(newCart.map(async (d) => { if (d.img !== "") { let result = await axios.get(d.img, { responseType: 'arraybuffer' }) // 优化:不用转base64,直接传二进制给pptxgen,减少转码开销 imgList.push(result.data) } })) // 原来的map没有用到返回值,换成forEach更合理 newCart.forEach((d, i) => { if (d.img !== "") { d.img = imgList[i] } }) newCart.forEach(d => { let slide = pres.addSlide() d.img && slide.addImage({ data: d.img, w: "100%", sizing: { type: "crop" } }); slide.addText(d.title, { x: '0', y: '0', w: '100%', h: '20%', align: 'center', valign: 'middle', color: "ffffff", }); slide.background = { color: "#000000" }; d.content.forEach(text => { let slide_2 = pres.addSlide(); d.img && slide_2.addImage({ data: d.img, w: "100%", sizing: { type: "crop" } }); slide_2.addText(text, { x: '0', y: '0', w: '100%', h: '20%', align: 'center', valign: 'middle', color: "ffffff", }) slide_2.background = { color: "#000000" }; }) }) pres.writeFile({ fileName: "Sample Presentation.pptx" }) // writeFile同步执行完成就已经触发下载,直接关闭加载动画即可 setIsGenerating(false) }, 0) }
2. 彻底解决卡顿
如果按上面的方法修改后还是有明显卡顿,可以把PPT生成、打包的全部逻辑放到Web Worker中执行,完全不占用主线程资源,就不会出现UI阻塞的问题:
- 新建独立的worker文件,引入pptxgen库,把图片拉取、幻灯片生成、打包逻辑都放在worker内部实现
- 主线程点击按钮后先开启加载动画,把cart数据发送给worker
- worker执行完所有逻辑后给主线程发送打包完成的消息,主线程收到消息后直接关闭加载动画即可。
内容的提问来源于stack exchange,提问作者David
相关产品推荐
相关产品推荐

