H5 Web Worker 计算卸载实践
H5 Web Worker 计算卸载实践
当数据处理需要长时间占用主线程时,页面会出现无法点击、动画卡顿和输入延迟。Web Worker 允许把纯计算任务放到独立线程执行,让主线程继续响应用户。但 Worker 不是万能提速器,它也有启动成本、通信成本和兼容限制。
一、什么任务适合 Worker
适合 Worker 的任务通常计算密集、输入输出数据量可控、不需要直接操作 DOM。
- 大数组过滤、排序和聚合。
- 复杂文本处理。
- 文件内容解析。
- 图像像素批量处理。
- 加密或压缩计算。
如果任务本身只需要几毫秒,放到 Worker 反而可能因为通信开销变得更慢。先在主线程测量真实耗时,再决定是否拆分。
二、主线程与 Worker 通信
主线程通过消息把任务发给 Worker,Worker 处理完成后回传结果。
const worker = new Worker('/workers/data-process.js');
worker.onmessage = (event) => {
renderResult(event.data);
};
worker.postMessage({
type: 'aggregate',
payload: rows
});
消息内容应是可结构化克隆的数据。不能直接传递函数、DOM 节点或包含复杂引用的业务对象。
三、模块化 Worker 代码
简单场景可以使用单独脚本文件。如果项目支持模块 Worker,可以复用现有类型和纯函数。
import type { OrderRow } from '../types/order';
self.onmessage = (event) => {
if (event.data.type !== 'aggregate') return;
const result = aggregateOrders(event.data.payload);
self.postMessage({ type: 'aggregate', payload: result });
};
function aggregateOrders(rows: OrderRow[]): number {
return rows.reduce(
(total, row) => total + row.priceInCent,
0
);
}
Worker 内部应保持纯计算,不发业务请求、不操作页面状态,让任务边界清晰。
四、消息协议要明确
一个 Worker 可能处理多种任务。消息应包含类型和任务编号,便于匹配响应。
type WorkerRequest =
| { type: 'aggregate'; taskId: number; payload: OrderRow[] }
| { type: 'cancel'; taskId: number };
type WorkerResponse =
| { type: 'success'; taskId: number; payload: number }
| { type: 'error'; taskId: number; message: string };
任务编号能解决响应错序问题,也能在任务取消时忽略过期结果。
五、取消任务
用户切换筛选条件后,上一次任务的结果可能已经没有意义。Worker 内部应支持中断计算。
let activeTaskId = 0;
self.onmessage = (event) => {
if (event.data.type === 'cancel') {
activeTaskId = 0;
return;
}
activeTaskId = event.data.taskId;
const result = processRows(event.data.payload);
if (activeTaskId === event.data.taskId) {
self.postMessage({
type: 'success',
taskId: event.data.taskId,
payload: result
});
}
};
取消只是设置状态,不会强杀正在执行的代码。长循环内部应定期检查任务是否已过期。
六、传递大数据用可转移对象
普通消息传递会产生数据克隆。处理大数组时,可以使用可转移对象减少复制成本。
worker.postMessage(
{ type: 'process', buffer },
[buffer]
);
Buffer 被转移后,原线程不能再访问它。业务代码要明确所有权转移时机,避免出现读取已转移数据的错误。
七、Worker 的启动成本
创建 Worker 需要下载脚本并初始化环境。如果任务很小且只在特定操作后触发,可以在触发前预创建 Worker,或复用已创建实例。
let worker: Worker | undefined;
function getWorker(): Worker {
if (!worker) {
worker = new Worker('/workers/data-process.js');
}
return worker;
}
长期空闲的 Worker 也可以在合适时机终止,释放资源。是否预创建和常驻,应根据任务频率和页面目标决定。
八、错误处理
Worker 内部抛出的异常不会自动出现在主线程调用栈中,需要显式处理。
worker.onerror = (event) => {
tracker.reportWorkerError(event.message);
showError();
};
主线程发消息后应有超时或失败处理,不能让界面永远停留在“处理中”状态。
九、不要让 Worker 变成第二个应用
Worker 适合承接计算,不适合承接业务流程。如果把状态管理、请求、页面跳转逻辑都放进 Worker,主线程和 Worker 之间的消息会变成隐式调用链,排查困难。
主线程:交互、状态、请求、渲染
Worker:纯计算、数据转换
边界保持简单,才能发挥并发优势,同时不增加架构复杂度。
十、降级策略
如果项目最低兼容范围包含不支持 Worker 的浏览器,需要提供降级路径,例如在主线程按批次处理。
if (typeof Worker === 'undefined') {
const result = processInMainThread(rows);
renderResult(result);
}
降级方案应明确执行方式,不能默认所有环境都具备 Worker 能力。
总结
Web Worker 的价值在于把真正耗时的纯计算移出主线程。使用前先测量任务耗时,设计明确的消息协议和任务编号,支持取消和错误处理。大数据传输关注结构化克隆和可转移对象,任务频繁时考虑复用实例。保持 Worker 只做计算,不把它变成另一个业务容器,才能真正提升页面响应性。
- 点赞
- 收藏
- 关注作者
评论(0)