Partytown:通过ServiceWorker加速Web的原理解析

/* Partytown 0.10.2-dev1727590485751 - MIT builder.io */
Object.freeze((obj => {
    const properties = new Set;
    let currentObj = obj;
    do {
        Object.getOwnPropertyNames(currentObj).forEach((item => {
            "function" == typeof currentObj[item] && properties.add(item);
        }));
    } while ((currentObj = Object.getPrototypeOf(currentObj)) !== Object.prototype);
    return Array.from(properties);
})([]));

const resolves = new Map;

const swMessageError = (accessReq, $error$) => ({
    $msgId$: accessReq.$msgId$,
    $error$: $error$
});

const httpRequestFromWebWorker = req => new Promise((async resolve => {
    const accessReq = await req.clone().json();
    const responseData = await (accessReq => new Promise((async resolve => {
        const clients = await self.clients.matchAll();
        const client = ((clients, msgId) => {
            const tabId = msgId.split(".").pop();
            let client = clients.find((a => a.url.endsWith(`?${tabId}`)));
            client || (client = [ ...clients ].sort(((a, b) => a.url > b.url ? -1 : a.url < b.url ? 1 : 0))[0]);
            return client;
        })([ ...clients ], accessReq.$msgId$);
        if (client) {
            const timeout = 12e4;
            const msgResolve = [ resolve, setTimeout((() => {
                resolves.delete(accessReq.$msgId$);
                resolve(swMessageError(accessReq, "Timeout"));
            }), timeout) ];
            resolves.set(accessReq.$msgId$, msgResolve);
            client.postMessage(accessReq);
        } else {
            resolve(swMessageError(accessReq, "NoParty"));
        }
    })))(accessReq);
    resolve(response(JSON.stringify(responseData), "application/json"));
}));

const response = (body, contentType) => new Response(body, {
    headers: {
        "content-type": contentType || "text/html",
        "Cache-Control": "no-store"
    }
});

self.oninstall = () => self.skipWaiting();

self.onactivate = () => self.clients.claim();

self.onmessage = ev => {
    const accessRsp = ev.data;
    const r = resolves.get(accessRsp.$msgId$);
    if (r) {
        resolves.delete(accessRsp.$msgId$);
        clearTimeout(r[1]);
        r[0](accessRsp);
    }
};

self.onfetch = ev => {
    const req = ev.request;
    const url = new URL(req.url);
    const pathname = url.pathname;
    if (pathname.endsWith("sw.html")) {
        ev.respondWith(response('<!DOCTYPE html><html><head><meta charset="utf-8"><script src="./partytown-sandbox-sw.js?v=0.10.2-dev1727590485751"><\/script></head></html>'));
    } else {
        pathname.endsWith("proxytown") && ev.respondWith(httpRequestFromWebWorker(req));
    }
};

这个 Service Worker 脚本主要与 Partytown 相关,Partytown 是一个用于在 Web Worker 或 Service Worker 中运行第三方脚本的库,以减少主线程阻塞,提升网页性能。以下是对其作用的详细分析:

主要作用

  1. 初始化和配置 Partytown 环境
  • 脚本开头通过 Object.freeze 冻结了一个函数的执行结果,该函数提取了某个对象的原型链中的所有方法名(function 类型属性),并返回为数组。这可能是 Partytown 用于初始化或限制某些对象的行为,确保不可变性或安全性。
  1. 处理 Service Worker 生命周期
  • self.oninstall:调用 self.skipWaiting(),使 Service Worker 立即进入激活状态,跳过等待阶段。
  • self.onactivate:调用 self.clients.claim(),让 Service Worker 立即接管所有客户端(例如页面或标签页),确保新版本的 Service Worker 生效。
  1. 消息通信机制
  • self.onmessage:处理来自客户端(例如网页)的消息。Service Worker 使用 resolves Map 存储消息 ID 对应的 Promise 解析器和超时计时器。
  • 当收到消息(ev.data)时,查找对应的 msgId,清除超时计时器,并调用存储的 resolve 函数处理响应数据。
  1. 拦截和处理网络请求
  • self.onfetch:拦截客户端发起的 fetch 请求,并根据请求路径执行不同逻辑:
    • 如果请求路径以 sw.html 结尾,返回一个简单的 HTML 页面,包含一个指向 partytown-sandbox-sw.js<script> 标签。这可能是 Partytown 的沙箱环境入口,用于加载其他脚本。
    • 如果请求路径以 proxytown 结尾,调用 httpRequestFromWebWorker 函数处理请求。
  1. 代理第三方脚本请求
  • httpRequestFromWebWorker:处理以 proxytown 结尾的请求,通常用于代理第三方脚本的网络请求。
    • 从请求中提取 JSON 数据(accessReq)。
    • 使用 self.clients.matchAll() 获取所有客户端,并根据 msgId 找到目标客户端(通过 URL 中的 tabId 匹配)。
    • 如果找到客户端,将请求数据通过 postMessage 发送给客户端,并设置 120 秒(12e4 毫秒)的超时机制。
    • 如果超时或未找到客户端,返回错误响应(TimeoutNoParty)。
    • 最终返回 JSON 格式的响应,设置 content-typeapplication/jsonCache-Control: no-store

具体用途

  • Partytown 的核心功能:这个 Service Worker 是 Partytown 框架的一部分,用于将第三方脚本(例如分析工具、广告脚本等)的执行从主线程转移到 Web Worker 或 Service Worker 中,从而减少对主线程的阻塞,提高页面性能。
  • 消息代理:充当客户端与 Web Worker 之间的通信桥梁,处理第三方脚本的请求和响应。
  • 沙箱环境:通过 sw.html 提供一个沙箱入口,可能用于加载 Partytown 的其他脚本或隔离第三方代码的执行。
  • 动态路由:根据请求路径(sw.htmlproxytown)决定如何处理,灵活支持不同类型的请求。

技术细节

  • 超时机制:每个消息请求有 120 秒的超时时间,防止请求挂起。
  • 客户端匹配:通过 tabId 匹配客户端,如果没有匹配到特定客户端,则选择 URL 按字母顺序排序的第一个客户端作为默认目标。
  • 响应头:设置 Cache-Control: no-store 确保响应不被缓存,适合动态内容。
  • 错误处理:通过 swMessageError 返回标准化的错误消息,包含 msgId 和错误信息(如 TimeoutNoParty)。

总结

这个 Service Worker 是 Partytown 框架的核心组件,用于:

  1. 管理 Service Worker 生命周期(安装、激活)。
  2. 提供沙箱环境(sw.html)以加载 Partytown 脚本。
  3. 代理第三方脚本的网络请求(proxytown),通过消息通信在客户端和 Web Worker 之间传递数据。
  4. 确保高效、隔离的第三方脚本执行,优化网页性能。

发表评论

人生梦想 - 关注前沿的计算机技术 acejoy.com 🐾 步子哥の博客 🐾 背多分论坛 🐾 知差(chai)网 🐾 DeepracticeX 社区 🐾 老薛主机 🐾