Electron加载大量FontFace时,用Worker处理Promise是否合理?
核心问题:用Worker处理大量FontFace加载Promise是否合理?
一、先搞懂为什么大量Promise会让主进程卡顿
Promise确实是异步的,但它的then/catch回调都是跑在主事件循环里的。当数万个字体加载的Promise同时完成(不管成功还是失败),海量回调会挤爆主进程的事件队列——主进程既要处理这些回调,还要管UI渲染、用户点击这些核心任务,卡顿自然就来了。另外你代码里的document.fonts.add(ff)是同步操作,大量执行这类操作也会直接阻塞主线程。
二、用Worker处理的合理性完全成立
这么做不仅合理,还非常对症,原因有三个:
- 给主进程减负:把FontFace创建、加载的逻辑全丢去Worker,主进程只负责接收结果,不用再被海量回调占满事件循环,UI自然流畅。
- 错误隔离:单个字体加载失败的错误处理在Worker里执行,不会污染主进程的事件队列,避免一个错误拖垮整个渲染流程。
- 并行潜力大:可以根据CPU核心数创建多个Worker,把任务拆分并行处理,效率能再上一个台阶。
不过你现在的代码有几个硬伤得改:
- Worker里没有DOM环境,
document.fonts.add(ff)会直接报错,必须把加载成功的字体信息传回主进程,在主进程里执行添加操作。 - 循环里逐个
postMessage会造成消息队列拥堵,建议批量发任务或者控制并发数。 - 主进程没监听Worker的返回消息和错误,等于白忙活。
三、优化后的代码示例
主进程(renderer.js)
myAPI.onFontListReady(jsonFontList => { const worker = new Worker('worker.js'); // 接收Worker返回的加载结果 worker.addEventListener('message', (e) => { const [objFontData, errorMsg] = e.data; if (errorMsg) { console.error(`字体加载失败:${objFontData.newFileName}`, errorMsg); return; } // 主进程中执行字体添加操作 const ff = new FontFace(`_${objFontData.newFileName}`, `url('${objFontData.newFilePath.replace(/\\/g, '/')}')`); document.fonts.add(ff); }); // 监听Worker自身的错误 worker.addEventListener('error', (err) => { console.error('Worker运行出错:', err.message); }); // 批量发送任务,避免消息队列过载 const batchSize = 50; for (let i = 0; i < jsonFontList.length; i += batchSize) { const batch = jsonFontList.slice(i, i + batchSize); worker.postMessage({ command: 'batch-load', fontBatch: batch }); } });
Worker进程(worker.js)
addEventListener('message', async (message) => { if (message.data.command === 'batch-load') { await processFontBatch(message.data.fontBatch); } }); async function processFontBatch(fontBatch) { // 控制并发数,避免同时发起太多加载请求耗尽资源 const concurrencyLimit = 10; const results = []; // 分块处理,每块控制并发数 for (let i = 0; i < fontBatch.length; i += concurrencyLimit) { const chunk = fontBatch.slice(i, i + concurrencyLimit); const chunkPromises = chunk.map(async (objFontData) => { try { const ff = new FontFace(`_${objFontData.newFileName}`, `url('${objFontData.newFilePath.replace(/\\/g, '/')}')`); await ff.load(); return [objFontData, null]; } catch (error) { console.error(`加载字体出错:${objFontData.newFileName}`, error); return [objFontData, error.message]; } }); const chunkResults = await Promise.all(chunkPromises); results.push(...chunkResults); } // 逐个返回结果(也可以批量返回,进一步减少通信次数) results.forEach(result => postMessage(result)); }
四、额外优化技巧
- 动态调整并发数:可以根据当前系统资源(比如CPU使用率)调整Worker数量和单Worker内的并发加载数。
- 用Transferable Objects传递大资源:如果是直接读取字体文件的ArrayBuffer,可以用Transferable方式传递,减少内存拷贝开销。
- 任务队列调度:如果字体数量特别大,Worker可以维护一个任务队列,逐步处理,避免一次性加载压力过大。
内容的提问来源于stack exchange,提问作者oldboy
相关产品推荐
相关产品推荐

