Electron应用长耗时挖矿函数执行时DOM无法实时更新问题
解决Electron挖矿函数阻塞UI、无法实时更新nonce的问题
这个问题我太熟了——本质是JavaScript单线程模型和Electron的进程分工导致的:你的挖矿函数是同步的密集计算任务,完全占用了渲染进程的主线程,Chromium根本没机会处理UI更新、用户点击这些事件,直到挖矿结束才会一次性把所有pending的DOM更新刷出来,所以页面会冻结,最后才看到结果。
下面给你两种落地性很强的解决方案,选哪种看你的需求:
方案1:用Web Workers把计算移到后台线程
适合纯前端计算的场景,不需要主进程介入,代码改动相对小。
步骤1:把挖矿逻辑抽成Worker文件
新建一个miner.worker.js文件,把挖矿的核心逻辑放进去,通过消息和渲染进程通信:
const crypto = require('crypto'); // 监听渲染进程发来的挖矿请求 self.onmessage = (e) => { const { index, time, prev, transactions, difficulty } = e.data; if (index !== 0) { self.postMessage({ type: 'log', message: `Mining Block ${index}` }); } const startTime = Date.now(); let nonce = 0; let hash = ''; while (hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')) { nonce++; hash = crypto .createHash('sha256') .update( index.toString() + time.toString() + prev.toString() + transactions.toString() + nonce.toString() ) .digest('hex') .toString(); // 别每次nonce++都发消息,太耗性能!每隔1000次更一次 if (nonce % 1000 === 0) { self.postMessage({ type: 'nonce', value: nonce }); } } // 最后发一次最终的nonce值 self.postMessage({ type: 'nonce', value: nonce }); // 计算性能和难度调整 const performanceTime = Date.now() - startTime; if (performanceTime <= 60000 && index !== 0) { self.postMessage({ type: 'difficulty', value: difficulty + 1 }); self.postMessage({ type: 'log', message: `Difficulty Increased. Difficulty Is Now ${difficulty + 1}` }); } const seconds = performanceTime / 1000; const performance = Math.floor((10 * nonce) / seconds) / 10000; let performanceText = 'Error'; if (index !== 0) { if (performance !== Infinity && performance >= 25) { performanceText = `${performance} kh/s`; self.postMessage({ type: 'log', message: `Performance: ${performance} kh/s` }); } else { self.postMessage({ type: 'log', message: `Performance Could Not Be Measured` }); } self.postMessage({ type: 'log', message: `Block ${index} Successfully Mined` }); } self.postMessage({ type: 'performance', value: performanceText }); self.postMessage({ type: 'done', nonce }); };
步骤2:在渲染进程里调用Worker
修改你原来的渲染进程代码,创建Worker并监听消息更新UI:
let minerWorker = null; function mineHandler(props) { // 防止重复点击启动多个挖矿任务 if (minerWorker) { minerWorker.terminate(); } minerWorker = new Worker('./miner.worker.js'); // 接收Worker发来的各种更新 minerWorker.onmessage = (e) => { const { type, value, message, nonce } = e.data; switch(type) { case 'nonce': $("#nonce").text(`Nonce: ${value}`); break; case 'performance': $("#performance").text(`Performance: ${value}`); break; case 'log': this.log(message); // 假设你的日志函数是this.log break; case 'difficulty': this.difficulty = value; break; case 'done': // 挖矿结束,清理Worker minerWorker.terminate(); minerWorker = null; break; } }; // 把挖矿参数传给Worker minerWorker.postMessage(props); } $("#miningBtn").click(mineHandler);
方案2:用Electron主进程处理挖矿任务
如果你的挖矿逻辑需要访问Node.js深层API或者系统资源,用主进程更合适——主进程是独立线程,完全不会阻塞渲染进程的UI。
步骤1:在主进程里添加IPC监听
修改你的main.js(主进程入口文件),添加挖矿的IPC处理:
const { ipcMain } = require('electron'); const crypto = require('crypto'); // 监听渲染进程的挖矿请求 ipcMain.handle('start-mining', async (event, props) => { const { index, time, prev, transactions, difficulty } = props; if (index !== 0) { // 给渲染进程发日志消息 event.sender.send('mining-log', `Mining Block ${index}`); } const startTime = Date.now(); let nonce = 0; let hash = ''; while (hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')) { nonce++; hash = crypto .createHash('sha256') .update( index.toString() + time.toString() + prev.toString() + transactions.toString() + nonce.toString() ) .digest('hex') .toString(); // 每隔1000次更新一次nonce if (nonce % 1000 === 0) { event.sender.send('mining-nonce-update', nonce); } } event.sender.send('mining-nonce-update', nonce); // 计算性能和难度调整 const performanceTime = Date.now() - startTime; let newDifficulty = difficulty; if (performanceTime <= 60000 && index !== 0) { newDifficulty = difficulty + 1; event.sender.send('mining-log', `Difficulty Increased. Difficulty Is Now ${newDifficulty}`); } const seconds = performanceTime / 1000; const performance = Math.floor((10 * nonce) / seconds) / 10000; let performanceText = 'Error'; if (index !== 0) { if (performance !== Infinity && performance >= 25) { performanceText = `${performance} kh/s`; event.sender.send('mining-log', `Performance: ${performance} kh/s`); } else { event.sender.send('mining-log', `Performance Could Not Be Measured`); } event.sender.send('mining-log', `Block ${index} Successfully Mined`); } event.sender.send('mining-performance-update', performanceText); // 返回最终结果给渲染进程 return { nonce, newDifficulty }; });
步骤2:在渲染进程里通过IPC调用主进程
修改渲染进程的代码,用ipcRenderer和主进程通信:
const { ipcRenderer } = require('electron'); let isMining = false; function mineHandler(props) { if (isMining) return; isMining = true; // 定义各个更新的监听函数 const handleNonceUpdate = (event, nonce) => { $("#nonce").text(`Nonce: ${nonce}`); }; const handleLog = (event, message) => { this.log(message); }; const handlePerformanceUpdate = (event, text) => { $("#performance").text(`Performance: ${text}`); }; // 注册监听 ipcRenderer.on('mining-nonce-update', handleNonceUpdate); ipcRenderer.on('mining-log', handleLog); ipcRenderer.on('mining-performance-update', handlePerformanceUpdate); // 调用主进程的挖矿任务 ipcRenderer.invoke('start-mining', props) .then(({ nonce, newDifficulty }) => { this.difficulty = newDifficulty; // 清理监听,避免内存泄漏 ipcRenderer.removeListener('mining-nonce-update', handleNonceUpdate); ipcRenderer.removeListener('mining-log', handleLog); ipcRenderer.removeListener('mining-performance-update', handlePerformanceUpdate); isMining = false; }) .catch((err) => { console.error('Mining failed:', err); // 出错也要清理监听 ipcRenderer.removeListener('mining-nonce-update', handleNonceUpdate); ipcRenderer.removeListener('mining-log', handleLog); ipcRenderer.removeListener('mining-performance-update', handlePerformanceUpdate); isMining = false; }); } $("#miningBtn").click(mineHandler);
关键注意事项
- 不要频繁发送消息:不管用Worker还是IPC,别每次nonce++都发更新,这样会把通信开销拉满,反而拖慢挖矿速度。我上面用的是每1000次发一次,你可以根据自己的需求调整这个数值。
- 避免重复任务:一定要加判断,防止用户多次点击按钮启动多个挖矿进程,浪费资源。
- 清理资源:Worker用完要terminate,IPC监听用完要removeListener,避免内存泄漏。
内容的提问来源于stack exchange,提问作者L. Rothman
相关产品推荐
相关产品推荐

