Partytown调研说明

什么是PartyTown

Partytown 是一个主要用于前端性能优化的工具,它通过将第三方脚本放入web worker中运行,来将主线程与第三方脚本的执行隔离开,从而加速页面加载和交互。

参考文档

  1. https://github.com/BuilderIO/partytown
  2. https://developer.aliyun.com/article/1034800
  3. https://blog.miniasp.com/post/2023/01/27/Partytown-Run-Third-Party-Scripts-From-Web-Worker

如何使用

1. 引入包

import { Partytown } from '@builder.io/partytown/react';

2. 在Head最顶端引入<Partytown />

3. 第三方的script标签的type设置为type=‘text/partytown’

import Head from 'next/head';
import { Partytown } from '@builder.io/partytown/react';

const Home = () => {
  return (
    <>
      <Head>
        <title>My App</title> 
        <Partytown debug forward={['dataLayer.push']} />
        <script src='https://example.com/analytics.js' type='text/partytown' />
      </Head>
      <main>...</main>
    </>
  );
};

export default Home;

实现原理

Partytown 依赖于 Web Workers、Service Workers、JavaScript 代理 以及 webWorker和主线程的通信层。
实现 Web Worker 和主线程之间进行同步通信目前他们的文档展示了两种方式:

  • Atomics方案(优先、速度更快)。
  • (降级)同步 xhr 请求+ Service Workers 。

Web Worker与主线程之间的postMessage是异步通信,因此比如采用中间通信层达到同步的效果。

1. 通过 script 标记上的 type=“text/partytown” 属性来禁止脚本在主线程上运行。仍然会发起脚本的JS资源请求,但是不会执行。
2. PartyTown在webworker中执行脚本,支持动态添加:使用ptupdate事件通知partyTown检测。
window.dispatchEvent(new CustomEvent('ptupdate'));
3. Partytown检测是否可以使用 Atomics ,如果不行则降级使用ServiceWorker,加载 时采用对应的构建。
4. Partytown 创建 Web Worker 并且获得需要执行的第三方脚本。
5. Web Worker 创建 JavaScript Proxy代理来复制和转发对主线程 API 的调用(例如 DOM 操作)。

举例:
例如在 Web Worker 中获取 document.title 会经过以下步骤:
webworker在线程内定义一个document变量,使用 Proxy 代理拦截get / set方法。(大概类似下方),在get方法拦截后,从主线程获取document.title再返回。

const document = new Proxy({}, {
  get(target, property) {
    // 当尝试访问 document 的属性时,拦截该操作
    console.log(`访问了 document 的属性 ${property}`);
    
    // 阻塞同步 发送消息到主线程来执行实际的 DOM 操作
    
    return get_value;
  },

  // 要设置改动document上的值,则触发set拦截
  set(target, property, value) {
    console.log(`设置了 document 的属性 ${property}${value}`);
    
    // 同步发送消息到主线程等操作,把变化传输过去。
    
    return true;  // 设置成功
  }
});

而get / set方法中使用注释说明的“同步方法传送消息”,就是指Atomice方案和serviceWorker +XHR同步请求的方案两种。

整个流程虽然比较繁琐,但是其好处就是在Web Worker 运行的 JS 来说,其访问 DOM API 是同步的,完全和主线程一样,就不必重写 JS 来处理 DOM API了。

缺点在于:
1. 其支持的ducoument API有限,兼容性问题仍然存在。
2. 如果SDK内部存在大量的主线程环境变量交互,仍然可能会阻塞主线程并且加重线程之间消息传递的开销。(可以后续待观察)
3. 并且第三方SDK可能会自主变动,可能出现测试成功上线,但是某一天突然报错失败的情况。(是否有partytown执行失败的容错)
Atomics方案
  1. 遇到使用主线程API的情况,使用 Atomics.store() 和 postMessage() 将数据发送到主线程,并运行 Atomics.wait()。
  2. Web Worker 从主线程接收到结果数据,执行 Atomics.load()。
  3. 从 Web Worker 上执行的代码的角度来看,一切都是同步的,并且对文档的每次调用都是阻塞的。
serviceWorker +XHR同步请求方案
  1. 遇到使用主线程API的情况,使用同步的 XHR 发起请求
  2. 然后 Service Worker 监听onFetch拦截请求,通过 postMessage异步通信发送到主线程。
  3. 主线程返回结果,响应 Web Worker 的请求。(webworker的请求不是同步的吗?为什么sw是异步发送?)
  4. 从 Web Worker 上执行的代码的角度来看,一切都是同步的,并且对文档的每次调用都是阻塞的。

使用缺点 & 注意自测事项

  1. party可以使webworker同步地访问浏览器环境变量和DOM节点,解决了webworker无法使用环境变量的问题。
    (不一定准确)根据他们github上提供的单元测试用例,看起来它支持的工作线程中的环境变量代理如下:
  environments[ctx.winId] = {
    $winId$: ctx.winId,
    $parentWinId$: ctx.winId,
    $window$: ctx.window,
    $document$: ctx.document,
    $documentElement$: ctx.document.documentElement,
    $head$: ctx.document.head,
    $body$: ctx.document.body,
    $location$: ctx.window.location,
    $visibilityState$: 'visible',
    $createNode$: () => null as any,
    $isSameOrigin$: true,
  };
  1. partytown的兼容性和降级方案。
  2. SDK下载 / 解析完毕后是否能够正常使用,如果不能使用的容错 / 上报方案。
    可能出现的不能使用的场景:
  • SDK解析时因为兼容性或者其他问题失败。
  • SDK解析完毕,但是调用第三方方法失败。
  • webworker线程因为SDK解析出错 / 或者其他异常线程报错 / 退出。
  1. 是否存在频繁沟通主线程造成额外开销导致负优化。

具体实现方案

1. 服务器源站托管partytown的静态lib文件

因为partytown中需要使用serviceworker,必须把sw的工作线程文件放在服务器下,可以从我们站点根路径直接访问的源文件,所以他们的静态文件不能托管到CDN。
说明:https://partytown.builder.io/copy-library-files
为此,partytown提供了几个方案:

1. webpack打包插件
// webpack.config.js
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const partytown = require('@builder.io/partytown/utils');

module.exports = {
  plugins: [
    new CopyPlugin({
      patterns: [
        {
          from: partytown.libDirPath(),
          to: path.join(__dirname, 'public', '~partytown'),
        },
      ],
    }),
  ],
};
2. 前置npm命令
{
  "scripts": {
    "build": "npm run partytown && next build",
    "partytown": "partytown copylib public/~partytown"
  }
}
  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值