在 Transformers.js 中实验提议的跨源存储 API
Experimenting with the proposed Cross-Origin Storage API in Transformers.js
Transformers.js 在浏览器中运行 AI 模型时,不同来源的 Web 应用会重复下载并缓存相同的模型资源(如 Xenova/whisper-tiny.en)和 Wasm 运行时文件(如 4,733 kB 的 ort-wasm-simd-threaded.asyncify.wasm),即使资源 URL 相同,浏览器因 Network Isolation Key 隔离缓存,单次 demo 就产生 177 MB 冗余下载和存储。Cross-Origin Storage API 是一项早期提案,旨在让跨来源应用共享缓存的模型和运行时资源。目前该 API 尚未在浏览器原生实现,但可通过 Chrome 扩展注入 polyfill 进行实验。
这个Chrome提案让不同网站的AI模型共享缓存,对用Transformers.js的Web开发者是切实的性能改进,但还只是早期实验。
Transformers.js 为 Web 开发者提供了一种简单的方式,通过任务专用 pipeline 在 Web 应用中发挥 Transformer 的能力。要在浏览器中运行推理,开发者需要创建一个 pipeline() 实例,并指定他们想要使用该 pipeline 的任务。举一个具体的例子,下面的代码片段展示了如何设置一个自动语音识别(ASR)pipeline。
import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0';
const asr = await pipeline(
'automatic-speech-recognition',
'Xenova/whisper-tiny.en',
{ device: 'webgpu' },
);
const result = await asr('jfk.wav');
console.log(result);
缓存挑战
你会在源代码中注意到,我指定了Xenova/whisper-tiny.en作为模型,对于常见的英语自动语音识别任务来说,这是一个相当不错的选择。事实上,它甚至是那个根据 Transformers.js 的默认模型默认模型解析,依据所链接的摘录.
模型资源
当你在浏览器中运行此示例时,Transformers.js 会自动负责下载并缓存相关的模型资源和 Wasm 文件。下面的截图展示了访问该应用后 Chrome DevTools 的缓存存储部分。当你重新加载页面时,资源将从Cache API提供,模型几乎可以立即返回结果。
然而,Xenova/whisper-tiny.en作为一个热门模型(而且,如前所述,甚至是Transformers.js 中默认的 ASR 模型),你可以想见,你访问的应用中不止一个会用到它。为了模拟这种情况,这里给出之前那个相同的示例应用,但它由不同的源提供服务。当你访问这个不同源的应用时,浏览器无法几乎即时可用,而是不得不再次下载并缓存所有模型资源,即使它们与之前逐字节完全相同。即便在这个玩具示例中,这也累计造成了 177 MB 的重复下载和存储,你可以在 Chrome DevTools 的Storage部分、Application 面板中查看。你可以想见,这会迅速累积。
Wasm 运行时资源
但情况还会更糟。让我们给这个玩具示例再加上第二条流水线:情感分析。情感分析默认使用Xenova/distilbert-base-uncased-finetuned-sst-2-english模型。由于没有指定模型,Transformers.js 的默认模型解析会自动为你选择它。
const classifier = await pipeline('sentiment-analysis');
const sentiment = await classifier(result.text);
pre.append('\n\n' + JSON.stringify(sentiment, null, 2));
两个完全不同的 AI 模型,但它们都依赖于同一个 4,733 kB 的ort-wasm-simd-threaded.asyncify.wasm WebAssembly(Wasm)运行时文件,该文件来自 Transformers.js 所构建于其上的底层 ONNX Runtime 库。在另一个源上打开扩展演示,你就会在网络标签页中注意到,Wasm 运行时同样会被重新下载并缓存。
所以,即使你运行的应用并不共享相同的 AI 模型,你的浏览器仍然会为你已经拥有的共享 Wasm 资源发出冗余请求,除此之外还会再次缓存它们,从而占用你硬盘上的空间。
缓存隔离
AI 模型资源服务
默认情况下,AI 模型资源来自 Hugging Face Hub,最终来自 Hugging Face CDN。浏览器会为诸如 https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english/resolve/main/config.json 这样的资源发出请求,随后该请求会被重定向到最终的 CDN URL,在此例中即 https://huggingface.co/api/resolve-cache/models/Xenova/distilbert-base-uncased-finetuned-sst-2-english/0b6928efcb76139cae2c6881d49cda67fe119f42/config.json?%2FXenova%2Fdistilbert-base-uncased-finetuned-sst-2-english%2Fresolve%2Fmain%2Fconfig.json=&etag=%223c36342ef1f74de2797d667c68c6b7b988d0b87c%22。
Wasm 运行时资源服务
Wasm 运行时资源默认由 jsDelivr CDN 提供。例如,在撰写本文时,ort-wasm-simd-threaded.asyncify.wasm 来自 https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm。
你可能会说,如果不同的应用即便运行在不同的源上,最终都从相同的 CDN URL 提供资源,那么只要最终 URL 相同,缓存就不应该成为问题。遗憾的是,浏览器中的缓存机制长期以来并非如此运作。文章 通过分区缓存来提升安全性与隐私性对此有详尽阐述,但本质上,缓存是按源隔离的,以防止时序攻击:网站响应 HTTP 请求所需的时间可能泄露浏览器过去是否访问过同一资源,从而使浏览器面临安全与隐私泄露的风险。
Chrome 的实现
具体实现可能因浏览器而异,但在 Chrome 中,缓存资源除了使用 资源 URL 作为键之外,还使用网络隔离键(Network Isolation Key)。网络隔离键由 顶层站点和 当前框架站点组成。以前面托管在源 https://googlechrome.github.io 和 https://rawcdn.rawgit.net 上的玩具示例为例。如果它们都使用来自 https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm 的 Wasm 运行时,它们的缓存键将如下表所示。
| 网络隔离键 | 资源 URL | |
|---|---|---|
| 顶层站点 | 当前框架站点 | |
https://googlechrome.github.io | https://googlechrome.github.io | https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm |
https://rawcdn.rawgit.net | https://rawcdn.rawgit.net | https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm |
所以,即使资源 URL 完全相同,由于网络隔离密钥不匹配,也不会命中缓存,这意味着重复下载和重复存储。这正是跨源存储提案旨在解决的挑战。
跨源存储 API 登场
💡 注意:跨源存储 API 是一个早期阶段的提案,尚未最终定稿。虽然该提案中的 API 尚未在任何浏览器中原生实现,但你不必等待就能进行实验。安装 跨源存储扩展,即可在所有页面上注入
navigator.crossOriginStoragepolyfill,并测试完整流程。
所提出的 跨源存储(COS)API 引入了一个专用 navigator.crossOriginStorage 接口,Web 应用可通过该接口跨源边界存储和检索大文件,其标识方式不是 URL,而是加密哈希。

关于加密哈希的最后一点是关键。因为 COS 通过文件的哈希而非其 URL 或来源来识别文件,所以同一个ort-wasm-simd-threaded.asyncify.wasm你在访问时下载的 Wasm 运行时https://googlechrome.github.io会被认定为与https://rawcdn.rawgit.net即将请求的那个完全相同,无论这两个来源是从哪里获取它的。参见以下代码片段,它展示了基本流程。
const hash = {
algorithm: 'SHA-256',
value: '8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4',
};
try {
const handle = await navigator.crossOriginStorage.requestFileHandle(hash);
const fileBlob = await handle.getFile();
} catch (err) {
const fileBlob = await fetch('https://cdn.jsdelivr.net/.../ort-wasm-simd-threaded.asyncify.wasm')
.then(r => r.blob());
const handle = await navigator.crossOriginStorage.requestFileHandle(
hash,
{ create: true, origins: '*' },
);
const writableStream = await handle.createWritable();
await writableStream.write(fileBlob);
await writableStream.close();
}
如果资源在 COS 中,你会得到一个 FileSystemFileHandle,你可以直接通过 getFile() 从中读取 blob(得到的 File 继承自 Blob)。如果资源不在 COS 中,你就回退到网络,并把该资源写入 COS,供下一个需要它的应用使用——可能是你的应用,也可能是另一个不相关的应用,甚至可能来自完全不同的源。
该 API 刻意按照 File System Standard 的 FileSystemDirectoryHandle.getFileHandle() 来设计,你很可能从 Origin Private File System(OPFS)API 中已经熟悉它。hash 参数与 OPFS 中的 name 参数作用相同:唯一标识一个资源。options.create 标志的用法也相同:不传或为 false 表示只读访问,true 表示你打算写入。
控制谁可以读取什么
并非每个资源都应该全局共享。COS 通过存储文件时的 origins 选项,让开发者可以精确控制可见性。
- 设置
origins: '*'会使文件全局可用。任何源都可以通过哈希找到它。对于 AI 模型资源或 Transformers.js 示例中的 Wasm 运行时来说,这是正确的选择:其核心意义就在于,Web 上的每个应用都能受益于同一份缓存副本。 - 传入一份具体的来源列表,例如
origins: ['https://write.example.com', 'https://calculate.example.com'],会限制对这些站点的访问。这非常适合在公司自有资产之间共享、且不应被其他任何人发现的专有资源,比如商业办公套件中使用的专有校对 AI 模型。 - 完全省略
origins会使该文件仅对同站来源可用。对于在组织所有子域之间共享的资源来说,这是一个合理的默认设置,但其本意并非跨越组织边界。
有一条重要规则:可见性可以升级,但绝不能降级。如果某个文件已经全局可用,那么之后尝试用受限的 origins 列表来存储它会被静默忽略。这可以防止恶意行为者重新存储某个公共资源并缩小其可用范围。反过来则是可行的:一个最初以受限 origins 列表存储的文件,之后可以变得更宽松。任何站点,而不仅仅是原始存储方,都可以针对同一哈希(哈希并非机密)调用 requestFileHandle(),并带上 create: true 和更宽松的 origins 值,而由于浏览器会验证哈希是否匹配,该资源从那一刻起便对更广泛的受众可用。请注意,执行升级的站点必须仍然通过返回的句柄写入完整文件。这一要求的存在是为了防止站点利用升级路径作为侧信道,来探测某个特定文件是否已经存储在 COS 中。
设计即完整性
COS 的一个微妙但重要的特性是:当你写入文件时,浏览器会验证哈希值。如果你写入的数据与声明的哈希值不匹配,写入就会失败并报错。这使得完整性校验变得自动化:从 COS 读取文件的应用可以确信自己拿到的正是预期的字节。这与它在网络下载后自行计算哈希值所获得的保证完全相同。
在 Transformers.js 的场景中,这一点被证明有双重用处。如今,在下载模型权重之后,大多数应用没有切实可行的办法来验证 CDN 是否提供了正确的字节。有了 COS,存储中的每个文件在写入时都会被隐式验证,无论它来自哪里——是官方 Hugging Face CDN 还是某个随机网站的自托管镜像。
不牺牲实用性的隐私保护
当然,跨源共享缓存反过来也引发了与分区 HTTP 缓存相同的问题:如果任何网站都能通过哈希值探测某个文件是否存在,那么攻击者难道不能通过检查某个游戏引擎 Wasm 模块是否被缓存来了解用户的浏览历史吗?
COS 通过两种互补机制来解决这个问题:
- 首先,
origins字段:不应被全局探测的专有资源就不应该以origins: '*'存储,通过开发者教育,开发者被鼓励在合理的情况下考虑这一点。 - 其次,可用性门控:即便是全局声明的文件,如果浏览器未在足够多不同的源上遇到过该文件,也可能拒绝确认其存在。一个只出现在一两个网站上的文件仍可能被用作跨站标识符,因此浏览器可能直接返回错误,仿佛该文件根本不存在,而不管磁盘上实际有什么。在 Chrome 团队,我们意识到罕见资源可能导致的隐私泄露问题,并计划总体上通过限制哪些具体资源可以被缓存来缓解这一问题。具体的缓解措施仍在细化中。
关键在于,这意味着错误并非确定性答案。它可能表示“未存储”,也可能表示“已存储,但浏览器不告诉你”。应用应始终以相同方式处理:回退到网络。
这对 Transformers.js 示例意味着什么
回到之前的玩具示例:ort-wasm-simd-threaded.asyncify.wasm 运行时大小为 4,733 kB,被每个由 Transformers.js 驱动的应用共享,无论它使用哪个 AI 模型。借助 COS,第一个加载它的应用只需下载一次,并以 origins: '*' 将其存储在 SHA-256 哈希下。之后每一个应用,无论是在 https://googlechrome.github.io、https://rawcdn.rawgit.net 还是任何其他源上,都能立即在 COS 中找到它。那 177 MB 的重复 Whisper 模型权重呢?同样如此:Xenova/whisper-tiny.en 只需下载一次,第二次便通过哈希被识别,并在毫秒内从 COS 提供。当然,Xenova/distilbert-base-uncased-finetuned-sst-2-english 也是如此。
Transformers.js 本身已经在库层面率先试用了 COS API。Pull request #1549 引入了一个实验性的 COS 缓存后端,置于一个需主动开启的开关之后。启用它只需在设置 pipeline 之前加一行代码:
import { env, pipeline } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0";
env.experimental_useCrossOriginStorage = true;
const asr = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny.en', { device: 'webgpu' });
const result = await asr('jfk.wav');
console.log(result);
设置该标志后,Transformers.js 会为每个 Xet 跟踪的 模型文件(即大型 ONNX 权重文件)解析 SHA-256 哈希:它先获取原始 Xet 指针文件(示例原始指针文件),并从中提取其 oid sha256: 字段。随后,它将该哈希用作 navigator.crossOriginStorage 的键。如果该模型已在 COS 中(因为另一个站点先前已将其存储在那里),则会立即提供,无需网络往返。如果不在,则回退到常规下载,并将结果存入 COS,供下一个调用者使用。在这个玩具示例中,实际优势在于:无论有多少不同的源请求它们,Xenova/whisper-tiny.en 和 Xenova/distilbert-base-uncased-finetuned-sst-2-english(当然还有 ort-wasm-simd-threaded.asyncify.wasm)都只需穿越网络一次。
请注意该标志上的 experimental_ 前缀。这是有意为之,表明底层浏览器 API 尚未标准化,可能会在没有主版本号变更的情况下发生变化。
立即试用
COS API 尚未在任何浏览器中原生实现,但你不必等待就能体验它。安装 Cross-Origin Storage 扩展,即可在所有页面上注入 navigator.crossOriginStorage polyfill 并测试完整流程。你可以查看该扩展的源代码,并按照使用说明开始使用。
安装扩展后,你现在就可以体验完整的端到端流程:打开第一个启用 COS 的玩具示例,让它加载Xenova/whisper-tiny.en,然后打开来自第二个源的启用 COS 的玩具示例。不再是之前看到的 177 MB 重新下载,模型会在几毫秒内从 COS 提供。当你打开扩展的弹出窗口时,你可以看到 COS 正在运作。如果你按资源查看,你可以看到 SHA-256 哈希为950978b1dbcbf250335358c1236053ba19a7f7849b33dc777f4421b72b7626fa的资源在https://googlechrome.github.io和https://rawcdn.rawgit.net之间共享。这可能不太明显,但你可以通过比较 Hugging Face 上的 SHA-256 哈希来验证,你看到的是https://huggingface.co/Xenova/whisper-tiny.en/blob/main/onnx/decoder_model_merged.onnx。目前,该扩展主要面向像你这样的高级用户。一旦在浏览器中实现,浏览器的设置页面中将会有更友好的集成。下面的截图显示了扩展的弹出窗口,其中按资源查看标签页处于激活状态,你可以看到共享资源及其哈希,以及在其 COS 缓存中拥有该资源的两个源。
行动号召
如果你正在构建自己的 Transformers.js 应用,行动号召很简单:添加env.experimental_useCrossOriginStorage = true在你的第一次pipeline()调用之前,安装该扩展,然后看着重复下载从你的 Network 标签页中消失。每一个选择接入的网站,都会让其他所有网站用户的体验变得更快、更便宜。选择接入完全没有风险:如果因为用户没有安装 COS 扩展而不支持 COS API,代码只会回退到默认路径(即Web CacheAPI)。
Transformers.js 并非唯一在试验 COS 的项目。WebLLM(需主动启用,参见文档)和 wllama(自动启用,参见PR)同样对这一拟议 API 感到兴奋。
在 Chrome 团队,我们正在考虑在浏览器中原生实现 COS API。作为一个早期阶段的提案,我们欢迎针对该 API 以及提案本身形态的反馈。Cross-Origin Storage 仓库是提交 issue、表达支持或提交 PR 的地方。
![]()
Transformers.js v4:现已上线 NPM!
来源:Hugging Face:Blog(RSS) · huggingface.co