【阅读完这篇文章我们就可以更好的理解线程/进程】
JavaScript最初设计为单线程运行在浏览器中,主要是为了避免多个线程同时操作DOM导致的渲染冲突问题。这种设计确保了页面渲染的一致性和安全性,简化了编程模型
。然而,随着前端技术的发展,JavaScript的应用场景已远超简单的页面交互,当遇到图像处理、视频解码等需要大量计算的场景时,单线程的局限性就显现出来:主线程会被长时间阻塞,导致页面卡顿,严重影响用户体验。
为了解决单线程的瓶颈,HTML5引入了Web Worker标准
。Web Worker允许JavaScript脚本创建多个子线程,这些子线程可以执行耗时的计算任务,从而解放主线程,避免阻塞UI渲染和事件响应。需要注意的是,子线程完全受主线程控制,且无法直接操作DOM。通过将任务拆分为多个小任务(每个任务执行时间控制在50毫秒以内),并使用setTimeout或queueMicrotask等机制调度,可以有效避免主线程被长时间占用。
进程与线程的区别
在介绍进程与线程的概念前,我们先来看个进程与线程之间关系形象的比喻:

如上图所示,进程是一个工厂,它有独立的资源,线程是工厂中的工人,多个工人协作完成任务,工人之间共享工厂内的资源,比如工厂内的食堂或餐厅。此外,工厂(进程)与工厂(进程)之间是相互独立的。为了让大家能够更直观地理解进程与线程的区别,我们继续来看张图:

由上图可知,操作系统会为每个进程分配独立的内存空间,一个进程由一个或多个线程组成,同个进程下的各个线程之间共享程序的内存空间。相信通过前面两张图,小伙伴们对进程和线程之间的区别已经有了一定的了解,那么实际情况是不是这样呢?这里我们打开 macOS 操作系统下的活动监视器,来看一下写作本文时所有进程的状态:

(这里大家可以打开windows的进程,也能看见不同的进程有多条线程在进行中。)
通过上图可知,我们常用的软件,比如微信和搜狗输入法都是一个独立的进程,拥有不同的 PID(进程 ID),而且图中的每个进程都含有多个线程,以微信进程为例,它就含有 「36」 个线程。那么什么是进程和线程呢?下面我们来介绍进程和线程的概念。
进程和线程是操作系统中两个核心概念,它们在资源分配、执行方式和应用场景上有着显著区别。
基本定义
进程 是操作系统进行资源分配和调度的基本单位,拥有独立的地址空间和系统资源。可以理解为程序的一次执行实例。
线程是进程内部的一个执行单元,是CPU调度的基本单位,共享所属进程的地址空间和资源。一个进程可以包含多个线程。
核心区别
1. 资源分配 :进程是资源分配的基本单位,拥有独立内存空间;线程共享进程资源,不拥有独立资源。
2. 独立性 :进程是独立运行的单位;线程不能独立执行,必须依赖于进程。
3. 切换开销 :进程切换需要保存和恢复整个地址空间,开销较大(1-5微秒);线程切换只需保存寄存器状态,开销较小(0.1-0.3微秒)。
4. 健壮性:进程间通过IPC机制通信,错误隔离性好;线程间直接共享内存,一个线程的非法操作可能导致整个进程崩溃。
单线程与多线程
如果一个进程只有一个线程,我们称之为单线程。单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。单线程处理的优点:同步应用程序的开发比较容易,但由于需要在上一个任务完成后才能开始新的任务,所以其效率通常比多线程应用程序低。
如果完成同步任务所用的时间比预计时间长,应用程序可能会不响应。针对这个问题,我们可以考虑使用多线程,即在进程中使用多个线程,这样就可以处理多个任务。
对于 Web 开发者熟悉的 JavaScript 来说,它运行在浏览器中,是单线程的,每个窗口一个 JavaScript 线程,既然是单线程的,在某个特定的时刻,只有特定的代码能够被执行,其它的代码会被阻塞。
"JS 中其实是没有线程概念的,所谓的单线程也只是相对于多线程而言。JS 的设计初衷就没有考虑这些,针对 JS
这种不具备并行任务处理的特性,我们称之为 "单线程"。 ------ 来自知乎 "如何证明 JavaScript 是单线程的?"
浏览器内核
其实在浏览器内核(渲染进程)中除了 JavaScript 引擎线程之外,还含有 GUI 渲染线程、事件触发线程、定时触发器线程等。因此对于浏览器的渲染进程来说,它是多线程的。多个协同工作的线程,共同完成页面的渲染和交互任务。
主要线程
- GUI渲染线程
GUI 渲染线程负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。 - JavaScript引擎线程
JavaScript 引擎线程负责解析 JavaScript 脚本并运行相关代码。 JavaScript 引擎一直等待着任务队列中任务的到来,然后进行处理,一个Tab页(Renderer 进程)中无论什么时候都只有一个 JavaScript 线程在运行 JavaScript 程序。
需要注意的是,GUI 渲染线程与 JavaScript 引擎线程是互斥的,所以如果 JavaScript 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染被阻塞。
3. 事件触发线程
当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JavaScript 引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等,但由于 JavaScript 引擎是单线程的,所有这些事件都得排队等待 JavaScript 引擎处理。
4. 定时器触发线程
浏览器定时计数器并不是由 JavaScript 引擎计数的,这是因为 JavaScript 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确,所以通过单独线程来计时并触发定时是更为合理的方案。我们日常开发中常用的 setInterval 和 setTimeout 就在该线程中。 - 异步HTTP请求线程
在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。
前面我们已经知道了,由于 JavaScript 引擎与 GUI 渲染线程是互斥的,如果 JavaScript 引擎执行了一些计算密集型或高延迟的任务,那么会导致 GUI 渲染线程被阻塞或拖慢。那么如何解决这个问题呢?嘿嘿,当然是使用本文的主角 ------ Web Workers。
6. 合成线程
优化图层合成,提升渲染性能。

Web Worker 是什么
Web Worker 作为 HTML5 标准的关键特性,其核心在于通过一套 API 机制,在 JavaScript 主线程之外创建独立的 Worker 线程。这种设计突破了传统单线程模型的限制,使开发者能够利用 JavaScript 实现多线程操作。
由于 Worker 线程与主线程相互独立并行运行,二者不会相互阻塞。当面对大规模计算任务时,开发者可将计算逻辑转移至 Worker 线程处理。待计算完成后,Worker 线程再将结果返回主线程。这种分工模式使主线程得以专注于业务逻辑处理,无需耗费大量时间执行复杂运算,从而有效减少阻塞现象,显著提升运行效率,最终带来更流畅的页面交互体验和更优质的用户感受。

(图片来源:https://thecodersblog.com/web-worker-and-implementation/)
Web Workers 的限制与能力
通常情况下,你可以在 Worker 线程中运行任意的代码,但注意存在一些例外情况,比如:「直接在 worker 线程中操纵 DOM 元素,或使用 window 对象中的某些方法和属性 。」 大部分 window 对象的方法和属性是可以使用的,包括 WebSockets,以及诸如 IndexedDB 和 FireFox OS 中独有的 Data Store API 这一类数据存储机制。
下面我们以 Chrome 和 Opera 所使用的 Blink 渲染引擎为例,介绍该渲染引擎下 Web Worker 中所支持的常用 APIs:
Cache: Cache 接口为缓存的 Request / Response 对象对提供存储机制,例如,作为ServiceWorker 生命周期的一部分。
CustomEvent: 用于创建自定义事件。
Fetch: Fetch API 提供了一个获取资源的接口(包括跨域请求)。任何使用过 XMLHttpRequest 的人都能轻松上手,而且新的 API 提供了更强大和灵活的功能集。Promise: Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。FileReader:FileReader 对象允许 Web 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。
IndexedDB: IndexedDB 是一种底层 API,用于客户端存储大量结构化数据,包括文件/二进制大型对象(blobs)。
WebSocket: WebSocket 对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。
XMLHttpRequest: XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。
Web Worker 使用
创建 worker 只需要通过 new 调用 Worker() 构造函数即可,它接收两个参数。
const worker = new Worker(path, options);
| 参数 | 说明 |
|---|---|
| path | 有效的js脚本的地址,必须遵守同源策略。无效的js地址或者违反同源策略,会抛出SECURITY_ERR 类型错误 |
| options.type | 可选,用以指定 worker 类型。该值可以是 classic 或 module。 如未指定,将使用默认值 classic |
| options.credentials | 可选,用以指定 worker 凭证。该值可以是 omit, same-origin,或 include。如果未指定,或者 type 是 classic,将使用默认值 omit (不要求凭证) |
| options.name | 可选,在 DedicatedWorkerGlobalScope 的情况下,用来表示 worker 的 scope 的一个 DOMString 值,主要用于调试目的。 |
js 主线程与 worker 线程数据传递
主线程与 worker 线程都是通过 postMessage 方法来发送消息,以及监听 message 事件来接收消息。如下所示:
// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建worker
myWorker.addEventListener('message', e => { // 接收消息
console.log(e.data); // Greeting from Worker.js,worker线程发送的消息
});
// 这种写法也可以
// myWorker.onmessage = e => { // 接收消息
// console.log(e.data);
// };
myWorker.postMessage('Greeting from Main.js'); // 向 worker 线程发送消息,对应 worker 线程中的 e.data
// worker.js(worker线程)
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息
self.postMessage('Greeting from Worker.js'); // 向主线程发送消息
});
其他的详细的用法可以参看官网,也可以看其他博主写的,比如关闭worker线程的方法:
// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建worker
myWorker.terminate(); // 关闭worker
// worker.js(worker线程)
self.close(); // 直接执行close方法就ok了
又比如监听错误的信息:
web worker 提供两个事件监听错误,error (当worker内部出现错误时触发)和 messageerror(当 message 事件接收到无法被反序列化的参数时触发)。
// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建worker
myWorker.addEventListener('error', err => {
console.log(err.message);
});
myWorker.addEventListener('messageerror', err => {
console.log(err.message)
});
// worker.js(worker线程)
self.addEventListener('error', err => {
console.log(err.message);
});
self.addEventListener('messageerror', err => {
console.log(err.message);
});
Worker 线程引用其他js文件
总有一些场景,需要放到 worker 进程去处理的任务很复杂,需要大量的处理逻辑,我们当然不想把所有代码都塞到 worker.js 里,那样就太糟糕了。不出意料,web worker 为我们提供了解决方案,我们可以在 worker 线程中利用 importScripts() 方法加载我们需要的js文件,而且,通过此方法加载的js文件不受同源策略约束!
// utils.js
const add = (a, b) => a + b;
// worker.js(worker线程)
// 使用方法:importScripts(path1, path2, ...);
importScripts('./utils.js');
console.log(add(1, 2)); // log 3
记得如果是ESModule 模式的js文档,我们只需要修改一下配置即可,
// main.js(主线程)
const worker = new Worker('/worker.js', {
type: 'module' // 指定 worker.js 的类型
});
// utils.js
export default add = (a, b) => a + b;
// worker.js(worker线程)
import add from './utils.js'; // 导入外部js
self.addEventListener('message', e => {
postMessage(e.data);
});
add(1, 2); // log 3
export default self; // 只需把顶级对象self暴露出去即可
Web Workers 的分类
Web Worker 规范中定义了两类工作线程,分别是专用线程 Dedicated Worker 和共享线程 Shared Worker,其中,Dedicated Worker 只能为一个页面所使用,而 Shared Worker 则可以被多个页面所共享。
Dedicated Worker
一个专用 Worker 仅仅能被生成它的脚本所使用,其浏览器支持情况如下:

(图片来源:[https://caniuse.com/#search=Web Workers\](https://caniuse.com/#search=Web Workers))
需要注意的是,由于 Web Worker 有同源限制,所以在进行本地调试或运行以下示例的时候,需要先启动本地服务器,直接使用 file:// 协议打开页面的时候,会抛出以下异常:
Uncaught DOMException: Failed to construct 'Worker':
Script at 'file:///**/*.js' cannot be accessed from origin 'null'.
1 专用线程 Dedicated Worker:Ping/Pong

「index.html」
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>专用线程 Dedicated Worker ------ Ping/Pong</title>
</head>
<body>
<h3>阿宝哥:专用线程 Dedicated Worker ------ Ping/Pong</h3>
<script>
if (window.Worker) {
let worker = new Worker("dw-ping-pong.js");
worker.onmessage = (e) =>
console.log(`Main: Received message - ${e.data}`);
worker.postMessage("PING");
} else {
console.log("呜呜呜,不支持 Web Worker");
}
</script>
</body>
</html>
「dw-ping-pong.js」
onmessage = (e) => {
console.log(`Worker: Received message - ${e.data}`);
postMessage("PONG");
}
以上代码成功运行后,浏览器控制台会输出以下结果:
Worker: Received message - PING
Main: Received message - PONG
每个 Web Worker 都可以创建自己的子 Worker,这允许我们将任务分散到多个线程。创建子 Worker 也很简单,具体我们来看个例子。
2 专用线程 Dedicated Sub Worker:Ping/Pong

「index.html」
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>专用线程 Dedicated Sub Worker ------ Ping/Pong</title>
</head>
<body>
<h3>阿宝哥:专用线程 Dedicated Sub Worker ------ Ping/Pong</h3>
<script>
if (window.Worker) {
let worker = new Worker("dw-ping-pong.js");
worker.onmessage = (e) =>
console.log(`Main: Received message - ${e.data}`);
worker.postMessage("PING");
} else {
console.log("呜呜呜,不支持 Web Worker");
}
</script>
</body>
</html>
「dw-ping-pong.js」
onmessage = (e) => {
console.log(`Worker: Received message - ${e.data}`);
setTimeout(() => {
let worker = new Worker("dw-sub-ping-pong.js");
worker.onmessage = (e) => console.log(`Worker: Received from sub worker - ${e.data}`);
worker.postMessage("PING");
}, 1000);
postMessage("PONG");
};
「dw-sub-ping-pong.js」
onmessage = (e) => {
console.log(`Sub Worker: Received message - ${e.data}`);
postMessage("PONG");
};
以上代码成功运行后,浏览器控制台会输出以下结果:
Worker: Received message - PING
Main: Received message - PONG
Sub Worker: Received message - PING
Received from sub worker - PONG
3 专用线程 Dedicated Worker:importScripts
在 Web Worker 中,我们也可以使用 importScripts 方法将一个或多个脚本同步导入到 Web Worker 的作用域中。同样我们来举个例子。
「index.html」
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>专用线程 Dedicated Worker ------ importScripts</title>
</head>
<body>
<h3>阿宝哥:专用线程 Dedicated Worker ------ importScripts</h3>
<script>
let worker = new Worker("worker.js");
worker.onmessage = (e) => console.log(`Main: Received kebab case message - ${e.data}`);
worker.postMessage(
"Hello, My name is semlinker."
);
</script>
</body>
</html>
importScripts("https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.15/lodash.min.js");
onmessage = ({ data }) => {
postMessage(_.kebabCase(data));
};
以上代码成功运行后,浏览器控制台会输出以下结果:
Main: Received kebab case message - hello-my-name-is-semlinker
4 专用线程 Dedicated Worker:inline-worker
在前面的例子中,我们都是使用外部的 Worker 脚本来创建 Web Worker 对象。其实你也可以通过 Blob URL 或 Data URL 的形式来创建 Web Worker,这类 Worker 也被称为 Inline Worker。
「1. 使用 Blob URL 创建 Inline Worker」
Blob URL/Object URL 是一种伪协议,允许 Blob 和 File 对象用作图像,下载二进制数据链接等的 URL 源。在浏览器中,我们使用 URL.createObjectURL 方法来创建 Blob URL,该方法接收一个 Blob 对象,并为其创建一个唯一的 URL,其形式为 blob:/,对应的示例如下:
blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641
浏览器内部为每个通过 URL.createObjectURL 生成的 URL 存储了一个 URL → Blob 映射。因此,此类 URL 较短,但可以访问 Blob。生成的 URL 仅在当前文档打开的状态下才有效。它允许引用 、 中的 Blob,但如果你访问的 Blob URL 不再存在,则会从浏览器中收到 404 错误。
const url = URL.createObjectURL(
new Blob([`postMessage("Dedicated Worker created by Blob")`])
);
let worker = new Worker(url);
worker.onmessage = (e) =>
console.log(`Main: Received message - ${e.data}`);
除了在代码中使用字符串动态创建 Worker 脚本,也可以把 Worker 脚本使用类型为 javascript/worker 的 script 标签内嵌在页面中,具体如下所示:
<script id="myWorker" type="javascript/worker">
self['onmessage'] = function(event) {
postMessage('Hello, ' + event.data.name + '!');
};
</script>
接着就是通过 script 对象的 textContent 属性来获取对应的内容,然后使用 Blob API 和 createObjectURL API 来最终创建 Web Worker:
<script>
let workerScript = document.querySelector('#myWorker').textContent;
let blob = new Blob(workerScript, {type: "text/javascript"});
let worker = new Worker(URL.createObjectURL(blob));
</script>
「2. 使用 Data URL 创建 Inline Worker」
Data URLs 由四个部分组成:前缀(data:)、指示数据类型的 MIME 类型、如果非文本则为可选的 base64 标记、数据本身:
data:[<mediatype>][;base64],<data>
mediatype 是个 MIME 类型的字符串,例如 "image/jpeg" 表示 JPEG 图像文件。如果被省略,则默认值为 text/plain;charset=US-ASCII。如果数据是文本类型,你可以直接将文本嵌入(根据文档类型,使用合适的实体字符或转义字符)。如果是二进制数据,你可以将数据进行 base64 编码之后再进行嵌入。
const url = `data:application/javascript,${encodeURIComponent(
`postMessage("Dedicated Worker created by Data URL")`
)}`;
let worker = new Worker(url);
worker.onmessage = (e) =>
console.log(`Main: Received message - ${e.data}`);
SharedWorker
SharedWorker 是一种特殊类型的 Worker,可以被多个浏览上下文访问,比如多个 windows,iframes 和 workers,但这些浏览上下文必须同源。它们实现于一个不同于普通 worker 的接口,具有不同的全局作用域:SharedWorkerGlobalScope ,但是继承自WorkerGlobalScope

SharedWorker 线程的创建和使用跟 worker 类似,事件和方法也基本一样。
不同点在于,主线程与 SharedWorker 线程是通过MessagePort建立起链接,数据通讯方法都挂载在SharedWorker.port上。
值得注意的是,如果你采用 addEventListener 来接收 message 事件,那么在主线程初始化SharedWorker() 后,还要调用 SharedWorker.port.start() 方法来手动开启端口。
// main.js(主线程)
const myWorker = new SharedWorker('./sharedWorker.js');
myWorker.port.start(); // 开启端口
myWorker.port.addEventListener('message', msg => {
console.log(msg.data);
})
但是,如果采用 onmessage 方法,则默认开启端口,不需要再手动调用SharedWorker.port.start()方法
// main.js(主线程)
const myWorker = new SharedWorker('./sharedWorker.js');
myWorker.port.onmessage = msg => {
console.log(msg.data);
};
以上两种方式效果是一样的,具体信息请参考MessagePort。
由于 SharedWorker 是被多个页面共同使用,那么除了与各个页面之间的数据通讯是独立的,同一个SharedWorker 线程上下文中的其他资源都是共享的。基于这一点,很容易实现不同页面之间的数据通讯。
我们来看一个ShareWorker的例子,它是实现多页面数据共享的例子:
- index 页面的 add 按钮,每点击一次,向 sharedWorker 发送一次 add 数据,页面 count 增加1
// index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>index page</title>
</head>
<body>
<p>index page: </p>
count: <span id="container">0</span>
<button id="add">add</button>
<br>
// 利用iframe加载
<iframe src="./iframe.html"></iframe>
</body>
<script type="text/javascript">
if (!!window.SharedWorker) {
const container = document.getElementById('container');
const add = document.getElementById('add');
const myWorker = new SharedWorker('./sharedWorker.js');
myWorker.port.start();
myWorker.port.addEventListener('message', msg => {
container.innerText = msg.data;
});
add.addEventListener('click', () => {
myWorker.port.postMessage('add');
});
}
</script>
</html>
- iframe 页面的 reduce 按钮,每点击一次,向 sharedWorker 发送一次 reduce 数据,页面count 减少1
// iframe.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>iframe page</title>
</head>
<body>
<p>iframe page: </p>
count: <span id="container">0</span>
<button id="reduce">reduce</button>
</body>
<script type="text/javascript">
if (!!window.SharedWorker) {
const container = document.getElementById('container');
const reduce = document.getElementById('reduce');
const myWorker = new SharedWorker('./sharedWorker.js');
myWorker.port.start();
myWorker.port.addEventListener('message', msg => {
container.innerText = msg.data;
})
reduce.addEventListener('click', () => {
myWorker.port.postMessage('reduce');
});
}
</script>
</html>
-
sharedWorker 在接收到数据后,根据数据类型处理 num 计数,然后返回给每个已连接的主线程。
// sharedWorker.jslet num = 0;
const workerList = [];self.addEventListener('connect', e => {
const port = e.ports[0];
port.addEventListener('message', e => {
num += e.data === 'add' ? 1 : -1;
workerList.forEach(port => { // 遍历所有已连接的part,发送消息
port.postMessage(num);
})
});
port.start();
workerList.push(port); // 存储已连接的part
port.postMessage(num); // 初始化
});
结果可以发现,index 页面和 iframe 页面的 count 始终保持一致,实现了多个页面数据同步。

sharedWorker调试
在 sharedWorker 线程里使用 console 打印信息,不会出现在主线程的的控制台中。如果你想调试 sharedWorker,需要在 Chrome 浏览器输入 chrome://inspect/ ,这里能看到所有正在运行的 sharedWorker,然后开启一个独立的 dev-tool 面板。

如果是一个大型项目或者是需要两个不同的界面进行数据传送,那么shareWorker是非常好的一种方式了。
我们也可以将它在项目中进行配置,具体可以看看vite的配置。
参考文章