在 Transformers.js 中试验跨源存储 API

Transformers.js 通过面向具体任务的流水线,让 Web 开发者在应用中使用 Transformer 模型。在浏览器中运行推理时,先创建 pipeline() 实例,并指定任务。下面是自动语音识别(ASR)流水线的例子。

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 默认模型解析逻辑,它也是 ASR 的默认模型。

模型资源

在浏览器中运行这个示例时,Transformers.js 会自动下载并缓存相关模型资源和 Wasm 文件。原文的 Chrome DevTools「Cache storage」截图展示了访问应用后的缓存。重新加载页面时,资源来自 Cache API,模型几乎立即返回结果。

因为这个模型很常见,同一用户访问的多个应用可能都会使用它。原文提供了部署在 https://rawcdn.rawgit.net 这一不同源上的同一个示例。当用户访问第二个源,浏览器仍需重新下载并缓存所有模型资源,即使每个字节都完全相同。这个小例子就产生了 177 MB 的重复下载和存储;在 DevTools 的 Application 面板中可以看到。应用越多,累积的消耗就越大。

Wasm 运行时资源

再加一条情感分析流水线。未指定模型时,默认模型解析会选用 Xenova/distilbert-base-uncased-finetuned-sst-2-english。

const classifier = await pipeline('sentiment-analysis');
const sentiment = await classifier(result.text);
console.log(sentiment);

这两个 AI 模型完全不同,却都依赖 Transformers.js 底层 ONNX Runtime 的同一个 WebAssembly 运行时文件 ort-wasm-simd-threaded.asyncify.wasm,大小为 4,733 kB。原文在另一源上提供的扩展示例,在 Network 面板中也会显示运行时被重新下载和缓存。

因此,即便不同应用不共用同一个 AI 模型,浏览器仍可能对已经下载的共享 Wasm 资源发起重复请求,并再次将它们存到硬盘上。

缓存隔离

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。原文写作时使用的是:

https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm

也许你会认为,只要最终资源 URL 相同,即便应用来自不同源,也应命中同一份缓存。但浏览器早已不再这样处理缓存。Chrome 的缓存分区文章解释了原因:隔离缓存可以防止计时攻击。HTTP 请求的响应时间可能透露浏览器此前是否访问过同一资源,造成安全和隐私泄漏。

Chrome 的实现

各浏览器实现可能不同。Chrome 除资源 URL 外,还使用网络隔离键(Network Isolation Key)作为缓存键的一部分。这个键由顶层站点和当前框架站点组成。原文的两个示例虽然使用相同的 Wasm URL,但缓存键如下:

网络隔离键:顶层站点 网络隔离键:当前框架站点 资源 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

网络隔离键不相同,缓存就无法命中,产生重复下载和重复存储。这正是 Cross-Origin Storage 提案希望解决的问题。

跨源存储 API 登场

Cross-Origin Storage(COS)引入专用接口 navigator.crossOriginStorage,供 Web 应用跨源边界保存和读取大型文件。文件的标识是加密哈希,而不是 URL。

以哈希识别文件十分关键。在 https://googlechrome.github.io 下载过的 Wasm 运行时,可以被识别为与 https://rawcdn.rawgit.net 准备请求的文件相同;两边从什么地址下载它都不影响识别。基本流程如下:

const hash = {
  algorithm: 'SHA-256',
  value: '8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4',
};

try {
  const handle = await navigator.crossOriginStorage.requestFileHandle(hash);
  // Cache hit! Get the file as a Blob and use it directly.
  const fileBlob = await handle.getFile();
} catch {
  // Cache miss. Download from network, then store for next time.
  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() 可直接读取文件。返回的 File 继承自 Blob。若不存在,则回退到网络,并写入 COS,供下一个需要它的应用使用;这个应用可能是当前应用,也可能是完全不同源上的其他应用。

接口有意模仿 File System Standard 中的 FileSystemDirectoryHandle.getFileHandle(),即源私有文件系统(OPFS)里常见的接口。hash 与 OPFS 的 name 一样,唯一标识资源;options.create 未指定或为 false 时只读,为 true 时表示准备写入。

控制谁能读取什么

并不是所有资源都应全局共享。存储文件时的 origins 选项控制可见范围。

  • origins: '*' 允许任何源按哈希找到文件。对于这里的公开模型和 Wasm 运行时,这正好让各个应用受益于同一份缓存。
  • origins: ['https://write.example.com', 'https://calculate.example.com'] 只允许指定站点读取,适合公司自有站点间共享专有资源,例如商业办公套件里的专有校对模型。
  • 省略 origins 时,只能由同站(same-site)的源读取。它适合组织各子域共享资源,而不跨越组织边界的场景。

可见范围只能扩大,不能缩小。已经全局可用的文件,后来再用受限 origins 保存,缩小范围的尝试会被静默忽略,防止恶意参与者限制公共资源。反过来,受限资源可以扩大可见范围。

任何站点都可以对同一哈希调用 requestFileHandle(),设置 create: true 和更宽的 origins;哈希并不是秘密。在浏览器验证写入文件的哈希匹配后,文件便可向更广范围开放。但执行升级的站点仍必须通过返回的句柄写入完整文件,避免利用升级路径作为旁路,探测某个文件是否已经存在。

设计中自带完整性校验

浏览器写入文件时会验证哈希。内容与声明哈希不匹配,写入就会报错。这使完整性校验自动发生:应用从 COS 读取的字节正是所预期的内容,等同于网络下载后自行计算哈希获得的保证。

在 Transformers.js 场景中还有额外价值:多数应用下载权重后并没有实用的途径确认 CDN 返回了正确字节。COS 中每个文件写入时都经过验证,无论来自官方 Hugging Face CDN,还是其他站点自托管的镜像。

兼顾隐私与实用性

共享缓存也带来问题:任何站点都能按哈希探测文件,攻击者是否可通过某个游戏引擎 Wasm 模块是否被缓存,推断用户浏览历史?COS 采用两种互补措施。

  • 来源范围:不应公开探测的专有资源,不应使用 origins: '*'。开发者应根据资源性质选择范围。
  • 可用性门控:即使声明为全局可用,若文件没有在足够多不同源出现,浏览器也可能不确认它存在。只出现在一两个站点的文件可能成为跨站标识符,因此即使磁盘上实际保存了文件,浏览器仍可能像不存在一样返回错误。

Chrome 团队了解不常见资源可能造成的隐私泄漏,计划限制可以缓存的具体资源;按原文,具体缓解机制仍在制定。

错误不是存在与否的确定答案。它可能意味着「没有保存」,也可能是「已经保存,但浏览器不透露」。应用应一律回退到网络。

这对 Transformers.js 示例意味着什么

4,733 kB 的 Wasm 运行时由所有 Transformers.js 应用共用,与模型选择无关。第一个应用下载后,以 SHA-256 哈希和 origins: '*' 保存;之后来自任意源的应用便可找到同一文件。

Whisper 重复下载的 177 MB 权重也一样:第一次下载,第二次通过哈希识别,便可在毫秒级从 COS 提供。情感分析模型也遵循相同机制。

Transformers.js 已在库层面试验 COS。PR #1549 加入了需显式启用的实验缓存后端,在创建流水线之前设置一行即可:

import { env, pipeline } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0";

// 👇 Opt in to the experimental Cross-Origin Storage cache backend.
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);

选项中的 experimental_ 前缀是刻意的:底层浏览器 API 尚未标准化,可能在没有主版本升级时变化。启用后,Transformers.js 会获取由 Xet 跟踪的大型 ONNX 权重文件的原始 Xet 指针,提取 oid sha256: 字段,以该哈希作为 navigator.crossOriginStorage 的键。

模型已经由另一站点存入 COS 时,可立即读取,无需网络往返;否则进行普通下载,再保存供后续调用者使用。这样,两种模型及 Wasm 运行时无论有多少源请求,都只需传输一次。

灵活选择模型

虽然示例用 Xenova/whisper-tiny.en,用户的 COS 中可能已缓存其他 Whisper 变体,例如大得多的 Xenova/whisper-large-v3。Transformers.js 的 Model Registry 允许灵活选择。

如果 Xenova/whisper-tiny.en、whisper-medium.en 或 Xenova/whisper-large-v3 都能满足应用需求,可以查询各模型关联文件,探测它们在 COS 中的存在情况——可能只缓存了一部分,也可能全部齐备——再决定使用哪个模型。ModelRegistry.is_pipeline_cached() 直接集成了 COS 和 Cache API,让这个操作更方便。

现在就试试

按原文当时的状态,浏览器尚无原生实现。可以通过上述扩展注入 polyfill,并参阅扩展源码与使用说明。

安装后,先打开启用 COS 的第一个示例,等待模型加载,再打开原文链接中的第二源示例。原文演示中,原本重复下载的 177 MB 模型会改为从 COS 在毫秒级提供;扩展弹出窗口可查看共享情况。

切换到「View by Resource」,可以看到 SHA-256 为 950978b1dbcbf250335358c1236053ba19a7f7849b33dc777f4421b72b7626fa 的资源,被 https://googlechrome.github.io 与 https://rawcdn.rawgit.net 共享。对照 Hugging Face 上的哈希,可以确认这是 decoder_model_merged.onnx。

当前扩展主要面向熟悉技术的用户。未来浏览器若实现该接口,会在设置页面提供更友好的集成。原文的弹出窗口截图展示了资源哈希和两个共享它的源。

参与试验与反馈

开发 Transformers.js 应用时,在首次 pipeline() 调用前设置 env.experimental_useCrossOriginStorage = true,安装扩展,便可观察 Network 面板中的重复下载是否消失。每个选择参与的站点,都可能帮助其他站点的用户节省时间和传输成本。

原文把启用试验称为「完全无风险」,其给出的依据是:用户没有安装扩展、API 不受支持时,代码会回退到默认 Cache API 路径。这是对回退机制的描述,不构成对扩展安装或未定稿接口的全面安全保证。

其他项目也在试验 COS:WebLLM 需要显式启用,可参阅文档;wllama 自动使用,见 PR #248。

Chrome 团队正在考虑原生实现 COS。提案仍处于早期,欢迎反馈接口及提案形态。可以到 COS 仓库提交问题、表达支持或提交 PR。

原文:Experimenting with the proposed Cross-Origin Storage API in Transformers.js。作者:Google Chrome 团队开发者关系工程师 Thomas Steiner;发布日期:2026 年 6 月 23 日。中文译稿依据作者原文及用户已确认的转载授权制作。原文页面未列出文章专用许可证;各模型、扩展及依赖的许可证以其项目为准。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容