{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-06-23-experimenting-with-the-proposed-cross-origin-storage-api-in/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "(This is a guest post by Developer Relations Engineer [Thomas Steiner](https://blog.tomayac.com/) from the Chrome team at Google.)\n\nTransformers.js provides Web developers with a simple way to use the power of transformers in their Web apps through task-specific pipelines. To run inference in the browser, developers create an instance of [`pipeline()`](https://huggingface.co/docs/transformers.js/en/api/pipelines) and specify a task they want to use the pipeline for. As a concrete example, the following snippet shows how to set up an automatic speech recognition (ASR) pipeline.\n\n```js\nimport { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0';\n\nconst asr = await pipeline(\n  'automatic-speech-recognition',\n  'Xenova/whisper-tiny.en',\n  { device: 'webgpu' },\n);\nconst result = await asr('jfk.wav');\nconsole.log(result);\n```\n\n[![A minimalistic example of the automatic speech recognition pipeline.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/87a91qnbicf.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/87a91qnbicf.png)\n\n## The cache challenge\n\nYou will notice in the source code that I specified [`Xenova/whisper-tiny.en`](https://huggingface.co/Xenova/whisper-tiny.en) as the model, which is a very decent choice for common English automatic speech recognition tasks. In fact, it's even *the* default model according to the Transformers.js [default model resolution](https://github.com/huggingface/transformers.js/blob/main/packages/transformers/src/pipelines/index.js), as per the linked [excerpt](https://github.com/huggingface/transformers.js/blob/bc9cf7400f4f2c8695016699f879e31026ff0313/packages/transformers/src/pipelines/index.js#L151-L158).\n\n### Model resources\n\nWhen you [run this example in the browser](https://googlechrome.github.io/samples/transformersjs-automatic-speech-recognition/index.html), Transformers.js automatically takes care of downloading and caching the relevant model resources and Wasm files. The following screenshot shows the Chrome DevTools [Cache storage](https://developer.chrome.com/docs/devtools/storage/cache) section after visiting the app. When you reload the page, the resources are served from the [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache), and the model returns results almost instantly.\n\n[![The Chrome DevTools Cache storage section showing Whisper AI model resources and Wasm runtime files after visiting the app.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/otd8tt1gusb.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/otd8tt1gusb.png)\n\nHowever, `Xenova/whisper-tiny.en` being a popular model (and, as mentioned before, even being *the* ASR default model in Transformers.js), you can well imagine that more than just one app that you visit would use it. To simulate this situation, here's the same example app from before, but served from a [different origin](https://rawcdn.rawgit.net/GoogleChrome/samples/c4192bd7a3c66fc288a7b22b77acb935df00b8a1/transformersjs-automatic-speech-recognition/index.html). When you visit this different origin app, rather than being usable almost instantly, the browser instead has to download and cache all the model resources again, even if they're byte-by-byte the same as before. Even in this toy example, this adds up to 177 MB of duplicate download and storage, as you can examine in the **Storage** section of the Chrome DevTools [Application panel](https://developer.chrome.com/docs/devtools/application#open_the_application_panel). You can imagine that this quickly adds up.\n\n[![The Chrome DevTools Storage overview showing 177 MB of used storage.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/9byoniem0pw.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/9byoniem0pw.png)\n\n### Wasm runtime resources\n\nBut it gets worse. Let's add a second pipeline to the toy example: sentiment analysis. Sentiment analysis [by default](https://github.com/huggingface/transformers.js/blob/bc9cf7400f4f2c8695016699f879e31026ff0313/packages/transformers/src/pipelines/index.js#L65) uses the [`Xenova/distilbert-base-uncased-finetuned-sst-2-english`](https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english) model. By not specifying the model, Transformers.js' default model resolution automatically picks it for you.\n\n```js\nconst classifier = await pipeline('sentiment-analysis');\nconst sentiment = await classifier(result.text);\nconsole.log(sentiment);\n```\n\n[![image](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/le7l1km7o4g.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/le7l1km7o4g.png)\n\nTwo entirely different AI models, but they depend on the same 4,733 kB `ort-wasm-simd-threaded.asyncify.wasm` WebAssembly (Wasm) runtime file [from the underlying ONNX Runtime library](https://onnxruntime.ai/docs/api/js/interfaces/Env.WasmFilePaths.html#wasm) that Transformers.js is built on top of. Open the [extended demo on a different origin](https://rawcdn.rawgit.net/GoogleChrome/samples/d47114a15637383015c274e7bdcd81e1a17b0ccf/transformersjs-automatic-speech-recognition/index2.html), and you will notice in the [**Network** tab](https://developer.chrome.com/docs/devtools/network#load) how also the Wasm runtime gets downloaded and cached again.\n\n[![Chrome DevTools Network panel showing the download of the Wasm runtime resource.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/pz12g20fqeg.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/pz12g20fqeg.png)\n\nSo even if you run apps that don't share the same AI models, your browser still makes redundant requests for shared Wasm resources you already have, and on top of that also caches them again, which consumes space on your hard disk.\n\n### Cache isolation\n\n#### AI model resources serving\n\nBy default, **AI model resources** come from the [Hugging Face Hub](https://huggingface.co/docs/hub/en/models-the-hub), and ultimately the Hugging Face CDN. The browser makes a request for a resource like [`https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english/resolve/main/config.json`](https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english/resolve/main/config.json) which then gets redirected to the final CDN URL like [`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`](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) in this case.\n\n#### Wasm runtime resources serving\n\nThe **Wasm runtime resources** are served from the [jsDelivr CDN](https://www.jsdelivr.com/) by default. For example, `ort-wasm-simd-threaded.asyncify.wasm` comes from [`https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/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) at the time of this writing.\n\nNow you may say that if different apps, even though running on different origins, in the end serve their resources from the same CDN URLs, caching shouldn't be a problem, as long as the final URLs are the same. Unfortunately, this is not how caching works in browsers for a long time. The article [Gaining security and privacy by partitioning the cache](https://developer.chrome.com/blog/http-cache-partitioning) goes into all the details, but essentially, **caches are isolated by origin** to prevent timing attacks: the time a website takes to respond to HTTP requests can reveal that the browser has accessed the same resource in the past, which makes the browser vulnerable to security and privacy leaks.\n\n#### Chrome's implementation\n\nThe concrete implementation may vary by browser, but in Chrome, cached resources are keyed using a Network Isolation Key in addition to the **resource URL**. The Network Isolation Key is composed of the **top-level site** and the **current-frame site**. Take the previous toy examples hosted on the origins `https://googlechrome.github.io` and `https://rawcdn.rawgit.net`. If they both use the Wasm runtime from `https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm`, their cache keys will look like in the following table.\n\n| Network Isolation Key                  | **Resource URL**                       |                                                                                                                               |\n| -------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |\n| **Top-level site**                     | **Current-frame site**                 |                                                                                                                               |\n| ``` 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 ``` |\n| ``` 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 ``` |\n\nSo even if the resource URLs are exactly the same, since the Network Isolation Keys don't match, there's no cache hit, which means duplicate download and duplicate storage. This is the challenge that the Cross-Origin Storage proposal aims to tackle.\n\n## Enter the Cross-Origin Storage API\n\n> **💡 Note:** The Cross-Origin Storage API is an early-stage proposal that isn't final. While the proposed API is not yet natively implemented in any browser, you don't have to wait to experiment with it. Install the [Cross-Origin Storage extension](https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih) to inject the `navigator.crossOriginStorage` polyfill on all pages and test the complete flow.\n\nThe proposed **[Cross-Origin Storage](https://github.com/WICG/cross-origin-storage) (COS) API** introduces a dedicated `navigator.crossOriginStorage` interface through which web apps can store and retrieve large files across origin boundaries, identified not by a URL, but by a cryptographic hash.\n\n![The Cross-Origin Storage API logo: a stylized walking person, as typically encountered on crosswalk signs.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/klwb5fryaa.png)\n\nThat last point about cryptographic hashes is key. Because COS identifies files by their **hash** rather than by their URL or origin, the same `ort-wasm-simd-threaded.asyncify.wasm` Wasm runtime you downloaded while visiting `https://googlechrome.github.io` is recognized as identical to the one `https://rawcdn.rawgit.net` is about to request, no matter where either of the two origins fetched it from. See the following code snippet that illustrates the basic flow.\n\n```js\nconst hash = {\n  algorithm: 'SHA-256',\n  value: '8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4',\n};\n\ntry {\n  const handle = await navigator.crossOriginStorage.getFileHandle(hash);\n  // Cache hit! Get the file as a Blob and use it directly.\n  const fileBlob = await handle.getFile();\n} catch {\n  // Cache miss. Download from network, then store for next time.\n  const fileBlob = await fetch('https://cdn.jsdelivr.net/.../ort-wasm-simd-threaded.asyncify.wasm')\n    .then(r => r.blob());\n  const handle = await navigator.crossOriginStorage.getFileHandle(\n    hash,\n    { create: true, origins: '*' },\n  );\n  const writableStream = await handle.createWritable();\n  await writableStream.write(fileBlob);\n  await writableStream.close();  \n}\n```\n\nIf the resource is in COS, you get back a [`FileSystemFileHandle`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle) from which you can read the blob directly via [`getFile()`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/getFile) (the resulting [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) inherits from [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob)). If the resource is not in COS, you fall back to the network, and write the resource into COS for the next app that needs it, which could be your app, or another unrelated app, potentially on a completely different origin.\n\nThe API is deliberately shaped after the [File System Standard](https://fs.spec.whatwg.org/)'s [`FileSystemDirectoryHandle.getFileHandle()`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/getFileHandle) you likely are familiar with from the [Origin Private File System](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) (OPFS) API. The `hash` parameter plays the same role as the `name` parameter in OPFS: uniquely identifying a resource. The `options.create` flag works the same way: absent or `false` for read-only access, `true` when you intend to write.\n\n### Control who can read what\n\nNot every resource should be globally shared. COS gives developers precise control over visibility through the `origins` option when storing a file.\n\n- Setting `origins: '*'` makes a file **globally available**. Any origin can find it by hash. This is the right choice for AI model resources or the Wasm runtime in the Transformers.js example: the whole point is that every app on the Web benefits from a single cached copy.\n- Passing a specific list of origins, like `origins: ['https://write.example.com', 'https://calculate.example.com']`, **restricts** access to those sites. This works well for proprietary resources shared across a company's own properties that shouldn't be discoverable by anyone else, like a proprietary proofreading AI model used in a commercial office suite.\n- Omitting `origins` entirely makes the file available only to **[same-site](https://web.dev/articles/same-site-same-origin#same-site-cross-site) origins**. This is a sensible default for resources shared across all of an organization's subdomains, but not intended to cross organizational boundaries.\n\nOne important rule: visibility can be upgraded but never downgraded. If a file is already globally available, a later attempt to store it with a restricted `origins` list is silently ignored. This prevents a malicious actor from re-storing a public resource and narrowing its availability. The reverse is possible: a file initially stored with a restricted `origins` list can later be made more permissive. Any site, not just the original storer, can call `getFileHandle()` for the same hash (hashes are not a secret) with `create: true` and a broader `origins` value, and given the browser verifies the hash matches, the resource becomes available to the wider audience from that point on. Note that the upgrading site **must** still write the full file through the returned handle. This requirement exists to prevent sites from exploiting the upgrade path as a side-channel to detect whether a particular file was already stored in COS.\n\n### Integrity by design\n\nA subtle but important property of COS is that the browser **verifies the hash** when you write a file. If the data you write doesn't match the declared hash, the write fails with an error. This makes integrity checking automatic: an app reading a file from COS can be confident it's getting exactly the bytes it expected. The same guarantee it would have had if it had computed the hash itself after a network download.\n\nThis turns out to be doubly useful in the Transformers.js scenario. Today, after downloading model weights, most apps have no practical way to verify that the CDN served the right bytes. With COS, every file in the store is implicitly verified on write, no matter where it came from, the official Hugging Face CDN or a random site's self-hosted mirror.\n\n### Privacy without sacrificing utility\n\nOf course a cross-origin shared cache raises the same question as the partitioned HTTP cache in reverse: if any site can probe for the presence of a file by hash, couldn't an attacker learn something about the user's browsing history by checking whether, say, a game engine Wasm module is cached?\n\nCOS addresses this through two complementary mechanisms:\n\n- First, the `origins` field: proprietary resources that shouldn't be globally probeable simply shouldn't be stored with `origins: '*'`, which, through **developer education**, developers are encouraged to consider whenever it makes sense.\n- Second, **availability gating**: even for globally declared files, the browser may suppress confirmation of a file's presence if it hasn't been encountered across a sufficient number of distinct origins. A file that only appears on one or two sites could still serve as a cross-site identifier, so the browser may return an error as if the file weren't there at all, regardless of what's physically on disk. On the Chrome team, we are conscious of the possible privacy leaks uncommon resources could cause and plan generally to mitigate it through restricting which exact resources can be cached. The concrete mitigations are still being fleshed out.\n\nCrucially, this means an error is not a definitive answer. It might mean \"not stored\", or it might mean \"stored, but the browser isn't telling you\". Apps should always handle it the same way: fall back to the network.\n\n### What this means for the Transformers.js example\n\nGoing back to the toy examples from before: the `ort-wasm-simd-threaded.asyncify.wasm` runtime weighs in at 4,733 kB and is shared by every Transformers.js-powered app regardless of which AI model it uses. With COS, the first app to load it downloads it once and stores it under its SHA-256 hash with `origins: '*'`. Every subsequent app, whether on `https://googlechrome.github.io`, on `https://rawcdn.rawgit.net`, or any other origin, finds it in COS immediately. The 177 MB of duplicate Whisper model weights? Same story: `Xenova/whisper-tiny.en` gets downloaded once, recognized by hash the second time around, and served from COS in milliseconds. And of course, the same also happens for `Xenova/distilbert-base-uncased-finetuned-sst-2-english`.\n\nTransformers.js itself is already piloting the COS API at the library level. [Pull request #1549](https://github.com/huggingface/transformers.js/pull/1549) introduced an experimental COS cache backend behind an opt-in flag. Enabling it takes a single line before you set up your pipeline:\n\n```js\nimport { env, pipeline } from \"https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0\";\n\n// 👇 Opt in to the experimental Cross-Origin Storage cache backend.\nenv.experimental_useCrossOriginStorage = true;\n\nconst asr = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny.en', { device: 'webgpu' });\nconst result = await asr('jfk.wav');\nconsole.log(result);\n```\n\nNote the `experimental_` prefix on the flag. It's intentional and signals that the underlying browser API has not yet been standardized and may change without a major version bump. With that flag set, Transformers.js resolves the SHA-256 hash for each [Xet-tracked](https://huggingface.co/docs/hub/en/xet/index) model file (the large ONNX weight files) by fetching the raw Xet pointer ([example raw pointer file](https://huggingface.co/Xenova/whisper-tiny.en/raw/main/onnx/decoder_model.onnx)) and extracting its `oid sha256:` field. It then uses that hash as the key for `navigator.crossOriginStorage`. If the model is already in COS (because another site stored it there first), it's served instantly without a network round-trip. If not, it falls back to a regular download and stores the result in COS for the next caller. With the toy example, the advantage in practice is that `Xenova/whisper-tiny.en` and `Xenova/distilbert-base-uncased-finetuned-sst-2-english` (and of course `ort-wasm-simd-threaded.asyncify.wasm`) only ever need to cross the ether once, regardless of how many different origins ask for them.\n\n### Model flexibility\n\nThe toy example works just fine with `Xenova/whisper-tiny.en`, but of course you possibly wouldn't say no if the user already has [any of the other Whisper variants](https://huggingface.co/Xenova/models?search=whisper) in their COS cache. For example, the user might already have [`Xenova/whisper-large-v3`](https://huggingface.co/Xenova/whisper-large-v3), which, as the name suggests, is a lot larger than the tiny variant. Transformers.js's [Model Registry](https://huggingface.co/docs/transformers.js/en/api/utils/model_registry) makes being flexible about your models possible. If you know your app's needs can be addressed by, for example, any of `Xenova/whisper-tiny.en`, `whisper-medium.en`, or `Xenova/whisper-large-v3`, you can check the registry for the associated files for each model, probe for their existence in the COS cache (which may partially or completely contain the model resources you need), and then take a decision what model to choose eventually. The [`ModelRegistry.is_pipeline_cached()`](https://huggingface.co/docs/transformers.js/en/api/utils/model_registry#modelregistryispipelinecachedtask-modelid-options--promise--boolean-) API directly integrates with COS (and of course the Cache API), so this operation is really ergonomic.\n\n### Try it today\n\nThe COS API is not yet natively implemented in any browser, but you don't have to wait to experiment with it. Install the [Cross-Origin Storage extension](https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih) to inject the `navigator.crossOriginStorage` polyfill on all pages and test the complete flow. Check out the [source code of the extension](https://github.com/web-ai-community/cross-origin-storage-extension) and follow the [usage instructions](https://github.com/web-ai-community/cross-origin-storage-extension?tab=readme-ov-file#usage) to get started.\n\n[![Chrome Web Store page for the Cross-Origin Storage extension.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/0q3rowmy67ta.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/0q3rowmy67ta.png)\n\nWith the extension installed, try the full end-to-end experience right now: open the first [toy example with COS enabled](https://googlechrome.github.io/samples/transformersjs-automatic-speech-recognition/index3.html), let it load `Xenova/whisper-tiny.en`, then open the [toy example with COS enabled from the second origin](https://rawcdn.rawgit.net/GoogleChrome/samples/1e4f2b8c10adc394352c6ec8327bb503bac7aba1/transformersjs-automatic-speech-recognition/index3.html). Instead of the 177 MB re-download you saw before, the model is served from COS in milliseconds. When you open the extension's popup window, you can see COS in action. If you **View by Resource**, you can see the resource with the SHA-256 hash `950978b1dbcbf250335358c1236053ba19a7f7849b33dc777f4421b72b7626fa` shared across `https://googlechrome.github.io` and `https://rawcdn.rawgit.net`. It may not be obvious, but as you can verify by comparing the SHA-256 hash on Hugging Face, you're looking at [`https://huggingface.co/Xenova/whisper-tiny.en/blob/main/onnx/decoder_model_merged.onnx`](https://huggingface.co/Xenova/whisper-tiny.en/blob/main/onnx/decoder_model_merged.onnx). For now, the extension is mostly aimed at power users like you. Once implemented in the browser, there will be a friendlier integration in the browser's **Settings** page. The screenshot below shows the extension's popup window with the **View by Resource** tab active, where you can see the shared resource with its hash and the two origins that have it in their COS cache.\n\n[![A resource seen in the Cross-Origin Storage extension, showing it's shared between two origins.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/usg5dq7dhm.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/usg5dq7dhm.png)\n\n## Call to action\n\nIf you're building your own Transformers.js app, the call to action is simple: add `env.experimental_useCrossOriginStorage = true` before your first `pipeline()` call, install the extension, and watch the duplicate downloads disappear from your Network tab. Every site that opts in makes the experience faster and cheaper for every other site's users. Opting in is completely risk-free: if the COS API isn't supported because the user doesn't have the COS extension installed, the code just falls back to the default path (the [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) API).\n\nTransformers.js is not alone in experimenting with COS. [WebLLM](https://webllm.mlc.ai/) (opt-in, see [documentation](https://webllm.mlc.ai/docs/user/advanced_usage.html#using-cross-origin-storage-cache)) and [wllama](https://github.com/ngxson/wllama) (automatic, see [PR](https://github.com/ngxson/wllama/pull/248)) are likewise excited about this proposed API.\n\nOn the Chrome team, we're [considering implementing the COS API](https://chromestatus.com/feature/5163371507875840) natively in the browser. As an early stage proposal, we welcome feedback on the API, and the shape of the proposal itself. The [Cross-Origin Storage repository](https://github.com/WICG/cross-origin-storage) is the place to file issues, [express support](https://github.com/WICG/cross-origin-storage/labels/expression%20of%20support), or open PRs.\n",
  "authors": [],
  "captured_at": "2026-10-19T01:26:53.998049939+00:00",
  "category": "ai",
  "content_html": "<p>(This is a guest post by Developer Relations Engineer <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://blog.tomayac.com/\">Thomas Steiner</a> from the Chrome team at Google.)</p>\n<p>Transformers.js provides Web developers with a simple way to use the power of transformers in their Web apps through task-specific pipelines. To run inference in the browser, developers create an instance of <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/docs/transformers.js/en/api/pipelines\"><code>pipeline()</code></a> and specify a task they want to use the pipeline for. As a concrete example, the following snippet shows how to set up an automatic speech recognition (ASR) pipeline.</p>\n<pre><code class=\"language-js\"><span class=\"code-snippet\" data-language=\"JavaScript\"><span class=\"syntax-source syntax-js\"><span class=\"syntax-meta syntax-import syntax-js\"><span class=\"syntax-keyword syntax-control syntax-import-export syntax-js\">import</span> <span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">pipeline</span> <span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span> <span class=\"syntax-keyword syntax-control syntax-import-export syntax-js\">from</span> <span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n\n<span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">asr</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">await</span> <span class=\"syntax-meta syntax-function-call syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">pipeline</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span>\n  <span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>automatic-speech-recognition<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span>\n  <span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>Xenova/whisper-tiny.en<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span>\n  <span class=\"syntax-meta syntax-object-literal syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span> <span class=\"syntax-meta syntax-object-literal syntax-key syntax-js\">device</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-js\">:</span> <span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>webgpu<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span> <span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span>\n</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n<span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">result</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">await</span> <span class=\"syntax-meta syntax-function-call syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">asr</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>jfk.wav<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n<span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-support syntax-type syntax-object syntax-console syntax-js\">console</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-support syntax-function syntax-console syntax-js\">log</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">result</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n</span></span></code></pre>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/87a91qnbicf.png\"><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/87a91qnbicf.png\" alt=\"A minimalistic example of the automatic speech recognition pipeline.\" loading=\"eager\" decoding=\"async\" fetchpriority=\"high\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></a></p>\n<h2>The cache challenge</h2>\n<p>You will notice in the source code that I specified <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/Xenova/whisper-tiny.en\"><code>Xenova/whisper-tiny.en</code></a> as the model, which is a very decent choice for common English automatic speech recognition tasks. In fact, it's even <em>the</em> default model according to the Transformers.js <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/transformers.js/blob/main/packages/transformers/src/pipelines/index.js\">default model resolution</a>, as per the linked <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/transformers.js/blob/bc9cf7400f4f2c8695016699f879e31026ff0313/packages/transformers/src/pipelines/index.js#L151-L158\">excerpt</a>.</p>\n<h3>Model resources</h3>\n<p>When you <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://googlechrome.github.io/samples/transformersjs-automatic-speech-recognition/index.html\">run this example in the browser</a>, Transformers.js automatically takes care of downloading and caching the relevant model resources and Wasm files. The following screenshot shows the Chrome DevTools <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.chrome.com/docs/devtools/storage/cache\">Cache storage</a> section after visiting the app. When you reload the page, the resources are served from the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.mozilla.org/en-US/docs/Web/API/Cache\">Cache API</a>, and the model returns results almost instantly.</p>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/otd8tt1gusb.png\"><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/otd8tt1gusb.png\" alt=\"The Chrome DevTools Cache storage section showing Whisper AI model resources and Wasm runtime files after visiting the app.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></a></p>\n<p>However, <code>Xenova/whisper-tiny.en</code> being a popular model (and, as mentioned before, even being <em>the</em> ASR default model in Transformers.js), you can well imagine that more than just one app that you visit would use it. To simulate this situation, here's the same example app from before, but served from a <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://rawcdn.rawgit.net/GoogleChrome/samples/c4192bd7a3c66fc288a7b22b77acb935df00b8a1/transformersjs-automatic-speech-recognition/index.html\">different origin</a>. When you visit this different origin app, rather than being usable almost instantly, the browser instead has to download and cache all the model resources again, even if they're byte-by-byte the same as before. Even in this toy example, this adds up to 177 MB of duplicate download and storage, as you can examine in the <strong>Storage</strong> section of the Chrome DevTools <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.chrome.com/docs/devtools/application#open_the_application_panel\">Application panel</a>. You can imagine that this quickly adds up.</p>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/9byoniem0pw.png\"><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/9byoniem0pw.png\" alt=\"The Chrome DevTools Storage overview showing 177 MB of used storage.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></a></p>\n<h3>Wasm runtime resources</h3>\n<p>But it gets worse. Let's add a second pipeline to the toy example: sentiment analysis. Sentiment analysis <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/transformers.js/blob/bc9cf7400f4f2c8695016699f879e31026ff0313/packages/transformers/src/pipelines/index.js#L65\">by default</a> uses the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english\"><code>Xenova/distilbert-base-uncased-finetuned-sst-2-english</code></a> model. By not specifying the model, Transformers.js' default model resolution automatically picks it for you.</p>\n<pre><code class=\"language-js\"><span class=\"code-snippet\" data-language=\"JavaScript\"><span class=\"syntax-source syntax-js\"><span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">classifier</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">await</span> <span class=\"syntax-meta syntax-function-call syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">pipeline</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>sentiment-analysis<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n<span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">sentiment</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">await</span> <span class=\"syntax-meta syntax-function-call syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">classifier</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-variable syntax-other syntax-object syntax-js\">result</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-property syntax-object syntax-js\">text</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n<span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-support syntax-type syntax-object syntax-console syntax-js\">console</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-support syntax-function syntax-console syntax-js\">log</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">sentiment</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n</span></span></code></pre>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/le7l1km7o4g.png\"><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/le7l1km7o4g.png\" alt=\"image\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></a></p>\n<p>Two entirely different AI models, but they depend on the same 4,733 kB <code>ort-wasm-simd-threaded.asyncify.wasm</code> WebAssembly (Wasm) runtime file <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://onnxruntime.ai/docs/api/js/interfaces/Env.WasmFilePaths.html#wasm\">from the underlying ONNX Runtime library</a> that Transformers.js is built on top of. Open the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://rawcdn.rawgit.net/GoogleChrome/samples/d47114a15637383015c274e7bdcd81e1a17b0ccf/transformersjs-automatic-speech-recognition/index2.html\">extended demo on a different origin</a>, and you will notice in the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.chrome.com/docs/devtools/network#load\"><strong>Network</strong> tab</a> how also the Wasm runtime gets downloaded and cached again.</p>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/pz12g20fqeg.png\"><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/pz12g20fqeg.png\" alt=\"Chrome DevTools Network panel showing the download of the Wasm runtime resource.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></a></p>\n<p>So even if you run apps that don't share the same AI models, your browser still makes redundant requests for shared Wasm resources you already have, and on top of that also caches them again, which consumes space on your hard disk.</p>\n<h3>Cache isolation</h3>\n<h4>AI model resources serving</h4>\n<p>By default, <strong>AI model resources</strong> come from the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/docs/hub/en/models-the-hub\">Hugging Face Hub</a>, and ultimately the Hugging Face CDN. The browser makes a request for a resource like <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english/resolve/main/config.json\"><code>https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english/resolve/main/config.json</code></a> which then gets redirected to the final CDN URL like <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"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=&amp;etag=%223c36342ef1f74de2797d667c68c6b7b988d0b87c%22\"><code>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=&amp;etag=%223c36342ef1f74de2797d667c68c6b7b988d0b87c%22</code></a> in this case.</p>\n<h4>Wasm runtime resources serving</h4>\n<p>The <strong>Wasm runtime resources</strong> are served from the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.jsdelivr.com/\">jsDelivr CDN</a> by default. For example, <code>ort-wasm-simd-threaded.asyncify.wasm</code> comes from <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm\"><code>https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm</code></a> at the time of this writing.</p>\n<p>Now you may say that if different apps, even though running on different origins, in the end serve their resources from the same CDN URLs, caching shouldn't be a problem, as long as the final URLs are the same. Unfortunately, this is not how caching works in browsers for a long time. The article <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.chrome.com/blog/http-cache-partitioning\">Gaining security and privacy by partitioning the cache</a> goes into all the details, but essentially, <strong>caches are isolated by origin</strong> to prevent timing attacks: the time a website takes to respond to HTTP requests can reveal that the browser has accessed the same resource in the past, which makes the browser vulnerable to security and privacy leaks.</p>\n<h4>Chrome's implementation</h4>\n<p>The concrete implementation may vary by browser, but in Chrome, cached resources are keyed using a Network Isolation Key in addition to the <strong>resource URL</strong>. The Network Isolation Key is composed of the <strong>top-level site</strong> and the <strong>current-frame site</strong>. Take the previous toy examples hosted on the origins <code>https://googlechrome.github.io</code> and <code>https://rawcdn.rawgit.net</code>. If they both use the Wasm runtime from <code>https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm</code>, their cache keys will look like in the following table.</p>\n<table>\n<thead>\n<tr>\n<th>Network Isolation Key</th>\n<th><strong>Resource URL</strong></th>\n<th></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Top-level site</strong></td>\n<td><strong>Current-frame site</strong></td>\n<td></td>\n</tr>\n<tr>\n<td><code>https://googlechrome.github.io</code></td>\n<td><code>https://googlechrome.github.io</code></td>\n<td><code>https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm</code></td>\n</tr>\n<tr>\n<td><code>https://rawcdn.rawgit.net</code></td>\n<td><code>https://rawcdn.rawgit.net</code></td>\n<td><code>https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm</code></td>\n</tr>\n</tbody>\n</table>\n<p>So even if the resource URLs are exactly the same, since the Network Isolation Keys don't match, there's no cache hit, which means duplicate download and duplicate storage. This is the challenge that the Cross-Origin Storage proposal aims to tackle.</p>\n<h2>Enter the Cross-Origin Storage API</h2>\n<blockquote>\n<p><strong>💡 Note:</strong> The Cross-Origin Storage API is an early-stage proposal that isn't final. While the proposed API is not yet natively implemented in any browser, you don't have to wait to experiment with it. Install the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih\">Cross-Origin Storage extension</a> to inject the <code>navigator.crossOriginStorage</code> polyfill on all pages and test the complete flow.</p>\n</blockquote>\n<p>The proposed <strong><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/WICG/cross-origin-storage\">Cross-Origin Storage</a> (COS) API</strong> introduces a dedicated <code>navigator.crossOriginStorage</code> interface through which web apps can store and retrieve large files across origin boundaries, identified not by a URL, but by a cryptographic hash.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/klwb5fryaa.png\" alt=\"The Cross-Origin Storage API logo: a stylized walking person, as typically encountered on crosswalk signs.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>That last point about cryptographic hashes is key. Because COS identifies files by their <strong>hash</strong> rather than by their URL or origin, the same <code>ort-wasm-simd-threaded.asyncify.wasm</code> Wasm runtime you downloaded while visiting <code>https://googlechrome.github.io</code> is recognized as identical to the one <code>https://rawcdn.rawgit.net</code> is about to request, no matter where either of the two origins fetched it from. See the following code snippet that illustrates the basic flow.</p>\n<pre><code class=\"language-js\"><span class=\"code-snippet\" data-language=\"JavaScript\"><span class=\"syntax-source syntax-js\"><span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">hash</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-meta syntax-object-literal syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span>\n  <span class=\"syntax-meta syntax-object-literal syntax-key syntax-js\">algorithm</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-js\">:</span> <span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>SHA-256<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span>\n  <span class=\"syntax-meta syntax-object-literal syntax-key syntax-js\">value</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-js\">:</span> <span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span>\n<span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n\n<span class=\"syntax-meta syntax-try syntax-js\"><span class=\"syntax-keyword syntax-control syntax-trycatch syntax-js\">try</span> <span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span>\n  <span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">handle</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">await</span> <span class=\"syntax-support syntax-type syntax-object syntax-dom syntax-js\">navigator</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-property syntax-object syntax-js\">crossOriginStorage</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">getFileHandle</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">hash</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n  <span class=\"syntax-comment syntax-line syntax-double-slash syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-js\">//</span> Cache hit! Get the file as a Blob and use it directly.\n</span>  <span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">fileBlob</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">await</span> <span class=\"syntax-variable syntax-other syntax-object syntax-js\">handle</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">getFile</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n</span><span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span></span> <span class=\"syntax-meta syntax-catch syntax-js\"><span class=\"syntax-keyword syntax-control syntax-trycatch syntax-js\">catch</span> <span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span>\n  <span class=\"syntax-comment syntax-line syntax-double-slash syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-js\">//</span> Cache miss. Download from network, then store for next time.\n</span>  <span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">fileBlob</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">await</span> <span class=\"syntax-meta syntax-function-call syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">fetch</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>https://cdn.jsdelivr.net/.../ort-wasm-simd-threaded.asyncify.wasm<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span>\n    <span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">then</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-meta syntax-function syntax-declaration syntax-js\"><span class=\"syntax-variable syntax-parameter syntax-function syntax-js\">r</span> <span class=\"syntax-storage syntax-type syntax-function syntax-arrow syntax-js\">=&gt;</span></span> <span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-variable syntax-other syntax-object syntax-js\">r</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">blob</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n  <span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">handle</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">await</span> <span class=\"syntax-support syntax-type syntax-object syntax-dom syntax-js\">navigator</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-property syntax-object syntax-js\">crossOriginStorage</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">getFileHandle</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span>\n    <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">hash</span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span>\n    <span class=\"syntax-meta syntax-object-literal syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span> <span class=\"syntax-meta syntax-object-literal syntax-key syntax-js\">create</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-js\">:</span> <span class=\"syntax-constant syntax-language syntax-boolean syntax-true syntax-js\">true</span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span> <span class=\"syntax-meta syntax-object-literal syntax-key syntax-js\">origins</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-js\">:</span> <span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>*<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span> <span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span>\n  </span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n  <span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">writableStream</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">await</span> <span class=\"syntax-variable syntax-other syntax-object syntax-js\">handle</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">createWritable</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n  <span class=\"syntax-keyword syntax-control syntax-flow syntax-js\">await</span> <span class=\"syntax-variable syntax-other syntax-object syntax-js\">writableStream</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-support syntax-function syntax-dom syntax-js\">write</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">fileBlob</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n  <span class=\"syntax-keyword syntax-control syntax-flow syntax-js\">await</span> <span class=\"syntax-variable syntax-other syntax-object syntax-js\">writableStream</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-support syntax-function syntax-dom syntax-js\">close</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>  \n</span><span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span></span>\n</span></span></code></pre>\n<p>If the resource is in COS, you get back a <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle\"><code>FileSystemFileHandle</code></a> from which you can read the blob directly via <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/getFile\"><code>getFile()</code></a> (the resulting <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.mozilla.org/en-US/docs/Web/API/File\"><code>File</code></a> inherits from <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.mozilla.org/en-US/docs/Web/API/Blob\"><code>Blob</code></a>). If the resource is not in COS, you fall back to the network, and write the resource into COS for the next app that needs it, which could be your app, or another unrelated app, potentially on a completely different origin.</p>\n<p>The API is deliberately shaped after the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://fs.spec.whatwg.org/\">File System Standard</a>'s <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/getFileHandle\"><code>FileSystemDirectoryHandle.getFileHandle()</code></a> you likely are familiar with from the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system\">Origin Private File System</a> (OPFS) API. The <code>hash</code> parameter plays the same role as the <code>name</code> parameter in OPFS: uniquely identifying a resource. The <code>options.create</code> flag works the same way: absent or <code>false</code> for read-only access, <code>true</code> when you intend to write.</p>\n<h3>Control who can read what</h3>\n<p>Not every resource should be globally shared. COS gives developers precise control over visibility through the <code>origins</code> option when storing a file.</p>\n<ul>\n<li>Setting <code>origins: '*'</code> makes a file <strong>globally available</strong>. Any origin can find it by hash. This is the right choice for AI model resources or the Wasm runtime in the Transformers.js example: the whole point is that every app on the Web benefits from a single cached copy.</li>\n<li>Passing a specific list of origins, like <code>origins: ['https://write.example.com', 'https://calculate.example.com']</code>, <strong>restricts</strong> access to those sites. This works well for proprietary resources shared across a company's own properties that shouldn't be discoverable by anyone else, like a proprietary proofreading AI model used in a commercial office suite.</li>\n<li>Omitting <code>origins</code> entirely makes the file available only to <strong><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://web.dev/articles/same-site-same-origin#same-site-cross-site\">same-site</a> origins</strong>. This is a sensible default for resources shared across all of an organization's subdomains, but not intended to cross organizational boundaries.</li>\n</ul>\n<p>One important rule: visibility can be upgraded but never downgraded. If a file is already globally available, a later attempt to store it with a restricted <code>origins</code> list is silently ignored. This prevents a malicious actor from re-storing a public resource and narrowing its availability. The reverse is possible: a file initially stored with a restricted <code>origins</code> list can later be made more permissive. Any site, not just the original storer, can call <code>getFileHandle()</code> for the same hash (hashes are not a secret) with <code>create: true</code> and a broader <code>origins</code> value, and given the browser verifies the hash matches, the resource becomes available to the wider audience from that point on. Note that the upgrading site <strong>must</strong> still write the full file through the returned handle. This requirement exists to prevent sites from exploiting the upgrade path as a side-channel to detect whether a particular file was already stored in COS.</p>\n<h3>Integrity by design</h3>\n<p>A subtle but important property of COS is that the browser <strong>verifies the hash</strong> when you write a file. If the data you write doesn't match the declared hash, the write fails with an error. This makes integrity checking automatic: an app reading a file from COS can be confident it's getting exactly the bytes it expected. The same guarantee it would have had if it had computed the hash itself after a network download.</p>\n<p>This turns out to be doubly useful in the Transformers.js scenario. Today, after downloading model weights, most apps have no practical way to verify that the CDN served the right bytes. With COS, every file in the store is implicitly verified on write, no matter where it came from, the official Hugging Face CDN or a random site's self-hosted mirror.</p>\n<h3>Privacy without sacrificing utility</h3>\n<p>Of course a cross-origin shared cache raises the same question as the partitioned HTTP cache in reverse: if any site can probe for the presence of a file by hash, couldn't an attacker learn something about the user's browsing history by checking whether, say, a game engine Wasm module is cached?</p>\n<p>COS addresses this through two complementary mechanisms:</p>\n<ul>\n<li>First, the <code>origins</code> field: proprietary resources that shouldn't be globally probeable simply shouldn't be stored with <code>origins: '*'</code>, which, through <strong>developer education</strong>, developers are encouraged to consider whenever it makes sense.</li>\n<li>Second, <strong>availability gating</strong>: even for globally declared files, the browser may suppress confirmation of a file's presence if it hasn't been encountered across a sufficient number of distinct origins. A file that only appears on one or two sites could still serve as a cross-site identifier, so the browser may return an error as if the file weren't there at all, regardless of what's physically on disk. On the Chrome team, we are conscious of the possible privacy leaks uncommon resources could cause and plan generally to mitigate it through restricting which exact resources can be cached. The concrete mitigations are still being fleshed out.</li>\n</ul>\n<p>Crucially, this means an error is not a definitive answer. It might mean &quot;not stored&quot;, or it might mean &quot;stored, but the browser isn't telling you&quot;. Apps should always handle it the same way: fall back to the network.</p>\n<h3>What this means for the Transformers.js example</h3>\n<p>Going back to the toy examples from before: the <code>ort-wasm-simd-threaded.asyncify.wasm</code> runtime weighs in at 4,733 kB and is shared by every Transformers.js-powered app regardless of which AI model it uses. With COS, the first app to load it downloads it once and stores it under its SHA-256 hash with <code>origins: '*'</code>. Every subsequent app, whether on <code>https://googlechrome.github.io</code>, on <code>https://rawcdn.rawgit.net</code>, or any other origin, finds it in COS immediately. The 177 MB of duplicate Whisper model weights? Same story: <code>Xenova/whisper-tiny.en</code> gets downloaded once, recognized by hash the second time around, and served from COS in milliseconds. And of course, the same also happens for <code>Xenova/distilbert-base-uncased-finetuned-sst-2-english</code>.</p>\n<p>Transformers.js itself is already piloting the COS API at the library level. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/transformers.js/pull/1549\">Pull request #1549</a> introduced an experimental COS cache backend behind an opt-in flag. Enabling it takes a single line before you set up your pipeline:</p>\n<pre><code class=\"language-js\"><span class=\"code-snippet\" data-language=\"JavaScript\"><span class=\"syntax-source syntax-js\"><span class=\"syntax-meta syntax-import syntax-js\"><span class=\"syntax-keyword syntax-control syntax-import-export syntax-js\">import</span> <span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">env</span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">pipeline</span> <span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span> <span class=\"syntax-keyword syntax-control syntax-import-export syntax-js\">from</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&quot;</span>https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n\n<span class=\"syntax-comment syntax-line syntax-double-slash syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-js\">//</span> 👇 Opt in to the experimental Cross-Origin Storage cache backend.\n</span><span class=\"syntax-variable syntax-other syntax-object syntax-js\">env</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-property syntax-object syntax-js\">experimental_useCrossOriginStorage</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-constant syntax-language syntax-boolean syntax-true syntax-js\">true</span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n\n<span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">asr</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">await</span> <span class=\"syntax-meta syntax-function-call syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">pipeline</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>automatic-speech-recognition<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span> <span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>Xenova/whisper-tiny.en<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span> <span class=\"syntax-meta syntax-object-literal syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span> <span class=\"syntax-meta syntax-object-literal syntax-key syntax-js\">device</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-js\">:</span> <span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>webgpu<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span> <span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n<span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">result</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">await</span> <span class=\"syntax-meta syntax-function-call syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">asr</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-string syntax-quoted syntax-single syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&#39;</span>jfk.wav<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&#39;</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n<span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-support syntax-type syntax-object syntax-console syntax-js\">console</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-support syntax-function syntax-console syntax-js\">log</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">result</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n</span></span></code></pre>\n<p>Note the <code>experimental_</code> prefix on the flag. It's intentional and signals that the underlying browser API has not yet been standardized and may change without a major version bump. With that flag set, Transformers.js resolves the SHA-256 hash for each <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/docs/hub/en/xet/index\">Xet-tracked</a> model file (the large ONNX weight files) by fetching the raw Xet pointer (<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/Xenova/whisper-tiny.en/raw/main/onnx/decoder_model.onnx\">example raw pointer file</a>) and extracting its <code>oid sha256:</code> field. It then uses that hash as the key for <code>navigator.crossOriginStorage</code>. If the model is already in COS (because another site stored it there first), it's served instantly without a network round-trip. If not, it falls back to a regular download and stores the result in COS for the next caller. With the toy example, the advantage in practice is that <code>Xenova/whisper-tiny.en</code> and <code>Xenova/distilbert-base-uncased-finetuned-sst-2-english</code> (and of course <code>ort-wasm-simd-threaded.asyncify.wasm</code>) only ever need to cross the ether once, regardless of how many different origins ask for them.</p>\n<h3>Model flexibility</h3>\n<p>The toy example works just fine with <code>Xenova/whisper-tiny.en</code>, but of course you possibly wouldn't say no if the user already has <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/Xenova/models?search=whisper\">any of the other Whisper variants</a> in their COS cache. For example, the user might already have <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/Xenova/whisper-large-v3\"><code>Xenova/whisper-large-v3</code></a>, which, as the name suggests, is a lot larger than the tiny variant. Transformers.js's <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/docs/transformers.js/en/api/utils/model_registry\">Model Registry</a> makes being flexible about your models possible. If you know your app's needs can be addressed by, for example, any of <code>Xenova/whisper-tiny.en</code>, <code>whisper-medium.en</code>, or <code>Xenova/whisper-large-v3</code>, you can check the registry for the associated files for each model, probe for their existence in the COS cache (which may partially or completely contain the model resources you need), and then take a decision what model to choose eventually. The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/docs/transformers.js/en/api/utils/model_registry#modelregistryispipelinecachedtask-modelid-options--promise--boolean-\"><code>ModelRegistry.is_pipeline_cached()</code></a> API directly integrates with COS (and of course the Cache API), so this operation is really ergonomic.</p>\n<h3>Try it today</h3>\n<p>The COS API is not yet natively implemented in any browser, but you don't have to wait to experiment with it. Install the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih\">Cross-Origin Storage extension</a> to inject the <code>navigator.crossOriginStorage</code> polyfill on all pages and test the complete flow. Check out the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/web-ai-community/cross-origin-storage-extension\">source code of the extension</a> and follow the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/web-ai-community/cross-origin-storage-extension?tab=readme-ov-file#usage\">usage instructions</a> to get started.</p>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/0q3rowmy67ta.png\"><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/0q3rowmy67ta.png\" alt=\"Chrome Web Store page for the Cross-Origin Storage extension.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></a></p>\n<p>With the extension installed, try the full end-to-end experience right now: open the first <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://googlechrome.github.io/samples/transformersjs-automatic-speech-recognition/index3.html\">toy example with COS enabled</a>, let it load <code>Xenova/whisper-tiny.en</code>, then open the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://rawcdn.rawgit.net/GoogleChrome/samples/1e4f2b8c10adc394352c6ec8327bb503bac7aba1/transformersjs-automatic-speech-recognition/index3.html\">toy example with COS enabled from the second origin</a>. Instead of the 177 MB re-download you saw before, the model is served from COS in milliseconds. When you open the extension's popup window, you can see COS in action. If you <strong>View by Resource</strong>, you can see the resource with the SHA-256 hash <code>950978b1dbcbf250335358c1236053ba19a7f7849b33dc777f4421b72b7626fa</code> shared across <code>https://googlechrome.github.io</code> and <code>https://rawcdn.rawgit.net</code>. It may not be obvious, but as you can verify by comparing the SHA-256 hash on Hugging Face, you're looking at <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/Xenova/whisper-tiny.en/blob/main/onnx/decoder_model_merged.onnx\"><code>https://huggingface.co/Xenova/whisper-tiny.en/blob/main/onnx/decoder_model_merged.onnx</code></a>. For now, the extension is mostly aimed at power users like you. Once implemented in the browser, there will be a friendlier integration in the browser's <strong>Settings</strong> page. The screenshot below shows the extension's popup window with the <strong>View by Resource</strong> tab active, where you can see the shared resource with its hash and the two origins that have it in their COS cache.</p>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/usg5dq7dhm.png\"><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/usg5dq7dhm.png\" alt=\"A resource seen in the Cross-Origin Storage extension, showing it&#39;s shared between two origins.\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></a></p>\n<h2>Call to action</h2>\n<p>If you're building your own Transformers.js app, the call to action is simple: add <code>env.experimental_useCrossOriginStorage = true</code> before your first <code>pipeline()</code> call, install the extension, and watch the duplicate downloads disappear from your Network tab. Every site that opts in makes the experience faster and cheaper for every other site's users. Opting in is completely risk-free: if the COS API isn't supported because the user doesn't have the COS extension installed, the code just falls back to the default path (the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.mozilla.org/en-US/docs/Web/API/Cache\">Cache</a> API).</p>\n<p>Transformers.js is not alone in experimenting with COS. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://webllm.mlc.ai/\">WebLLM</a> (opt-in, see <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://webllm.mlc.ai/docs/user/advanced_usage.html#using-cross-origin-storage-cache\">documentation</a>) and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/ngxson/wllama\">wllama</a> (automatic, see <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/ngxson/wllama/pull/248\">PR</a>) are likewise excited about this proposed API.</p>\n<p>On the Chrome team, we're <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://chromestatus.com/feature/5163371507875840\">considering implementing the COS API</a> natively in the browser. As an early stage proposal, we welcome feedback on the API, and the shape of the proposal itself. The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/WICG/cross-origin-storage\">Cross-Origin Storage repository</a> is the place to file issues, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/WICG/cross-origin-storage/labels/expression%20of%20support\">express support</a>, or open PRs.</p>\n",
  "content_markdown": "(This is a guest post by Developer Relations Engineer [Thomas Steiner](https://blog.tomayac.com/) from the Chrome team at Google.)\n\nTransformers.js provides Web developers with a simple way to use the power of transformers in their Web apps through task-specific pipelines. To run inference in the browser, developers create an instance of [`pipeline()`](https://huggingface.co/docs/transformers.js/en/api/pipelines) and specify a task they want to use the pipeline for. As a concrete example, the following snippet shows how to set up an automatic speech recognition (ASR) pipeline.\n\n```js\nimport { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0';\n\nconst asr = await pipeline(\n  'automatic-speech-recognition',\n  'Xenova/whisper-tiny.en',\n  { device: 'webgpu' },\n);\nconst result = await asr('jfk.wav');\nconsole.log(result);\n```\n\n[![A minimalistic example of the automatic speech recognition pipeline.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/87a91qnbicf.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/87a91qnbicf.png)\n\n## The cache challenge\n\nYou will notice in the source code that I specified [`Xenova/whisper-tiny.en`](https://huggingface.co/Xenova/whisper-tiny.en) as the model, which is a very decent choice for common English automatic speech recognition tasks. In fact, it's even *the* default model according to the Transformers.js [default model resolution](https://github.com/huggingface/transformers.js/blob/main/packages/transformers/src/pipelines/index.js), as per the linked [excerpt](https://github.com/huggingface/transformers.js/blob/bc9cf7400f4f2c8695016699f879e31026ff0313/packages/transformers/src/pipelines/index.js#L151-L158).\n\n### Model resources\n\nWhen you [run this example in the browser](https://googlechrome.github.io/samples/transformersjs-automatic-speech-recognition/index.html), Transformers.js automatically takes care of downloading and caching the relevant model resources and Wasm files. The following screenshot shows the Chrome DevTools [Cache storage](https://developer.chrome.com/docs/devtools/storage/cache) section after visiting the app. When you reload the page, the resources are served from the [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache), and the model returns results almost instantly.\n\n[![The Chrome DevTools Cache storage section showing Whisper AI model resources and Wasm runtime files after visiting the app.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/otd8tt1gusb.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/otd8tt1gusb.png)\n\nHowever, `Xenova/whisper-tiny.en` being a popular model (and, as mentioned before, even being *the* ASR default model in Transformers.js), you can well imagine that more than just one app that you visit would use it. To simulate this situation, here's the same example app from before, but served from a [different origin](https://rawcdn.rawgit.net/GoogleChrome/samples/c4192bd7a3c66fc288a7b22b77acb935df00b8a1/transformersjs-automatic-speech-recognition/index.html). When you visit this different origin app, rather than being usable almost instantly, the browser instead has to download and cache all the model resources again, even if they're byte-by-byte the same as before. Even in this toy example, this adds up to 177 MB of duplicate download and storage, as you can examine in the **Storage** section of the Chrome DevTools [Application panel](https://developer.chrome.com/docs/devtools/application#open_the_application_panel). You can imagine that this quickly adds up.\n\n[![The Chrome DevTools Storage overview showing 177 MB of used storage.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/9byoniem0pw.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/9byoniem0pw.png)\n\n### Wasm runtime resources\n\nBut it gets worse. Let's add a second pipeline to the toy example: sentiment analysis. Sentiment analysis [by default](https://github.com/huggingface/transformers.js/blob/bc9cf7400f4f2c8695016699f879e31026ff0313/packages/transformers/src/pipelines/index.js#L65) uses the [`Xenova/distilbert-base-uncased-finetuned-sst-2-english`](https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english) model. By not specifying the model, Transformers.js' default model resolution automatically picks it for you.\n\n```js\nconst classifier = await pipeline('sentiment-analysis');\nconst sentiment = await classifier(result.text);\nconsole.log(sentiment);\n```\n\n[![image](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/le7l1km7o4g.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/le7l1km7o4g.png)\n\nTwo entirely different AI models, but they depend on the same 4,733 kB `ort-wasm-simd-threaded.asyncify.wasm` WebAssembly (Wasm) runtime file [from the underlying ONNX Runtime library](https://onnxruntime.ai/docs/api/js/interfaces/Env.WasmFilePaths.html#wasm) that Transformers.js is built on top of. Open the [extended demo on a different origin](https://rawcdn.rawgit.net/GoogleChrome/samples/d47114a15637383015c274e7bdcd81e1a17b0ccf/transformersjs-automatic-speech-recognition/index2.html), and you will notice in the [**Network** tab](https://developer.chrome.com/docs/devtools/network#load) how also the Wasm runtime gets downloaded and cached again.\n\n[![Chrome DevTools Network panel showing the download of the Wasm runtime resource.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/pz12g20fqeg.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/pz12g20fqeg.png)\n\nSo even if you run apps that don't share the same AI models, your browser still makes redundant requests for shared Wasm resources you already have, and on top of that also caches them again, which consumes space on your hard disk.\n\n### Cache isolation\n\n#### AI model resources serving\n\nBy default, **AI model resources** come from the [Hugging Face Hub](https://huggingface.co/docs/hub/en/models-the-hub), and ultimately the Hugging Face CDN. The browser makes a request for a resource like [`https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english/resolve/main/config.json`](https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english/resolve/main/config.json) which then gets redirected to the final CDN URL like [`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`](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) in this case.\n\n#### Wasm runtime resources serving\n\nThe **Wasm runtime resources** are served from the [jsDelivr CDN](https://www.jsdelivr.com/) by default. For example, `ort-wasm-simd-threaded.asyncify.wasm` comes from [`https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/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) at the time of this writing.\n\nNow you may say that if different apps, even though running on different origins, in the end serve their resources from the same CDN URLs, caching shouldn't be a problem, as long as the final URLs are the same. Unfortunately, this is not how caching works in browsers for a long time. The article [Gaining security and privacy by partitioning the cache](https://developer.chrome.com/blog/http-cache-partitioning) goes into all the details, but essentially, **caches are isolated by origin** to prevent timing attacks: the time a website takes to respond to HTTP requests can reveal that the browser has accessed the same resource in the past, which makes the browser vulnerable to security and privacy leaks.\n\n#### Chrome's implementation\n\nThe concrete implementation may vary by browser, but in Chrome, cached resources are keyed using a Network Isolation Key in addition to the **resource URL**. The Network Isolation Key is composed of the **top-level site** and the **current-frame site**. Take the previous toy examples hosted on the origins `https://googlechrome.github.io` and `https://rawcdn.rawgit.net`. If they both use the Wasm runtime from `https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm`, their cache keys will look like in the following table.\n\n| Network Isolation Key                  | **Resource URL**                       |                                                                                                                               |\n| -------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |\n| **Top-level site**                     | **Current-frame site**                 |                                                                                                                               |\n| ``` 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 ``` |\n| ``` 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 ``` |\n\nSo even if the resource URLs are exactly the same, since the Network Isolation Keys don't match, there's no cache hit, which means duplicate download and duplicate storage. This is the challenge that the Cross-Origin Storage proposal aims to tackle.\n\n## Enter the Cross-Origin Storage API\n\n> **💡 Note:** The Cross-Origin Storage API is an early-stage proposal that isn't final. While the proposed API is not yet natively implemented in any browser, you don't have to wait to experiment with it. Install the [Cross-Origin Storage extension](https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih) to inject the `navigator.crossOriginStorage` polyfill on all pages and test the complete flow.\n\nThe proposed **[Cross-Origin Storage](https://github.com/WICG/cross-origin-storage) (COS) API** introduces a dedicated `navigator.crossOriginStorage` interface through which web apps can store and retrieve large files across origin boundaries, identified not by a URL, but by a cryptographic hash.\n\n![The Cross-Origin Storage API logo: a stylized walking person, as typically encountered on crosswalk signs.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/klwb5fryaa.png)\n\nThat last point about cryptographic hashes is key. Because COS identifies files by their **hash** rather than by their URL or origin, the same `ort-wasm-simd-threaded.asyncify.wasm` Wasm runtime you downloaded while visiting `https://googlechrome.github.io` is recognized as identical to the one `https://rawcdn.rawgit.net` is about to request, no matter where either of the two origins fetched it from. See the following code snippet that illustrates the basic flow.\n\n```js\nconst hash = {\n  algorithm: 'SHA-256',\n  value: '8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4',\n};\n\ntry {\n  const handle = await navigator.crossOriginStorage.getFileHandle(hash);\n  // Cache hit! Get the file as a Blob and use it directly.\n  const fileBlob = await handle.getFile();\n} catch {\n  // Cache miss. Download from network, then store for next time.\n  const fileBlob = await fetch('https://cdn.jsdelivr.net/.../ort-wasm-simd-threaded.asyncify.wasm')\n    .then(r => r.blob());\n  const handle = await navigator.crossOriginStorage.getFileHandle(\n    hash,\n    { create: true, origins: '*' },\n  );\n  const writableStream = await handle.createWritable();\n  await writableStream.write(fileBlob);\n  await writableStream.close();  \n}\n```\n\nIf the resource is in COS, you get back a [`FileSystemFileHandle`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle) from which you can read the blob directly via [`getFile()`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/getFile) (the resulting [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) inherits from [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob)). If the resource is not in COS, you fall back to the network, and write the resource into COS for the next app that needs it, which could be your app, or another unrelated app, potentially on a completely different origin.\n\nThe API is deliberately shaped after the [File System Standard](https://fs.spec.whatwg.org/)'s [`FileSystemDirectoryHandle.getFileHandle()`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/getFileHandle) you likely are familiar with from the [Origin Private File System](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) (OPFS) API. The `hash` parameter plays the same role as the `name` parameter in OPFS: uniquely identifying a resource. The `options.create` flag works the same way: absent or `false` for read-only access, `true` when you intend to write.\n\n### Control who can read what\n\nNot every resource should be globally shared. COS gives developers precise control over visibility through the `origins` option when storing a file.\n\n- Setting `origins: '*'` makes a file **globally available**. Any origin can find it by hash. This is the right choice for AI model resources or the Wasm runtime in the Transformers.js example: the whole point is that every app on the Web benefits from a single cached copy.\n- Passing a specific list of origins, like `origins: ['https://write.example.com', 'https://calculate.example.com']`, **restricts** access to those sites. This works well for proprietary resources shared across a company's own properties that shouldn't be discoverable by anyone else, like a proprietary proofreading AI model used in a commercial office suite.\n- Omitting `origins` entirely makes the file available only to **[same-site](https://web.dev/articles/same-site-same-origin#same-site-cross-site) origins**. This is a sensible default for resources shared across all of an organization's subdomains, but not intended to cross organizational boundaries.\n\nOne important rule: visibility can be upgraded but never downgraded. If a file is already globally available, a later attempt to store it with a restricted `origins` list is silently ignored. This prevents a malicious actor from re-storing a public resource and narrowing its availability. The reverse is possible: a file initially stored with a restricted `origins` list can later be made more permissive. Any site, not just the original storer, can call `getFileHandle()` for the same hash (hashes are not a secret) with `create: true` and a broader `origins` value, and given the browser verifies the hash matches, the resource becomes available to the wider audience from that point on. Note that the upgrading site **must** still write the full file through the returned handle. This requirement exists to prevent sites from exploiting the upgrade path as a side-channel to detect whether a particular file was already stored in COS.\n\n### Integrity by design\n\nA subtle but important property of COS is that the browser **verifies the hash** when you write a file. If the data you write doesn't match the declared hash, the write fails with an error. This makes integrity checking automatic: an app reading a file from COS can be confident it's getting exactly the bytes it expected. The same guarantee it would have had if it had computed the hash itself after a network download.\n\nThis turns out to be doubly useful in the Transformers.js scenario. Today, after downloading model weights, most apps have no practical way to verify that the CDN served the right bytes. With COS, every file in the store is implicitly verified on write, no matter where it came from, the official Hugging Face CDN or a random site's self-hosted mirror.\n\n### Privacy without sacrificing utility\n\nOf course a cross-origin shared cache raises the same question as the partitioned HTTP cache in reverse: if any site can probe for the presence of a file by hash, couldn't an attacker learn something about the user's browsing history by checking whether, say, a game engine Wasm module is cached?\n\nCOS addresses this through two complementary mechanisms:\n\n- First, the `origins` field: proprietary resources that shouldn't be globally probeable simply shouldn't be stored with `origins: '*'`, which, through **developer education**, developers are encouraged to consider whenever it makes sense.\n- Second, **availability gating**: even for globally declared files, the browser may suppress confirmation of a file's presence if it hasn't been encountered across a sufficient number of distinct origins. A file that only appears on one or two sites could still serve as a cross-site identifier, so the browser may return an error as if the file weren't there at all, regardless of what's physically on disk. On the Chrome team, we are conscious of the possible privacy leaks uncommon resources could cause and plan generally to mitigate it through restricting which exact resources can be cached. The concrete mitigations are still being fleshed out.\n\nCrucially, this means an error is not a definitive answer. It might mean \"not stored\", or it might mean \"stored, but the browser isn't telling you\". Apps should always handle it the same way: fall back to the network.\n\n### What this means for the Transformers.js example\n\nGoing back to the toy examples from before: the `ort-wasm-simd-threaded.asyncify.wasm` runtime weighs in at 4,733 kB and is shared by every Transformers.js-powered app regardless of which AI model it uses. With COS, the first app to load it downloads it once and stores it under its SHA-256 hash with `origins: '*'`. Every subsequent app, whether on `https://googlechrome.github.io`, on `https://rawcdn.rawgit.net`, or any other origin, finds it in COS immediately. The 177 MB of duplicate Whisper model weights? Same story: `Xenova/whisper-tiny.en` gets downloaded once, recognized by hash the second time around, and served from COS in milliseconds. And of course, the same also happens for `Xenova/distilbert-base-uncased-finetuned-sst-2-english`.\n\nTransformers.js itself is already piloting the COS API at the library level. [Pull request #1549](https://github.com/huggingface/transformers.js/pull/1549) introduced an experimental COS cache backend behind an opt-in flag. Enabling it takes a single line before you set up your pipeline:\n\n```js\nimport { env, pipeline } from \"https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0\";\n\n// 👇 Opt in to the experimental Cross-Origin Storage cache backend.\nenv.experimental_useCrossOriginStorage = true;\n\nconst asr = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny.en', { device: 'webgpu' });\nconst result = await asr('jfk.wav');\nconsole.log(result);\n```\n\nNote the `experimental_` prefix on the flag. It's intentional and signals that the underlying browser API has not yet been standardized and may change without a major version bump. With that flag set, Transformers.js resolves the SHA-256 hash for each [Xet-tracked](https://huggingface.co/docs/hub/en/xet/index) model file (the large ONNX weight files) by fetching the raw Xet pointer ([example raw pointer file](https://huggingface.co/Xenova/whisper-tiny.en/raw/main/onnx/decoder_model.onnx)) and extracting its `oid sha256:` field. It then uses that hash as the key for `navigator.crossOriginStorage`. If the model is already in COS (because another site stored it there first), it's served instantly without a network round-trip. If not, it falls back to a regular download and stores the result in COS for the next caller. With the toy example, the advantage in practice is that `Xenova/whisper-tiny.en` and `Xenova/distilbert-base-uncased-finetuned-sst-2-english` (and of course `ort-wasm-simd-threaded.asyncify.wasm`) only ever need to cross the ether once, regardless of how many different origins ask for them.\n\n### Model flexibility\n\nThe toy example works just fine with `Xenova/whisper-tiny.en`, but of course you possibly wouldn't say no if the user already has [any of the other Whisper variants](https://huggingface.co/Xenova/models?search=whisper) in their COS cache. For example, the user might already have [`Xenova/whisper-large-v3`](https://huggingface.co/Xenova/whisper-large-v3), which, as the name suggests, is a lot larger than the tiny variant. Transformers.js's [Model Registry](https://huggingface.co/docs/transformers.js/en/api/utils/model_registry) makes being flexible about your models possible. If you know your app's needs can be addressed by, for example, any of `Xenova/whisper-tiny.en`, `whisper-medium.en`, or `Xenova/whisper-large-v3`, you can check the registry for the associated files for each model, probe for their existence in the COS cache (which may partially or completely contain the model resources you need), and then take a decision what model to choose eventually. The [`ModelRegistry.is_pipeline_cached()`](https://huggingface.co/docs/transformers.js/en/api/utils/model_registry#modelregistryispipelinecachedtask-modelid-options--promise--boolean-) API directly integrates with COS (and of course the Cache API), so this operation is really ergonomic.\n\n### Try it today\n\nThe COS API is not yet natively implemented in any browser, but you don't have to wait to experiment with it. Install the [Cross-Origin Storage extension](https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih) to inject the `navigator.crossOriginStorage` polyfill on all pages and test the complete flow. Check out the [source code of the extension](https://github.com/web-ai-community/cross-origin-storage-extension) and follow the [usage instructions](https://github.com/web-ai-community/cross-origin-storage-extension?tab=readme-ov-file#usage) to get started.\n\n[![Chrome Web Store page for the Cross-Origin Storage extension.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/0q3rowmy67ta.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/0q3rowmy67ta.png)\n\nWith the extension installed, try the full end-to-end experience right now: open the first [toy example with COS enabled](https://googlechrome.github.io/samples/transformersjs-automatic-speech-recognition/index3.html), let it load `Xenova/whisper-tiny.en`, then open the [toy example with COS enabled from the second origin](https://rawcdn.rawgit.net/GoogleChrome/samples/1e4f2b8c10adc394352c6ec8327bb503bac7aba1/transformersjs-automatic-speech-recognition/index3.html). Instead of the 177 MB re-download you saw before, the model is served from COS in milliseconds. When you open the extension's popup window, you can see COS in action. If you **View by Resource**, you can see the resource with the SHA-256 hash `950978b1dbcbf250335358c1236053ba19a7f7849b33dc777f4421b72b7626fa` shared across `https://googlechrome.github.io` and `https://rawcdn.rawgit.net`. It may not be obvious, but as you can verify by comparing the SHA-256 hash on Hugging Face, you're looking at [`https://huggingface.co/Xenova/whisper-tiny.en/blob/main/onnx/decoder_model_merged.onnx`](https://huggingface.co/Xenova/whisper-tiny.en/blob/main/onnx/decoder_model_merged.onnx). For now, the extension is mostly aimed at power users like you. Once implemented in the browser, there will be a friendlier integration in the browser's **Settings** page. The screenshot below shows the extension's popup window with the **View by Resource** tab active, where you can see the shared resource with its hash and the two origins that have it in their COS cache.\n\n[![A resource seen in the Cross-Origin Storage extension, showing it's shared between two origins.](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/usg5dq7dhm.png)](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cross-origin-storage/usg5dq7dhm.png)\n\n## Call to action\n\nIf you're building your own Transformers.js app, the call to action is simple: add `env.experimental_useCrossOriginStorage = true` before your first `pipeline()` call, install the extension, and watch the duplicate downloads disappear from your Network tab. Every site that opts in makes the experience faster and cheaper for every other site's users. Opting in is completely risk-free: if the COS API isn't supported because the user doesn't have the COS extension installed, the code just falls back to the default path (the [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) API).\n\nTransformers.js is not alone in experimenting with COS. [WebLLM](https://webllm.mlc.ai/) (opt-in, see [documentation](https://webllm.mlc.ai/docs/user/advanced_usage.html#using-cross-origin-storage-cache)) and [wllama](https://github.com/ngxson/wllama) (automatic, see [PR](https://github.com/ngxson/wllama/pull/248)) are likewise excited about this proposed API.\n\nOn the Chrome team, we're [considering implementing the COS API](https://chromestatus.com/feature/5163371507875840) natively in the browser. As an early stage proposal, we welcome feedback on the API, and the shape of the proposal itself. The [Cross-Origin Storage repository](https://github.com/WICG/cross-origin-storage) is the place to file issues, [express support](https://github.com/WICG/cross-origin-storage/labels/expression%20of%20support), or open PRs.\n",
  "dateCreated": "2026-10-19T01:26:53.998049939+00:00",
  "date_created": "2026-06-23T00:00:00+00:00",
  "date_modified": "2026-06-23T00:00:00+00:00",
  "date_published": "2026-06-23T00:00:00+00:00",
  "description": "(This is a guest post by Developer Relations Engineer Thomas Steiner from the Chrome team at Google.) Transformers.js provides Web developers with a simple way to use the power of transformers in their Web apps through task-specific…",
  "external_url": "https://huggingface.co/blog/cross-origin-storage",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/huggingface-co/2026/06/2026-06-23-experimenting-with-the-proposed-cross-origin-storage-api-in.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/huggingface-co/2026/06/2026-06-23-experimenting-with-the-proposed-cross-origin-storage-api-in.md"
  },
  "id": "urn:aggr:item:d213899c7d0efcdff3ddde809628067ae1f0e8f6",
  "isBasedOn": {
    "@id": "https://huggingface.co/blog/cross-origin-storage"
  },
  "mainEntity": {
    "@id": "https://huggingface.co/blog/cross-origin-storage",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-06-23-experimenting-with-the-proposed-cross-origin-storage-api-in/#webpage"
    },
    "dateModified": "2026-06-23T00:00:00+00:00",
    "datePublished": "2026-06-23T00:00:00+00:00",
    "headline": "Experimenting with the proposed Cross-Origin Storage API in Transformers.js",
    "image": {
      "@type": "ImageObject",
      "caption": null,
      "height": 90,
      "url": "https://huggingface.co/blog/assets/cross-origin-storage/thumbnail.jpg",
      "width": 160
    },
    "timeRequired": "PT13M",
    "url": "https://huggingface.co/blog/cross-origin-storage",
    "wordCount": 2903
  },
  "name": "Archived snapshot: Experimenting with the proposed Cross-Origin Storage API in Transformers.js",
  "preview": {
    "alt": null,
    "color": null,
    "height": 90,
    "placeholder": {
      "data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
      "hash": ""
    },
    "url": "https://huggingface.co/blog/assets/cross-origin-storage/thumbnail.jpg",
    "width": 160
  },
  "reading_minutes": 13,
  "replicated_at": null,
  "source": "huggingface-co",
  "source_name": "Hugging Face - Blog",
  "summary": null,
  "tags": [],
  "temporalCoverage": "2026-10-19T01:26:53.998049939+00:00",
  "title": "Experimenting with the proposed Cross-Origin Storage API in Transformers.js",
  "url": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-06-23-experimenting-with-the-proposed-cross-origin-storage-api-in/",
  "word_count": 2903
}