什么是PartyTown
Partytown 是一个主要用于前端性能优化的工具,它通过将第三方脚本放入web worker中运行,来将主线程与第三方脚本的执行隔离开,从而加速页面加载和交互。
参考文档
- https://github.com/BuilderIO/partytown
- https://developer.aliyun.com/article/1034800
- 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方案
- 遇到使用主线程API的情况,使用 Atomics.store() 和 postMessage() 将数据发送到主线程,并运行 Atomics.wait()。
- Web Worker 从主线程接收到结果数据,执行 Atomics.load()。
- 从 Web Worker 上执行的代码的角度来看,一切都是同步的,并且对文档的每次调用都是阻塞的。
serviceWorker +XHR同步请求方案
- 遇到使用主线程API的情况,使用同步的 XHR 发起请求
- 然后 Service Worker 监听onFetch拦截请求,通过 postMessage异步通信发送到主线程。
- 主线程返回结果,响应 Web Worker 的请求。(webworker的请求不是同步的吗?为什么sw是异步发送?)
- 从 Web Worker 上执行的代码的角度来看,一切都是同步的,并且对文档的每次调用都是阻塞的。
使用缺点 & 注意自测事项
- 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,
};
- partytown的兼容性和降级方案。
- SDK下载 / 解析完毕后是否能够正常使用,如果不能使用的容错 / 上报方案。
可能出现的不能使用的场景:
- SDK解析时因为兼容性或者其他问题失败。
- SDK解析完毕,但是调用第三方方法失败。
- webworker线程因为SDK解析出错 / 或者其他异常线程报错 / 退出。
- 是否存在频繁沟通主线程造成额外开销导致负优化。
具体实现方案
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"
}
}