Bobolo
  • Home
  • Me

Web Worker Partytown

Web Worker 的应用 Partytown

7 Min Read

Sat Jan 06 2024

Written By Bobolo

Web Worker 是从后台运行的JavaScript脚本,独立于可能已运行的脚本,它不会被响应点击或其他用户交互的脚本中断,能够更有效地利用多核CPU。

这样做的好处是可以在独立线程中执行费时的处理任务,防止被用户活动打断应该运行的长任务,使主线程(通常是 UI 线程)的运行不会被阻塞/放慢。

Worker 类型

  • 专用 worker 是由单个脚本使用的 worker。该上下文由 DedicatedWorkerGlobalScope 对象表示。
  • Shared Worker: 是可以由在不同窗口、IFrame 等中运行的多个脚本使用的 worker,只要它们与 worker 在同一域中。它们比专用的 worker 稍微复杂一点——脚本必须通过活动端口进行通信。
  • Service Worker: 基本上是作为代理服务器,位于 web 应用程序、浏览器和网络(如果可用)之间。它们的目的是(除开其他方面)创建有效的离线体验、拦截网络请求,以及根据网络是否可用采取合适的行动并更新驻留在服务器上的资源。它们还将允许访问推送通知和后台同步 API。

Worker Api

Worker 接口是 Web Workers API 的一部分,一种可由脚本创建的后台任务,任务执行中可以向其创建者收发信息。

  • Worker(): 构造函数
  • Worker.onmessage: 当事件冒泡到 worker 时,监听函数被调用
  • Worker.postMessage(): 发送一条消息到最近的外层对象
  • Worker.terminate(): 立即终止 worker
  • Worker.onerror: worker 发生错误时触发
  • Worker.messageerror: 当 Worker 对象接收到一条无法被反序列化的消息时触发

以下是 MDN 的简单后台计算 Demo,可以直接打开

// index.tsx
export default component$(function WebWorker() {
  useOnWindow(
    'load',
    $(() => {
      const first = document.querySelector('#number1')
      const second = document.querySelector('#number2')
      const result = document.querySelector('.result')

      if (window.Worker) {
        const myWorker = new Worker('/worker/demo.js')
        first.onchange = () => {
          myWorker.postMessage([first.value, second.value])
          console.log('Message posted to worker')
        }
        second.onchange = () => {
          myWorker.postMessage([first.value, second.value])
          console.log('Message posted to worker')
        }
        myWorker.onmessage = e => {
          result.textContent = e.data
          console.log('Message received from worker')
        }
      } else {
        console.log("Your browser doesn't support web workers.")
      }
    })
  )
  return (
    <div class="controls" tabIndex={0}>
      <form>
        <div>
          <label for="number1">Multiply number 1: </label>
          <input type="text" id="number1" value="0" />
        </div>
        <div>
          <label for="number2">Multiply number 2: </label>
          <input type="text" id="number2" value="0" />
        </div>
      </form>
      <p class="result">Result: 0</p>
    </div>
  )
})
// worker.js
onmessage = function (e) {
  console.log('Worker: Message received from main script')
  const result = e.data[0] * e.data[1]
  if (isNaN(result)) {
    postMessage('Please write two numbers')
  } else {
    const workerResult = 'Result: ' + result
    console.log('Worker: Posting message back to main script')
    postMessage(workerResult)
  }
}

React 是否用到?

我们知道 React 在 State 更新的时候会进行 Diff 对比,这是一个比较消耗性能的地方,那 worker 能否用来进行优化?答案是不能

  • 需要共享的不可变持久数据结构、自定义 VM 调整等。JS 不太适合这种情况,因为可变的共享运行时(如原型)。
  • 调度能力、模块初始化开销(共享代码执行两次)、序列化开销和增加的复杂性

Partytown

Partytown 是一个延迟加载的库,可将资源密集型脚本放到到Web Worker中,并脱离主线程。它的目标在

  • 性能优化:
    • 释放主线程,让主线程用于主要的 Web 应用程序执行
    • 隔离长任务到 Web Worker
    • 通过将 DOM setter/getter 批量更新合并(和 React batching 一样),减少来自第三方脚本的布局影响
  • 安全性:
    • 对第三方脚本进行沙箱处理,并允许或拒绝其访问主线程 API
    • 限制第三方脚本对主线程的访问

Render

可能出现的问题 加载第三方 JavaScript

Partytown 如何运作

Web Worker 它无法直接访问可从主线程访问的 DOM API,例如window, document,或localStorage。而且传统上,主线程和 worker 间的通信是异步的。但 Partytown 它允许从 Web Worker 执行的代码同步访问 DOM 。

但是第三方库很可能会使用到 DOM Api 以及存在同步执行的代码,所以 Partytown 需要解决这两个问题,这才可以让第三方脚本继续按照原先逻辑正常工作。

同步通信与代理

目前有两种方法可以在 Web Worker 和主线程之间进行同步通信,即同步 XHR 请求与 Service WorkersAtomics 相结合。

下面我们以 Service Worker 为例,如果我们的 JS 在 Web Worker 中执行会是怎么样的流程

principle

Partytown 优先使用 Atomics,因为 Atomics 性能上比 Service Worker 更好

确定 JS

Partytown 并不会将所有的 JS 都放到 Worker 中处理,因为 Partytown 对于某些第三方脚本来说效果不错,但并不是适合每个JS。例如,一些第三方脚本可能会使用 setInterval() 循环,每隔 X 毫秒不断遍历整个文档。

因为不知道用户哪些 JS 需要处理,所以需要在 <script> 设置 type="text/partytown" 属性时,才会为 Partytown 启用。这个type属性有两件事:

  • 阻止主线程执行脚本。
  • 提供一个选择器供 Partytown 查询,例如document.querySelectorAll('script[type="text/partytown"]')

Conclusion

目前它的配置和使用都非常简单,如果项目有一些比较消耗性能的第三方 JS 脚本或者未来需要引入的时候,可以考虑看看是否能将它引入来进行优化,但需要多进行测试

Reference

Powered by Bobolo

Copyright © Bobolo Blog 2021