likes
comments
collection
share

前端性能优化篇—必学之Web Worker我们都知道AI生成是一件很耗时的任务。因为JS是一门单线程的语言,在我们调用L

作者站长头像
站长
· 阅读数 50

我们都知道AI生成是一件很耗时的任务。因为JS是一门单线程的语言,在我们调用LLM的时候,会有少则几秒,长则几分钟的等待时间。如果把这个任务给JS去执行的话,那界面将会阻塞,用户将无法操作界面,前端最需要做的就是用户体验,这是万万不行的。

前端性能优化篇—必学之Web Worker我们都知道AI生成是一件很耗时的任务。因为JS是一门单线程的语言,在我们调用L

让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)

    1. 调用栈 (Call Stack) 调用栈是一个 LIFO(后进先出)结构,用于存储在执行的函数调用。同步代码会按顺序在调用栈中执行。
    2. 任务队列 (Task Queue) 任务队列用于存放需要执行的异步任务,当调用栈为空时,事件循环会从任务队列中取出任务并将其放入调用栈中执行。任务队列通常包括宏任务队列和微任务队列。
    3. 宏任务 (Macro Task) 宏任务包括整体代码脚本、setTimeoutsetInterval、I/O 操作等。这些任务会在事件循环的每一次循环中依次执行。
    4. 微任务 (Micro Task) 微任务通常包括 Promise 的回调函数、MutationObserver 等。微任务会在当前宏任务执行完后立即执行,并且在下一个宏任务开始之前执行完所有微任务。

事件循环执行过程

  1. 执行调用栈中的同步代码,直到调用栈为空。
  2. 检查微任务队列并执行所有微任务,直到微任务队列为空。
  3. 从任务队列中取出一个宏任务并执行,回到第 1 步。

以下是一个包含同步代码、宏任务和微任务的示例代码,以及事件循环的执行过程:

console.log("同步代码开始");

setTimeout(() => {
  console.log("setTimeout 宏任务");
}, 0);

Promise.resolve().then(() => {
  console.log("Promise 微任务");
});

console.log("同步代码结束");

执行顺序分析

  1. 同步代码执行

    • 执行 console.log("同步代码开始");
    • 执行 console.log("同步代码结束");
    • 调用栈中同步代码执行完毕。
  2. 微任务队列执行

    • 执行 Promise.resolve().then(...) 回调中的 console.log("Promise 微任务");
    • 微任务队列执行完毕。
  3. 宏任务队列执行

    • 执行 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. 避免主线程阻塞

即使使用异步操作 (如 setTimeoutPromise),长时间的任务仍可能导致任务队列堆积,从而间接阻塞主线程。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 与主线程之间的消息传递是通过复制数据完成的,无法直接共享内存。如果需要共享内存,可以使用 SharedArrayBufferAtomics

主线程和后台线程的不同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
评论
请登录