我们在用 Web3.js 写 DApp 的时候,最崩溃的瞬间是什么?是你看着页面转圈,用户点一下转账,整个界面就像被冻住一样,点哪里都没反应。更恐怖的是,链上数据一旦多起来,比如你监听一堆合约事件,或者频繁查询区块,这卡顿就会变成常态。要说根子上的原因,就是 JavaScript 是单线程的,Web3.js 的同步或者复杂异步操作把这条线程堵死了。今天我们就来聊聊这个问题,以及怎么用 Worker 线程把“路”拓宽,同时把那些容易踩的坑一个一个填平。

一、卡顿到底是怎么发生的

1.1 单线程的无奈

先想象一个画面。你正在厨房炒菜,只有一个煤气灶,你既要洗菜、切菜,又要盯着锅里别糊了。如果你在某一步上愣了神,比如盯着锅盖看了一分钟,那其他的菜就只能等着。JavaScript 的主线程就是这唯一的煤气灶,它要干的事有 UI 渲染、用户点击事件响应、网络请求回调、定时器等等。而 Web3.js 要做的事情往往是计算量大得吓人的,比如对一笔交易做哈希计算,对一大段 RLP 编码的数据进行解码,或者把一个区块里的几百笔交易全部解析一遍。这些活儿一旦在主线程上执行,其他所有事都得排队,于是界面就卡了。

1.2 Web3.js 里的“重量级操作”

很多人以为 Web3.js 都是异步的,异步就不会卡,其实不然。异步只是说你不必等待结果返回,但计算过程本身还是占用主线程的。比如:

  • 对一个私钥进行签名或者恢复地址的操作,涉及椭圆曲线算法,这个计算是纯 CPU 密集型的。
  • 解析合约事件日志,尤其是 Event 参数里的动态类型数据,需要一层层递归解码。
  • 对区块进行 whole block 的格式化,比如把十六进制变成十进制、把数字字符串变成 BigInt,数据量大时主线程也会喘不上气。

我们来模拟一个简单的例子,看看卡顿是什么样的。假设我们要监听某个合约的 Transfer 事件,然后给每个交易做一次近似“重放”的解析,这在主线程里做就会让页面失去响应。

// 技术栈:JavaScript + Web3.js

const Web3 = require('web3');
const web3 = new Web3('wss://mainnet.infura.io/ws/v3/你的项目ID');

// 这是我们要监听的某个USDT类似的合约地址
const contractAddress = '0xdAC17F958D2ee523a2206206994597C13D831ec7';
const abi = [{
  "anonymous": false,
  "inputs": [
    { "indexed": true, "name": "from", "type": "address" },
    { "indexed": true, "name": "to", "type": "address" },
    { "indexed": false, "name": "value", "type": "uint256" }
  ],
  "name": "Transfer",
  "type": "event"
}];

const contract = new web3.eth.Contract(abi, contractAddress);

// 监听实时的Transfer事件
contract.events.Transfer()
  .on('data', async (event) => {
    // 注意!这里有个隐形的雷,下面我们一步步拆解

    // 模拟一个耗时的解码操作:这里不仅解析当前事件,还回查这笔交易所在区块的所有交易
    const block = await web3.eth.getBlock(event.blockNumber, true);

    // 遍历区块里所有交易并做“伪验证”,纯粹为了占用CPU
    const results = block.transactions.map(tx => {
      // 模拟做一次复杂的哈希重算
      let hash = tx.hash;
      for (let i = 0; i < 1000; i++) {
        // 在真实场景里这可能是某种签名重放或者Merkle证明验证
        hash = Web3.utils.sha3(hash);
      }
      return hash;
    });

    console.log(`解析了 ${results.length} 笔交易`);

    // 如果事件来得多,主线程就被永无止境的CPU计算占满了
  });

看到没?这个 for 循环加上 sha3 的一千次重复计算,在主线程里足够把界面冻结好几秒。哪怕只是偶尔来一个事件,用户每点一次按钮就卡一下,体验就很糟糕了。

二、解决思路:把脏活累活丢给 Worker 线程

2.1 Worker 线程是什么

Worker 线程就是浏览器或者 Node.js 里提供的“第二厨房”。主线程可以开一个 Worker,把复杂计算任务交给它,然后主线程和 Worker 之间通过消息来通信,类似于你把菜递给隔壁的厨师,等他把菜做好再端回来。这个过程中主线程就能继续去响应点击事件、渲染动画了。

在浏览器里用的是 Web Worker,在 Node.js 里用的是 worker_threads。好消息是,Web3.js 在两者里都能用,只是加载方式有些细节不同。我们先拿 Node.js 环境来举例,因为 Node.js 里跑服务端 DApp 也会遇到同样的问题,而且调试起来更方便。

2.2 Worker 线程能直接使用 Web3.js 吗

这是大家最关心的问题。答案是“能用,但有个大坑”,所以这里先卖个关子。我们直接看一个比较粗糙的例子里会遇到什么问题。

// 技术栈:Node.js + worker_threads + Web3.js

const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  // ---------- 这是主线程 ----------
  const worker = new Worker(__filename);
  worker.on('message', (msg) => {
    console.log('主线程收到回复:', msg);
  });

  // 给worker发送一个任务:计算一个区块的解析结果
  worker.postMessage({ task: 'parseBlock', blockNumber: 18000000 });
} else {
  // ---------- 这是Worker线程 ----------
  const Web3 = require('web3');

  // 在Worker里直接new一个Web3实例连主网
  const web3 = new Web3('https://mainnet.infura.io/v3/你的项目ID');

  parentPort.on('message', async (data) => {
    if (data.task === 'parseBlock') {
      // 请求区块数据并进行解析
      const block = await web3.eth.getBlock(data.blockNumber, true);
      const total = block.transactions.length;
      const list = block.transactions.map(tx => tx.from + ' -> ' + tx.to);
      parentPort.postMessage({ result: list, count: total });
    }
  });
}

你可能会想,这不是挺简单的吗?直接把 Web3 搬进 Worker 里,主线程就轻松了。但这里面埋了至少三个雷:

第一,连接数爆炸。每个 Worker 里 new 一个 Web3,等于开了新的网络连接。如果你起十个 Worker,就会跟链上节点保持十条连接。如果你的 RPC 服务商限制了并发连接数,你很快会被封掉。

第二,数据序列化开销。主线程和 Worker 之间传递消息时,所有数据都要经过结构化克隆或者JSON序列化。如果你把整个区块对象从主线程挪到 Worker,再把解析结果挪回来,传输的字节数可能比计算本身还要大,最终卡没卡好,反而憋得更难受了。

第三,Web3.js 里有浏览器兼容性坑。在某些版本里,Web3.js 进入 Worker 环境后,window 未定义,一些依赖浏览器 API 的模块会加载失败。尤其是旧版本 Web3.js 或者某些插件,一进 Worker 就崩溃。

所以,正确的姿势不是简单地把 Web3 整个塞进 Worker,而是拆分任务。

三、正确的分工姿势:主线程管网络,Worker 管计算

3.1 数据从哪来

通常我们建议的做法是:主线程负责跟链上节点通信,通过 Web3.js 拿到原始数据,然后把原始数据作为消息传给 Worker;Worker 只做纯计算和格式化,算完再扔回主线程。为什么这样更合理?因为网络 I/O 本身并不怎么占用 CPU,主线程发出请求后是处于异步等待状态的,UI 照样能动。真正让界面卡住的是收到数据之后那一段难以忍受的计算。

当然,如果主线程频繁地拿到数据,每次拿到都立刻 postMessage 给 Worker,消息传输的序列化也会成为瓶颈。所以最好在 Worker 里做数据整理,但不要反复传输原始大对象。比如你只需要区块里的交易哈希列表,就别把整个区块对象(包含所有交易详细信息)传给 Worker,先在主线程里做一次简单的字段裁剪,再传过去。

3.2 一个完整的最佳实践示例

下面我们来构建一个实际可用的方案。假设我们有这样一个功能:订阅一个合约的 Swap 事件,每收到一个事件,就把包含这笔交易的那个区块里的所有交易做个统计,比如算一下在这个区块里,有多少交易是代币转账,有多少是合约调用,以及平均 gas 价格。这活儿挺耗 CPU,原因是需要解码每个交易的 input 数据。

我们会把数据处理的活丢给 Worker,而主线程只负责订阅和展示结果。

// 技术栈:Node.js + worker_threads + Web3.js
// 文件:main.js —— 主线程部分

const { Worker } = require('worker_threads');
const Web3 = require('web3');

// 在主线程创建 Web3,只做网络请求
const web3 = new Web3('wss://mainnet.infura.io/ws/v3/你的项目ID');

// 这里定义一个处理消息的Worker
const analysisWorker = new Worker('./analysisWorker.js');

// 存储待处理的任务,回调函数以messageId为key存起来
const pendingTasks = new Map();

// 统一发送任务给Worker,并返回Promise以支持async/await
function sendTaskToWorker(payload) {
  return new Promise((resolve, reject) => {
    const messageId = Date.now() + Math.random().toString(16).slice(2);
    pendingTasks.set(messageId, { resolve, reject });

    analysisWorker.postMessage({
      id: messageId,
      data: payload
    });
  });
}

// 监听Worker的返回消息
analysisWorker.on('message', (message) => {
  const task = pendingTasks.get(message.id);
  if (!task) return;

  pendingTasks.delete(message.id);

  if (message.error) {
    task.reject(new Error(message.error));
  } else {
    task.resolve(message.result);
  }
});

// 模拟合约的Swap事件监听(这里用轮询代替,避免篇幅过长)
async function watchSwapEvents() {
  // 这里我们用一个简单的轮询来模拟实时事件
  let lastBlock = await web3.eth.getBlockNumber();

  setInterval(async () => {
    const currentBlock = await web3.eth.getBlockNumber();
    if (currentBlock === lastBlock) return;

    for (let blockNumber = lastBlock + 1; blockNumber <= currentBlock; blockNumber++) {
      // 主线程只做一件事:拿到区块的原始数据
      const block = await web3.eth.getBlock(blockNumber, true);

      // 裁剪数据,只保留必要字段,避免把巨大的原始对象发给Worker
      const simplified = {
        number: block.number,
        transactions: block.transactions.map(tx => ({
          from: tx.from,
          to: tx.to,
          gasPrice: tx.gasPrice,
          input: tx.input,
          value: tx.value
        }))
      };

      // 把解析任务丢给Worker
      const analysis = await sendTaskToWorker({
        type: 'analyzeBlock',
        block: simplified
      });

      // 拿到结果展示在主线程,完全不卡
      console.log(`区块 #${blockNumber} 分析结果:`);
      console.log(analysis);
    }

    lastBlock = currentBlock;
  }, 5000);
}

watchSwapEvents().catch(console.error);

上面的代码里,主线程每 5 秒检查一次新区块,拿到区块数据后,把区块裁剪成只有几个字段的轻量对象,然后发给 Worker。这样主线程本身除了网络请求和消息收发,不做任何繁重的计算。

下面是 Worker 的代码:

// 技术栈:Node.js + worker_threads
// 文件:analysisWorker.js —— Worker线程部分

const { parentPort } = require('worker_threads');
const Web3 = require('web3');

// 在Worker里我们只使用Web3的工具函数,不发起网络请求
// 因此这里不需要连接RPC,只是用来做数据解码等纯计算
// 注意:这样使用并不会产生连接,只利用了它的本地算法能力
const { utils } = Web3;

// 用来识别一个交易是不是代币转账(ERC20)
function isTokenTransfer(tx) {
  // 如果to字段为null,说明是合约创建
  if (!tx.to) return false;

  // 通过交易input来简单判断:
  // ERC20的transfer函数签名是 0xa9059cbb
  // 如果input以这个开头,大概率就是代币转账交易
  if (tx.input && tx.input.startsWith('0xa9059cbb')) {
    return true;
  }

  return false;
}

// 统计一段bytecode中的函数调用数量
function analyzeTransactions(transactions) {
  let tokenTransferCount = 0;
  let contractCallCount = 0;
  let totalGasPrice = 0n;

  for (const tx of transactions) {
    // 模拟一个复杂的计算:对每个交易做500次哈希,模拟CPU密集工作
    // 在实际项目中你一定有合理的复杂计算场景,这里只是体现压力
    let temporaryHash = tx.input;
    for (let i = 0; i < 500; i++) {
      // sha3是一种相对开销较大的哈希算法,特别是在大数据量时
      temporaryHash = utils.sha3(temporaryHash) || '0x';
    }

    // 这个循环只是为了让CPU累一点,不考究具体逻辑是否科学
    if (isTokenTransfer(tx)) {
      tokenTransferCount++;
    }

    if (tx.input && tx.input !== '0x') {
      contractCallCount++;
    }

    totalGasPrice += BigInt(tx.gasPrice || 0);
  }

  return {
    tokenTransferCount,
    contractCallCount,
    avgGasPrice: (totalGasPrice / BigInt(transactions.length || 1)).toString()
  };
}

// 监听主线程发来的消息
parentPort.on('message', (message) => {
  const { id, data } = message;

  try {
    let result;

    if (data.type === 'analyzeBlock') {
      // 在Worker里做繁重的数据分析和解码
      result = analyzeTransactions(data.block.transactions);
    }

    // 把结果回传给主线程
    parentPort.postMessage({
      id: id,
      result: result
    });
  } catch (error) {
    parentPort.postMessage({
      id: id,
      error: error.message || String(error)
    });
  }
});

这里 Worker 没连接任何节点,只用了 Web3.utils 来做哈希,完全够了。这样主线程不会卡,Worker 也不会跟 RPC 节点抢连接数。

四、Worker 线程使用中的关键注意事项

4.1 注意 Worker 数量

有的人一听说 Worker 能解决卡顿,就啪的一下开十几个 Worker,结果内存暴涨,CPU 被占得比原来还满。同时跑太多 Worker 也会导致线程切换太频繁,得不偿失。对于大多数 DApp 应用,一个到两个 Worker 就足够。如果任务可以分成两拨,比如一拨处理链上事件,一拨处理签名计算,就用两个。但如果任务性质差不多,用一个 Worker 就做好了。

4.2 注意消息传递的带宽

前面提到了传输开销。我们再来一个真实对比。如果你的区块里有一笔交易,input 数据特别长,比如一个几十 KB 的合约调用数据,你直接整个扔给 Worker,结构化克隆要把这几十 KB 复制一遍。如果来个 100 笔交易的区块,那一次消息可能就几 MB 了。这期间主线程虽然不用计算,但序列化和复制操作也会占用一部分主线程时间。所以,在发送前先想清楚,Worker 里到底需要哪些字段

4.3 注意 Web3.js 在 Worker 里的正确引入方式

在浏览器环境中,有些 Web3.js 版本直接通过 new Worker('worker.js') 创建 Worker,然后在 Worker 里 importScripts('web3.min.js'),这样的全局引入可能会出问题。推荐用模块化方式,或者在 Node.js 环境下用打包工具把 Worker 脚本打成独立文件。这里给出一个浏览器端的简版示例思路:

<!-- 技术栈:浏览器原生 Worker + Web3.js 打包文件 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>浏览器Worker处理链上数据</title>
</head>
<body>
  <script>
    // 主线程代码
    const worker = new Worker('worker.js');

    worker.onmessage = (event) => {
      console.log('从Worker拿到的解码结果:', event.data);
    };

    // 当我们收到一个链上事件时,把原始日志发给Worker
    function handleEventLog(rawLog) {
      // rawLog就是链上原始event数据,比如event.raw
      worker.postMessage(rawLog);
    }

    // 模拟一次事件日志
    handleEventLog({
      address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
      topics: [
        '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
        '0x000000000000000000000012b3a3e1e0ce2e1e0ce2e1e0ce2e1e0ce2e1e0ce2',
        '0x0000000000000000000012a3b3e1e0ce2e1e0ce2e1e0ce2e1e0ce2e1e0ce2'
      ],
      data: '0x0000000000000000000000000000000000000000000000000000000000000064'
    });
  </script>
</body>
</html>

对应 worker.js 里,你用 importScripts 引入 web3.min.js,然后处理消息:

// 技术栈:浏览器端Worker + Web3.js
// 文件:worker.js

// 引入Web3打包文件,注意这里路径要自己调整
importScripts('./lib/web3.min.js');

// 使用全局的Web3
const { utils } = Web3;

self.onmessage = (event) => {
  const rawLog = event.data;

  try {
    // 解析合约事件日志
    const decoded = {
      from: '0x' + rawLog.topics[1].slice(26),
      to: '0x' + rawLog.topics[2].slice(26),
      value: utils.hexToNumberString(rawLog.data)
    };

    // 这里我们可以做更复杂的事,比如Merkle验证等等
    // 目前只做一个简化的解码,示意为主

    self.postMessage(decoded);
  } catch (e) {
    self.postMessage({ error: e.message });
  }
};

这个浏览器端的例子,大家注意 importScripts 方式是同步加载脚本的,如果 web3.min.js 比较大,Worker 首次启动时会稍慢一点,但之后就不阻塞主线程了。

4.4 注意错误处理与超时控制

Worker 因为跑的是异步代码,如果你发给它的任务永远不回来,主线程的 Promise 就会一直挂着,用户界面虽然不卡,但功能就等于是坏了。因此,给每一个发给 Worker 的任务设置超时是很重要的。我们可以在 sendTaskToWorker 函数里加一个定时器来兜底。

// 技术栈:Node.js

function sendTaskToWorker(payload, timeout = 30000) {
  return new Promise((resolve, reject) => {
    const messageId = Date.now() + Math.random().toString(16).slice(2);

    // 设置超时计时器
    const timer = setTimeout(() => {
      pendingTasks.delete(messageId);
      reject(new Error('Worker处理任务超时'));
    }, timeout);

    // 在pendingTasks里存储时带着timer,以便消息返回后清除计时器
    pendingTasks.set(messageId, {
      resolve: (value) => {
        clearTimeout(timer);
        resolve(value);
      },
      reject: (error) => {
        clearTimeout(timer);
        reject(error);
      }
    });

    analysisWorker.postMessage({ id: messageId, data: payload });
  });
}

4.5 注意 Worker 里的异步任务排队

如果你把所有事件直接发给某一个 Worker,而 Worker 同时只能处理一条消息,那事件哐哐哐来了,Worker 处理不过来,消息就会在队列里堆积。这个问题很多人会忽略。表面上看主线程不卡了,但 Worker 变成了新的瓶颈。

解决方案是引入一个任务队列管理器。在主线程里维护一个 FIFO 队列,只有等当前 Worker 的回包到了之后,再从队列里取下一个任务发出去。下面是一个简化版:

// 技术栈:Node.js

class WorkerQueue {
  constructor(worker) {
    this.worker = worker;
    this.taskQueue = [];       // 待发送的任务
    this.isBusy = false;       // Worker是否空闲
    this.callbacks = new Map(); // 回调映射

    worker.on('message', (message) => {
      const { id, result, error } = message;

      const callback = this.callbacks.get(id);
      if (callback) {
        this.callbacks.delete(id);
        error ? callback.reject(new Error(error)) : callback.resolve(result);
      }

      // 处理完一条,看看队列里还有没有任务
      this.isBusy = false;
      this._nextTick();
    });
  }

  // 添加任务
  addTask(data) {
    return new Promise((resolve, reject) => {
      this.taskQueue.push({ data, resolve, reject });
      this._nextTick();
    });
  }

  // 如果当前没有在处理任务,则从队列里取出一个发送给Worker
  _nextTick() {
    if (this.isBusy || this.taskQueue.length === 0) return;

    const task = this.taskQueue.shift();
    this.isBusy = true;

    const id = Date.now() + Math.random().toString(16).slice(2);
    this.callbacks.set(id, task);

    this.worker.postMessage({ id, data: task.data });
  }
}

// 使用方式示例:
// const queue = new WorkerQueue(analysisWorker);
// const result = await queue.addTask({ type: 'analyzeBlock', block: simplified });

有了队列之后,Worker 永远只会处理一个任务,消息排队都在主线程里,不会无限堆积 Worker 的消息缓冲。

五、应用场景与方案优缺点

5.1 什么时候必须用 Worker

如果你的 DApp 在交互中经常做以下事情,那基本属于必用 Worker 的场景:

  • 批量查询区块并计算一些聚合指标,比如统计某地址的历史交易总额。
  • 对合约交易的 calldata 进行深度解码,特别是涉及嵌套的 struct、数组里面套数组。
  • 频繁地进行本地签名操作,比如离线签名批量交易,用助记词批量生成地址。
  • 监听大量链上事件,并对事件日志做复杂的规则过滤或动态计算。

5.2 Worker 方案的优点

把计算丢到 Worker 之后,最直观的好处就是 DApp 界面终于能保持流畅了。用户点击按钮想转账,页面能立刻响应,输入框的焦点不会失焦,动画也不会一卡一卡的。同时主线程的崩溃风险也降低了,就算 Worker 里跑的任务抛了异常,也不会影响页面的基本渲染,实现了一定程度的隔离。

5.3 Worker 方案的缺点

缺点也不能装看不见。首先是代码复杂度上来了,以前就是一个回调,现在要搞主线程、worker、消息协议、队列管理,调试起来难度也大了。其次,数据传输的序列化开销在某些极端场景下会被放大,特别是如果数据是一段极大的 hex 字符串,那复制成本是避不开的。第三,Worker 环境里没有窗口对象,也没有 DOM,Web3.js 里如果有些模块是依赖浏览器环境的(比如某些需要调用 window.ethereum 的注入函数),就不能在 Worker 里用。所以你要很小心,把跟钱包交互的代码留在主线程,不能一股脑全搬进去。

5.4 一种另类的替代方案

其实还有一种思路是,不用 Web3.js 自带的解码工具,而是用更加轻量的库,像是 ethers.jsInterface 或者 @ethersproject/abi,单独拎出来放到 Worker 里做解码。这类库比 Web3.js 更小、更专注,而且不依赖网络环境。如果你只需要解码 ABI 数据,完全可以用这些轻量库。这里我们稍微演示一下怎么用 ethers 的 ABI 工具在 Worker 里做一些解码,也算是一种关联技术的介绍。

// 技术栈:Node.js + Worker线程 + ethers.js(ABI相关工具)

const { parentPort } = require('worker_threads');
const { ethers } = require('ethers');

// 定义我们想要的ERC20 Transfer事件的ABI结构
const transferAbi = [
  'event Transfer(address indexed from, address indexed to, uint256 value)'
];

// 创建Interface对象
const iface = new ethers.Interface(transferAbi);

parentPort.on('message', (message) => {
  const { id, data } = message;

  try {
    // data里包含事件日志的topics和data
    // 使用ethers的parseLog方法解码,比Web3.utils更轻更快
    const decoded = iface.parseLog({
      topics: data.topics,
      data: data.data
    });

    const result = {
      from: decoded.args.from,
      to: decoded.args.to,
      value: decoded.args.value.toString()
    };

    parentPort.postMessage({ id, result });
  } catch (error) {
    parentPort.postMessage({ id, error: error.message });
  }
});

这个示例告诉我们,如果任务只是解码 ABI 数据,不一定非要在 Worker 里塞下一个完整的 Web3.js,按需引入更细分的技术库反而更舒服。

六、一些额外的小提醒

6.1 不要忘了关闭 Worker

如果你的 DApp 生命周期比较长,在单页应用里经常需要切换页面,Worker 如果创建了不销毁,会一直被垃圾回收不了。在 React 或者 Vue 项目的组件卸载时,记得调用 worker.terminate() 来结束 worker。否则后台线程持续占用资源,时间长了整个应用会变得迟钝。

6.2 确保消息版本一致

业务升级时,主线程发的数据结构可能跟 Worker 里期待的不一致,如果你没有做兼容,Worker 解析就会出现问题。建议定义好消息协议,比如固定的 typeversion 字段,Worker 里校验版本号,避免静默出错。

6.3 消息不要串线

如果你用同一个 Worker 处理多种任务,务必带上唯一的任务 ID 和任务类型。我们的示例里已经做了这个设计,但还是要强调:回调映射里如果缺少 messageId,就很容易出现多个请求互相覆盖结果的情况,尤其是当前一个请求还没回来,后一个请求已经到了的时候。

七、文章总结

说到底,Web3.js 阻塞 DApp 不是一个无法解决的难题。第一步要先意识到哪些操作是 CPU 密集的,不能因为它们是“异步的”就觉得万事大吉。第二步是把网络 I/O 和计算解耦,主线程负责跟节点说话,Worker 负责算到头秃。第三步是别让自己创造的复杂度超越原始问题,控制好 Worker 数量、任务队列和消息体大小。Worker 不是万能药,但对于链上数据解析、地址生成、事件解码这些又重又脏的活计,它实在是一剂对症的良药。我希望你读完这篇文章后,能回头审视一下自己的 DApp,把那些让用户抓狂的大计算量操作统统挪到 Worker 线程里去,让你的 DApp 在重压下依然操作如飞。