JavaScript 多线程:Web Worker 其实很简单
JavaScript 在诞生之初主要用于浏览器 DOM 操作与用户交互。设想这样一个场景:多个线程同时操作同一个 DOM 节点,一个线程执行删除、另一个线程执行修改,浏览器将无法确定最终应该采用哪一份结果。为了规避 DOM 并发竞争冲突,JavaScript 主线程被设计为单线程模型,所有业务代码都在主线程按顺序串行执行。
JavaScript 语言本身是单线程,但浏览器、Node.js 运行环境提供了多线程能力:
- 浏览器:
Web Worker,在浏览器内部开辟独立后台线程; - Node.js:
Worker Threads,服务端实现并行计算。
Chrome 浏览器进程与线程模型
Chrome 采用多进程隔离架构,不同职责运行在独立进程,单个进程崩溃不会造成整个浏览器闪退。
浏览器主进程
浏览器顶层总控进程,不负责网页渲染,管理全部标签页整体行为。
- UI 线程:绘制浏览器本身界面,地址栏、书签、前进后退按钮,响应用户点击输入;
- 存储线程:读写
localStorage、IndexedDB、Cookie、文件系统; - IPC 进程通信线程:负责浏览器各个进程之间消息分发;
- 文件 IO 线程:处理本地文件读写操作。
渲染进程(Renderer 进程)
每一个网页 Tab 或者同源 iframe,一般分配独立渲染进程。网页解析、JS 执行、页面绘制、用户交互全部在此完成,页面崩溃只会影响当前标签页。
内部子线程:
- 主线程:JavaScript 执行核心线程,解析 HTML、CSS 生成 DOM 树、CSSOM 树,样式计算、重排 Layout、绘制 Paint;处理 DOM 事件,运行事件循环,调度宏、微任务。
- 合成线程:专门处理滚动、CSS 动画,将多个图层合并为屏幕最终图像。
- 光栅化线程池:把图层矢量绘图指令转换为像素位图。
- Worker 线程:运行各类 Worker(Dedicated、Shared、Service Worker),完全无法访问 DOM,依靠消息机制和主线程通信。
- 音频线程:Web Audio 音频解码、混音输出。
- 视频解码线程:软硬件解码视频流。
GPU 进程
专门处理图形加速,最初为 CSS3D 效果设计,现在网页 UI 渲染也大量依赖 GPU。
- GPU 主线程:调用系统图形驱动,管理显存、纹理、着色器资源;
- 合成线程:处理 CSS3D 变换、WebGL 图层合成;
- 显示线程:将合成完成帧输出到显示器。
网络进程
统一接管全部网络请求,独立于渲染进程。
- 网络调度线程:管理请求队列、优先级、TCP 连接复用;
- HTTP/2 线程:处理 HTTP/2 多路复用、头部 HPACK 压缩;
- QUIC/HTTP/3 线程:UDP 协议,解决 TCP 队头阻塞;
- 缓存线程:读写 HTTP 缓存,校验
Cache-Control、ETag。 - 代理线程:处理各类 HTTP、SOCKS 代理转发。
插件进程
用于运行 NPAPI、PPAPI 老式插件(Flash、旧 PDF 阅读器),现代 Web 已经极少使用。
JavaScript 主线程
JS 所有业务代码运行在渲染进程的主线程,语言层面单线程。
主线程核心特点:
- 同一时间只能执行一件任务;
- 同步任务顺序执行;
- 异步依靠事件循环 Event Loop调度,并非真正并行。
主线程承担工作:
- JS 代码执行:执行同步代码,消费宏任务、微任务队列;
- 网页解析构建:解析 HTML、CSS,生成 DOM 树、CSSOM 树;
- 样式计算:计算每个 DOM 元素最终生效样式;
- 布局(重排):计算元素坐标、宽高尺寸;
- 绘制记录:把布局结果转为绘图指令;
- 事件系统:捕获、冒泡用户交互事件,执行事件回调;
- 事件循环调度:维护宏任务、微任务队列,驱动异步执行;
- 跨线程协作:和合成线程、Worker 线程完成消息交互。
主线程页面完整执行流程:
- 用户输入 URL,网络进程下载 HTML 资源;
- 主线程接收 HTML 二进制数据,开始解析构建 DOM;
- 遇到
<script>标签,暂停 HTML 解析,加载并执行 JS,JS 可以修改 DOM 与 CSSOM; - JS 执行完毕,恢复 HTML 解析;
- 解析 CSS,构建 CSSOM 样式树;
- DOM + CSSOM 合并生成渲染树;
- 执行样式计算;
- 执行布局 Layout;
- 生成绘制 Paint 指令,交付给合成线程;
- 合成线程 → 光栅化线程 → 输出图像到屏幕。
为什么需要 Web Worker?
主线程所有任务排队执行,一旦执行耗时巨大的同步 JS 逻辑,主线程会被阻塞:页面无法响应点击、滚动、动画卡顿,也就是 UI 卡死。
传统方案局限性:
setTimeout:只是延后执行,依旧跑在主线程,耗时计算依然阻塞 UI;Promise:只是异步语法,没有开启新线程,计算逻辑仍然占用主线程;requestIdleCallback:利用浏览器空闲碎片时间执行,不能实现真正并行。
适合交给 Web Worker 的任务:
- 海量数据循环、复杂算法计算、大数据转换;
- 图像处理(配合
ImageBitmap传输二进制数据); - 加解密、复杂数据校验;
- 后台数据同步;
- WebSocket 实时消息处理。
传统方案只是任务调度,不是多线程并行;Web Worker 就是浏览器提供的真正后台线程方案,把重计算丢到子线程,主线程专心负责页面渲染和交互。
Web Worker 基础使用
Web Worker 允许开辟独立子线程运行 JS 脚本,和主线程并行工作,不会阻塞 UI 渲染。
核心特性:
- 独立操作系统线程,拥有自己的调用栈、全局上下文,不和主线程共享内存;
- 不能访问 DOM API,没有
document、window; - 线程间通信:依靠
postMessage()、onmessage传递数据,使用结构化克隆算法拷贝数据(不是共享引用)。 - 同源策略限制:Worker 脚本必须和页面同源。
- 拥有独立全局对象,Worker 内部使用
self代表全局。
注意事项:
- Worker 线程也有开销,不要无限制大量创建,线程过多会带来 CPU、内存压力;
postMessage是数据拷贝,传递超大对象性能损耗大,二进制优先使用Transferable转移;- Worker 不会自动销毁,不用的时候务必调用
terminate()(主线程)或者self.close()(Worker 内部)释放资源; - Service Worker 专注网络缓存、PWA 场景,不适合做密集计算;
- Shared Worker 兼容性一般,业务尽量优先选择 Dedicated Worker。
Dedicated Worker(专用 Worker)
日常开发最常用,一对一绑定。
特点:
- 一对一:仅能被创建它的主线程使用,其他页面、iframe 无法访问;
- 独立线程;
- 全局对象:
self,可以访问navigator、只读location、fetch、定时器。
// 主线程
const worker = new Worker('worker.js');
worker.postMessage('hello');
worker.onmessage = (e) => {};
worker.onerror = (error) => {};
worker.terminate();
// worker.js(Worker 线程)
self.onmessage = (e) => {};
self.onerror = (message, filename, lineno, colno, error) => {};
self.postMessage('world');
self.close();
Shared Worker(共享 Worker)
同源下多个页面、iframe 可以共享同一个 Worker 实例。
特点:
- 一对多:同源多个页面共用同一个 SharedWorker 实例;
- 使用
port端口对象通信,每一个连接对应独立 port; - 只要有端口连接,Worker 就保持存活;全部端口关闭,Worker 才销毁。
// 主线程(任意多个页面/脚本)
const sharedWorker = new SharedWorker('shared-worker.js');
sharedWorker.port.postMessage('hello');
sharedWorker.port.onmessage = (e) => {};
sharedWorker.onerror = (error) => {};
sharedWorker.port.start();
sharedWorker.port.close();
// shared-worker.js(Shared Worker 线程)
const ports = [];
self.onconnect = (event) => {
const port = event.ports[0];
ports.push(port);
port.onmessage = (e) => {
port.postMessage('收到: ' + e.data);
ports.forEach(p => p.postMessage('广播: ' + e.data));
};
port.start();
port.onclose = () => {};
};
self.onerror = (message, filename, lineno, colno, error) => {};
Service Worker(服务工作线程)
PWA 核心技术,网络代理线程,重点用于离线缓存、后台推送、后台同步,不适合做大量计算。
核心特点:
- 作为浏览器和服务器之间网络代理,可以拦截页面全部网络请求;
- 独立后台线程,不绑定任意页面;页面关闭,Service Worker 依旧可以存活;
- 强制 HTTPS(本地开发
localhost例外); - 事件驱动,空闲时浏览器自动休眠节省资源;
- 拥有完整生命周期:
install→activate→fetch; - 存在作用域 scope,只管控指定路径下页面。
// 主线程(注册 Service Worker)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then(registration => {
console.log('注册成功:', registration.scope);
})
.catch(error => {
console.error('注册失败:', error);
});
navigator.serviceWorker.onmessage = (e) => {};
navigator.serviceWorker.oncontrollerchange = () => {};
navigator.serviceWorker.ready.then(registration => {});
}
// sw.js(Service Worker 线程)
self.oninstall = (event) => {
event.waitUntil(
caches.open('my-cache-v1').then(cache => cache.addAll(
['/', '/styles.css', '/script.js']
))
);
};
self.onactivate = (event) => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(keys.filter(key => key !== 'my-cache-v1').map(key => caches.delete(key)));
})
);
};
self.onfetch = (event) => {
event.respondWith(
caches.match(event.request).then(response => response || fetch(event.request))
);
};
self.onmessage = (event) => {
event.source.postMessage('收到消息');
// 或者向所有客户端广播消息
self.clients.matchAll().then(clients => {
clients.forEach(client => client.postMessage('广播消息'));
});
};
self.onerror = (message, filename, lineno, colno, error) => {};
Web Worker API 完整清单
以 Dedicated Worker(专用 Worker) 作为基准;SharedWorker、ServiceWorker 存在部分差异会标注。
Worker 构造函数
const worker = new Worker(url, [options])
url:Worker 脚本资源地址,必须遵守同源策略;可以是相对路径、绝对 URL。options(可选)配置对象:type:脚本模式,classic(默认普通脚本)、module(ES Module 模块模式)credentials:凭证策略,omit、same-origin、include;模块模式下才会生效name:给 Worker 设置名字,用于调试,worker 内部可通过self.name获取
SharedWorker:new SharedWorker(url[, options]);通信必须通过 .port 对象完成。
ServiceWorker:不使用 new 构造函数实例化,通过 navigator.serviceWorker.register() 进行注册。
主线程端:Worker
实例属性:
onmessage:监听 Worker 发送过来的消息;回调接收MessageEvent事件对象,e.data为接收到的数据。onmessageerror:消息序列化失败时触发,收到无法被结构化克隆解析的数据会执行该回调。onerror:捕获 Worker 线程内部抛出的错误。
实例方法:
postMessage(message[, transferList]):向 Worker 线程发送消息。message:任意可被结构化克隆算法处理的数据transferList:转移对象数组,用于ArrayBuffer这类 Transferable 对象;转移内存所有权之后,原线程无法再访问该内存。
terminate():主线程调用,立刻终止 Worker 线程;会直接停止执行,不会等待当前正在运行的脚本完成。
SharedWorker 差异:SharedWorker 实例本身不存在 onmessage、postMessage()、terminate();所有消息操作全部需要通过 sharedWorker.port 端口对象调用。
Worker 子线程全局作用域
Worker 内部,self 代表 Worker 的全局对象,等价于 this。
全局事件处理器
self.onmessage:接收主线程发送过来的消息。self.onmessageerror:解析收到的消息失败时触发。self.onerror:Worker 线程内部的全局错误捕获。
其他 Worker 差异:
- SharedWorker:使用
port.onmessage;全局监听新页面连接使用self.onconnect。 - ServiceWorker:具备专属事件:
install、activate、fetch、push、sync。
全局作用域方法
self.postMessage(data[, transferList]):Worker 向主线程发送消息,支持传入转移列表。self.close():Worker 线程内部调用关闭自身;停止接收新消息队列任务,但不会立刻中断正在同步执行的代码。importScripts(url1, url2, …):仅 Dedicated Worker 可用,同步加载并执行外部 JS 脚本,会阻塞当前 Worker 线程;SharedWorker、ServiceWorker 不支持该 API。
全局只读属性
self.name:构造函数 options 配置传入的 worker 名称。
支持的 API
- JavaScript 核心功能(全部支持):标准的 JavaScript 函数集(如 String、Array、Object、JSON 等),Array、Object、Map、Set、Promise、Symbol、ArrayBuffer、DataView、TypedArray、正则表达式、箭头函数、类、模块。
- 定时器函数:setTimeout、clearTimeout、setInterval、clearInterval。注意:不支持 setImmediate(非标准)。
- 网络请求 API:fetch、XMLHttpRequest、WebSocket。
- 存储 API:caches(Service Worker 专用,其他 Worker 也支持)、indexedDB。
- 编解码 API:TextEncoder、TextDecoder、btoa、atob。
- 二进制数据处理:Blob、FileReader(部分支持,无文件选择功能)、File/FileList(不支持,无法访问用户文件)。
- 加密 API(部分支持):Crypto(支持 getRandomValues,但不支持 subtle)。
- URL 相关 API:URL、URLSearchParams。
- 导航器信息(部分):支持 userAgent、language、onLine、serviceWorker(仅安全上下文);不支持 geolocation, clipboard, mediaDevices, permissions 等。
- 其他支持的 API:console、performance、BroadcastChannel、EventSource。
不支持的 API
- DOM 相关(完全不支持):document、window、parent、top、opener、DOM 元素操作。
- 渲染相关:requestAnimationFrame、canvas、WebGL、CSS 对象模型。
- 大多数交互 API:alert、confirm、prompt、localStorage、sessionStorage、Clipboard、Geolocation、Notification、Payment Request、Speech、Fullscreen、History。
- 部分其他 API:WebUSB、WebBluetooth、WebSerial、IntersectionObserver、ResizeObserver、MutationObserver(部分浏览器可能支持)。
三种 Worker 关键差异
Dedicated Worker:
- 创建方式:
new Worker(); - 通信入口:实例直接调用 postMessage;
- 支持
importScripts(); - 终止方式:主线程
terminate()、Worker 内部self.close(); - 无额外专属事件;
- 使用场景:大数据密集计算。
Shared Worker:
- 创建方式:
new SharedWorker(); - 通信入口:必须通过
.port端口对象; - 不建议使用
importScripts; - 终止方式:关闭 port 端口连接;
- 专属事件:
onconnect; - 使用场景:同源多个页面共享同一个后台线程。
Service Worker:
- 创建方式:
navigator.serviceWorker.register()注册; - 通信入口:主线程
navigator.serviceWorker.onmessage; - 禁止使用
importScripts; - 生命周期:浏览器自动休眠;内部使用
self.skipWaiting(); - 专属事件:
install、activate、fetch、push; - 使用场景:PWA、离线缓存、网络代理、后台消息推送。
小结
- JS 语言单线程,阻塞根源在主线程;Web Worker 是浏览器提供的子线程能力,实现真正并行。
- Dedicated Worker:一对一,适合大量计算,业务最常用;
- Shared Worker:同源多页面共享;
- Service Worker:PWA 网络代理,离线缓存、消息推送;
- Worker 没有 DOM,线程间依靠
postMessage消息通信。