前端性能优化篇—必学之Web Worker我们都知道AI生成是一件很耗时的任务。因为JS是一门单线程的语言,在我们调用L
我们都知道AI生成是一件很耗时的任务。因为JS是一门单线程的语言,在我们调用LLM的时候,会有少则几秒,长则几分钟的等待时间。如果把这个任务给JS去执行的话,那界面将会阻塞,用户将无法操作界面,前端最需要做的就是用户体验,这是万万不行的。
让AI耗时任务去进入后台线程
解决异步有哪些做法
-
回调函数 (Callback) 回调函数是最基础的异步处理方式。你将一个函数作为参数传递给另一个函数,当异步操作完成后,这个回调函数会被调用。
function asyncOperation(callback) { setTimeout(() => { console.log("异步操作完成"); callback(); }, 1000); } asyncOperation(() => { console.log("回调函数被调用"); });
-
事件监听 (Event Listener) 事件监听是一种基于事件驱动的异步处理方式。通过监听特定事件的触发来执行相应的操作。
const button = document.getElementById("myButton"); button.addEventListener("click", () => { console.log("按钮被点击"); });
-
Promise 是一种用于处理异步操作的对象,能够更好地管理回调函数嵌套问题。它可以有三种状态:
pending
(进行中)、fulfilled
(已完成)和rejected
(已失败)。const promise = new Promise((resolve, reject) => { setTimeout(() => { resolve("异步操作成功"); }, 1000); }); promise.then((result) => { console.log(result); }).catch((error) => { console.error(error); });
-
async/await
async/await
是基于 Promise 的语法糖,使得异步代码看起来更像是同步代码,从而提高了代码的可读性和维护性。function asyncOperation() { return new Promise((resolve, reject) => { setTimeout(() => { resolve("异步操作成功"); }, 1000); }); } async function executeAsyncOperation() { try { const result = await asyncOperation(); console.log(result); } catch (error) { console.error(error); } } executeAsyncOperation();
-
事件循环(Event Loop)
- 调用栈 (Call Stack) 调用栈是一个 LIFO(后进先出)结构,用于存储在执行的函数调用。同步代码会按顺序在调用栈中执行。
- 任务队列 (Task Queue) 任务队列用于存放需要执行的异步任务,当调用栈为空时,事件循环会从任务队列中取出任务并将其放入调用栈中执行。任务队列通常包括宏任务队列和微任务队列。
- 宏任务 (Macro Task) 宏任务包括整体代码脚本、
setTimeout
、setInterval
、I/O 操作等。这些任务会在事件循环的每一次循环中依次执行。 - 微任务 (Micro Task) 微任务通常包括
Promise
的回调函数、MutationObserver
等。微任务会在当前宏任务执行完后立即执行,并且在下一个宏任务开始之前执行完所有微任务。
事件循环执行过程
- 执行调用栈中的同步代码,直到调用栈为空。
- 检查微任务队列并执行所有微任务,直到微任务队列为空。
- 从任务队列中取出一个宏任务并执行,回到第 1 步。
以下是一个包含同步代码、宏任务和微任务的示例代码,以及事件循环的执行过程:
console.log("同步代码开始");
setTimeout(() => {
console.log("setTimeout 宏任务");
}, 0);
Promise.resolve().then(() => {
console.log("Promise 微任务");
});
console.log("同步代码结束");
执行顺序分析
-
同步代码执行
- 执行
console.log("同步代码开始");
- 执行
console.log("同步代码结束");
- 调用栈中同步代码执行完毕。
- 执行
-
微任务队列执行
- 执行
Promise.resolve().then(...)
回调中的console.log("Promise 微任务");
- 微任务队列执行完毕。
- 执行
-
宏任务队列执行
- 执行
setTimeout
回调中的console.log("setTimeout 宏任务");
- 宏任务队列执行完毕。
- 执行
最终的输出顺序为:
同步代码开始
同步代码结束
Promise 微任务
setTimeout 宏任务
虽然上面的方法是解决异步的办法,但他并没有并发线程,依旧有可能让几分钟的异步操作堵塞界面,属于是治标不治本。
为什么要用Web Worker
当你用Event loop事件循环的办法去解决AIGC长达几分钟的响应时间时,这个宏任务依旧会堵塞主线程。线程是处理长时间异步操作的有效方式,通过多线程并行执行任务,避免主线程阻塞。
以下是详细解释:
1. 避免阻塞主线程
Web Worker 可以在单独的线程中运行 JavaScript 代码,而不阻塞主线程。主线程负责处理用户交互、页面渲染和其他短时间的任务。如果长时间任务在主线程中执行,即使是通过异步调用,也会影响用户体验。
示例
当长时间任务在主线程中执行时,浏览器的响应速度会变慢,用户可能会感觉页面卡顿或无响应。
// 主线程中的长时间任务
for (let i = 0; i < 1e9; i++) {
// 执行一些计算
}
console.log('长时间任务完成');
使用 Web Worker 可以将这些计算移到后台线程,不影响主线程的运行。
// main.js
const worker = new Worker('worker.js');
worker.onmessage = function(event) {
console.log('长时间任务完成:', event.data);
};
worker.postMessage('开始任务');
// worker.js
self.onmessage = function(event) {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
self.postMessage(sum);
};
2. 提升性能
Web Worker 运行在独立的线程中,可以利用多核 CPU 提高性能。对于 CPU 密集型任务,使用多线程可以显著提升计算效率。
示例
在多核 CPU 上,多个 Web Worker 可以并行处理多个任务,充分利用硬件资源。
// main.js
const worker1 = new Worker('worker.js');
const worker2 = new Worker('worker.js');
worker1.onmessage = function(event) {
console.log('Worker 1 任务完成:', event.data);
};
worker2.onmessage = function(event) {
console.log('Worker 2 任务完成:', event.data);
};
worker1.postMessage('开始任务 1');
worker2.postMessage('开始任务 2');
3. 简化代码组织
Web Worker 使得长时间任务的代码可以独立于主线程运行,代码更加模块化和清晰。这种方式使得代码更易于维护和调试。
示例
在主线程中保持代码简洁,只需处理用户交互和页面渲染,而复杂的计算任务则放在 Worker 中处理。
// main.js
const worker = new Worker('worker.js');
worker.onmessage = function(event) {
document.getElementById('result').textContent = '计算结果: ' + event.data;
};
document.getElementById('startButton').addEventListener('click', () => {
worker.postMessage('开始计算');
});
// worker.js
self.onmessage = function(event) {
// 执行长时间任务
let result = performLongTask();
self.postMessage(result);
};
function performLongTask() {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
return sum;
}
4. 更好的用户体验
使用 Web Worker 可以确保用户界面在执行长时间任务时仍然响应用户操作。例如,用户仍然可以滚动页面、点击按钮和输入文本,而不会感觉到页面卡顿。
示例
当计算任务在 Worker 中执行时,主线程仍然可以处理用户的滚动操作。
// main.js
const worker = new Worker('worker.js');
worker.onmessage = function(event) {
document.getElementById('result').textContent = '计算结果: ' + event.data;
};
document.getElementById('startButton').addEventListener('click', () => {
worker.postMessage('开始计算');
});
window.addEventListener('scroll', () => {
console.log('页面滚动');
});
5. 避免主线程阻塞
即使使用异步操作 (如 setTimeout
和 Promise
),长时间的任务仍可能导致任务队列堆积,从而间接阻塞主线程。Web Worker 通过将任务移至独立线程,完全避免了这种情况。
示例
异步操作虽然不会阻塞主线程,但会增加事件循环的负担,导致高延迟。
// 使用 setTimeout 模拟异步分块处理长时间任务
function longTask() {
let sum = 0;
function computeChunk() {
for (let i = 0; i < 1e6; i++) {
sum += i;
}
if (sum < 1e9) {
setTimeout(computeChunk, 0);
} else {
console.log('长时间任务完成:', sum);
}
}
computeChunk();
}
longTask();
使用 Web Worker 而不是依赖 Event Loop 处理长时间的异步任务,可以避免主线程阻塞,提高性能和用户体验,并使代码更易于维护和组织。
Web Worker 有哪些用法
Web Worker 是一种在后台线程中运行的 JavaScript 脚本,它不会阻塞主线程,从而能提升 Web 应用的性能。使用 Web Worker 可以让你在不影响用户界面的情况下进行复杂的计算或处理。
1. 基本概念
主线程:这是你的主要 JavaScript 代码执行的线程,处理用户界面和事件。
Web Worker:在一个独立的线程中运行的脚本,不能直接访问 DOM,只能通过消息传递与主线程进行通信。
2. 创建 Web Worker
使用 Worker
构造函数来创建一个新的 Web Worker。你需要提供一个指向 Worker 脚本的 URL。
// main.js
const worker = new Worker('worker.js');
3. Worker 脚本
Worker 脚本是一个独立的 JavaScript 文件,运行在 Worker 线程中。你可以在其中编写需要后台处理的代码。
// worker.js
self.onmessage = function(e) {
// 接收主线程的消息
console.log('Message received from main script');
const result = e.data * 2;
// 处理完成后,将结果发送回主线程
self.postMessage(result);
};
4. 主线程与 Worker 之间的消息传递
使用 postMessage
方法将消息发送到 Worker,Worker 使用 onmessage
事件接收消息。反之,Worker 可以通过 postMessage
将结果发送回主线程。
// main.js
worker.postMessage(10); // 发送消息到 Worker
worker.onmessage = function(e) {
console.log('Message received from worker: ', e.data);
};
5. 错误处理
你可以使用 onerror
事件处理 Worker 中的错误。
// main.js
worker.onerror = function(e) {
console.error('Error in Worker: ', e.message);
};
6. 终止 Worker
当 Worker 不再需要时,可以使用 terminate
方法终止它。
// main.js
worker.terminate();
7. Web Worker 的局限性
- 不能直接操作 DOM:Worker 线程不能直接操作页面元素。
- 安全性限制:Worker 线程在沙箱环境中运行,不能访问主线程的全局变量。
- 共享内存:Worker 与主线程之间的消息传递是通过复制数据完成的,无法直接共享内存。如果需要共享内存,可以使用
SharedArrayBuffer
和Atomics
。
主线程和后台线程的不同API
主线程
-
主要 API:
Worker
构造函数:用于创建 Web Worker 实例。postMessage
:向 Worker 发送消息。onmessage
:处理 Worker 发回的消息。terminate
:立即停止 Worker 执行。
// main.js const worker = new Worker('worker.js'); worker.postMessage('Hello, Worker!'); worker.onmessage = function(e) { console.log('Message from Worker:', e.data); };
Web Worker 线程(后台线程)
-
主要 API:
self
:在 Worker 中代表 Worker 的全局对象,相当于主线程中的window
对象。可以用来访问 Worker 的属性和方法。self.onmessage
:指定当 Worker 收到主线程的消息时的处理函数。self.postMessage
:向主线程发送消息。self.close
:关闭 Worker 自身。self.importScripts
:加载额外的 JavaScript 脚本。
// worker.js self.onmessage = function(e) { console.log('Message from main thread:', e.data); const result = e.data + ' processed'; self.postMessage(result); }; // Optionally, you can load additional scripts if needed // self.importScripts('additional-script.js');
总结
- 主线程:负责创建和管理 Worker,处理主线程的任务。
- Web Worker 线程:负责后台任务的执行,通过
self
访问 Worker 的全局上下文,并与主线程通信。
Web Worker 是html5的新特性,利用多核的CPU的并行代码,他们之间都是独立运行的,但是可以通过消息传递进行通信。
转载自:https://juejin.cn/post/7400322144913555508