Command Palette

Search for a command to run...

Hectal
PHASE 12Advanced ~28 min· topic 4 of 8

Topic 12.4

Downloading and Caching Models

In one line

A browser model is a large download. Show honest progress, save the files in the Cache API so the next visit is instant, handle a full disk and an interrupted download, and put a version in the cache name so old files can be replaced.

Think of it like this

Installing a big game. The first time there is a long download with a progress bar. If the connection drops at 40 percent, a good installer carries on from 40 percent. Next time you start the game, there is no download at all. If the disk is full, it tells you what to delete. And when there is an update, the old files are replaced. A model in the browser needs the same care: progress, resume, space, version.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Content-Length
A response header that gives the size of the body in bytes. It is what lets you show a percentage.
Cache API
A browser store for request and response pairs, used for files like model weights. Methods: caches.open, put, match.
Quota
The amount of storage the browser allows a site. Saving past it throws QuotaExceededError.
Range request
An HTTP request for part of a file, such as Range: bytes=400-. The server answers 206 if it supports it.
Resume
Continuing a download from where it stopped instead of starting over.
Persistent storage
A request, navigator.storage.persist(), asking the browser not to clear the site's data when space runs low.
Cache key / cache name
The name under which files are stored. Putting a version in it lets you replace old files.
Indeterminate progress
A progress display that shows activity without a percentage, used when the total is unknown.

Step by step

01Count the bytes

The first live example serves a 1000-byte "model" and downloads it with a stream reader. Each chunk adds to the received bytes and the code prints the percentage when it moves into a new quarter. The server here is a serve() handler: in a real app this is a static file on your server or CDN, and the browser does the same job.

02Survive a break

The server breaks the first response after 400 bytes. The reader throws, and the code does the important thing: it keeps the bytes it already has. It then asks again with Range: bytes=400- and the server answers with status 206. The output shows starting with status 200, then resuming with status 206.

The final line compares every byte with the original. A resume that appends to the wrong position is a common mistake and gives a corrupt file. Real systems also check a hash of the finished file.

03Save it, with a version

The second example is a small stand-in for the Cache API with a quota, since the real one is unavailable in the preview. The cache name includes v2. It first creates an old models-v1 cache, deletes it with cleanOldVersions(), then saves a model (network, now cached), reads it back (cache), and tries a file too big for the quota.

That last file is not saved, and the app still gets its bytes: it uses the model from memory for this visit and says it could not keep a copy. A full disk should cost the user a slower next visit, not the feature.

Save it, with a versiondiagram
Rendering diagram…

04The real browser calls

In a real page, the same ideas use these calls. They are not live here: in the sandboxed preview the Cache API throws a SecurityError and navigator.storage.estimate() rejects with a TypeError, so a real app guards each of them with try … catch and treats a failure as "no cache, download again".

storage.js (real browser APIs, not live)whole filejs
const cache = await caches.open("models-v2");
const hit = await cache.match("/models/intent.bin");

const { usage, quota } = await navigator.storage.estimate(); // both in bytes
const persisted = await navigator.storage.persist();          // true if the browser agrees

try {
  await cache.put("/models/intent.bin", response);
} catch (e) {
  if (e.name === "QuotaExceededError") { /* use the model without saving it */ }
  else throw e;
}

05The progress screen

Show: the model name and size, a progress bar with a percentage, a Cancel button, and what happens next ("This is saved on your device, so next time it is instant"). On failure show the reason in plain words ("Download interrupted. Retry") and keep the bytes so Retry resumes. Make the bar a real <progress> element or give it role="progressbar" with aria-valuenow, so assistive technology can read it.

Try it yourself

  1. 1

    Break it twice

    In the first example change requests === 1 ? 400 : Infinity to requests <= 2 ? 400 : Infinity. The second attempt breaks too (at 400 more bytes, from where it resumed).

    The code has no loop, so it throws. Wrap the download in a loop that retries up to 3 times. Notice that each retry resumes from the bytes already kept.

  2. 2

    Bump the version

    In the second example change MODEL_VERSION to "v3" and run it again. The v1 cache is still removed by cleanOldVersions().

    Add a line that creates a models-v2 cache before the cleanup. It is removed too: only the current name survives.

  3. 3

    Progress when the size is unknown

    In the first example the total is a constant. A real server may send no Content-Length (and for a compressed response the header gives the compressed size, while the stream delivers the unpacked bytes, so the two can disagree). Pretend total is 0: received.length / 0 is Infinity, and the code would print Infinity%.

    Write const label = (got, total) => total > 0 ? Math.floor((got / total) * 100) + "%" : got + " bytes so far"; and call it with (400, 1000) and (400, 0). You get 40% and 400 bytes so far. On the page, that second form is an indeterminate bar: it shows that something is happening, and never invents a percentage.

Code & diagrams

Download with progress, an interruption, and a resume

The "server" is a serve() handler that supports the Range header and deliberately breaks the first response. The file is 1000 bytes, only so the example stays small.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

starting with status 200
progress: 10%
progress: 30%
interrupted at 400 of 1000 bytes: network error
resuming with status 206
progress: 50%
progress: 80%
progress: 100%
complete and identical: true
Versioned cache and a full disk

The real Cache API is blocked in this sandboxed preview, so the example uses a stand-in with the same method names and a size limit. The page shows whether the real one was usable; the console shows only that the check did not throw.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

the check itself never throws: true
caches before: models-v1
caches after cleanup: 
{"from":"network, now cached","bytes":400}
{"from":"cache","bytes":400}
{"from":"network only","bytes":900,"warning":"could not save for next time"}
bytes stored: 400

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Resume without asking for the rest

In the first example change the fetch line so it always sends {} as options (no Range header), then run it.

terminal
$ The end of the output
── what you'll see ──
resuming with status 200
progress: 50%
progress: 80%
progress: 100%
progress: 130%
complete and identical: false

Break #2

Forget that saving can fail

In the second example change if (e.name === "QuotaExceededError") return to if (false) return.

terminal
$ The last line of the output
── what you'll see ──
Uncaught QuotaExceededError: Quota exceeded

Myth vs fact

Myth

The browser keeps everything I save forever.

Fact

Storage can be cleared by the user or by the browser when space is short, unless persistent storage was granted. Always handle a cache miss.

Myth

A progress bar is optional for a few megabytes.

Fact

On a slow phone connection even 20 MB takes minutes. A frozen screen looks like a bug and users leave.

Myth

If the download breaks, just start again.

Fact

That wastes data and time. Keep the bytes and resume with a Range request, or split the model into shards.

Myth

The cache updates itself when I deploy a new model.

Fact

A cached file keeps matching its URL. Put a version in the URL or cache name and remove old caches.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Prefer many shards to one huge file. Several smaller files can be downloaded in parallel, can each be saved or retried on their own, and a failure costs one shard, not the whole model. Check how your library and model host split files.

  • ▸

    Hash what you download. A corrupt model file may load and give wrong answers silently. A checksum (for example SHA-256 through crypto.subtle.digest) of the finished file, compared with a value you publish, catches truncation, bad resumes and tampering.

  • ▸

    Ask before downloading. A model is a cost to the user: data, battery and disk. Show the size and ask, and honour the browser's data-saver signal where available. Never start a big download in the background of an unrelated page.

  • ▸

    Plan for eviction. Even a persistent cache can be deleted by the user's "clear site data". The first-run flow should be the same as the flow after eviction: no special errors, just a download.

Remember this

  1. 1

    The problem this solves: A page that starts a download of tens of megabytes and shows nothing looks frozen. A download that restarts from zero after every hiccup wastes the user's data plan. A model that downloads again on every visit makes the feature feel slow and costs you bandwidth. And a model file that never gets replaced keeps old answers forever. This topic covers all four.

  2. 2

    Size first. The weights of a model are the bulk of the download, and sizes differ enormously between models and precisions (Topic 12.2). Do not guess: read the model card and the file list, or measure. For a sense of scale, this site's own search lab uses an 8-bit version of all-MiniLM-L6-v2 that its source comments describe as about 23 MB. Larger chat models are far bigger. Tell the user the size before starting, and ask first on a metered connection.

  3. 3

    Progress needs a total. A progress bar needs two numbers: bytes received and bytes expected. The response header Content-Length gives the expected size when the server sends it. You read the body as a stream (Topic 0.4) and count the bytes of each chunk. If there is no total (compressed responses may not give one), show an indeterminate bar instead of lying with a percentage. Libraries wrap this: Transformers.js gives a progress_callback with file, loaded and total per file, and a model has several files, so you add them up (Topic 12.2, practice).

  4. 4

    The Cache API is a browser store for request-and-response pairs, made for exactly this: const cache = await caches.open(name), then cache.put(url, response) to save and cache.match(url) to look up. Files in it survive a page reload and a browser restart. Transformers.js uses it by default (env.useBrowserCache, with the cache name from env.cacheKey, default "transformers-cache"), and WebLLM defaults to a "cache" backend as well (it can also use IndexedDB or OPFS). The Cache API needs a secure context, and it can be blocked in other ways: in the sandboxed frame of this page, even reading typeof caches throws a SecurityError. So the second live example asks inside try … catch and shows the answer on the page.

  5. 5

    Storage has limits. The browser gives each site a quota, which depends on the device and the free disk space. navigator.storage.estimate() returns an object with usage and quota in bytes. MDN calls both values approximate (the quota is a conservative estimate, and it can differ between sites), so treat them as a hint. Saving past the quota fails with a QuotaExceededError in practice; MDN's Cache page does not name the error, so handle any rejected put(), and test for the name only to give a better message. The browser may also clear a site's storage when the device is short of space, unless the site has asked for persistent storage with navigator.storage.persist(), which the browser may grant or refuse. MDN adds that when a browser clears Cache API data it generally clears all of an origin's data or none of it, so a model can vanish together with everything else your site saved. So: check the estimate before a big download, catch the quota error when saving, and treat the cache as something that can disappear.

  6. 6

    Interrupted downloads. Networks drop. HTTP lets a client ask for part of a file with the Range header (Range: bytes=400-), and the server answers 206 Partial Content with a Content-Range header, if it supports ranges. The first live example does this by hand: a download breaks at 400 of 1000 bytes, the code keeps those bytes, asks for bytes=400-, and ends with a file identical to the original. Whether your library resumes by itself varies, so check it, and for very large files consider splitting the model into shards you download and store separately.

  7. 7

    Versioning. Put the model version in the cache name (models-v2) or in the file URLs. A new version is then a new cache; after it is saved, delete caches that start with models- but are not the current one. Without a version, an updated model on your server is never fetched, because the cached old one always matches. The second example shows the cleanup.

Explain it without notes

01

How do you show honest progress for a model download, and what if the size is unknown?

02

What is the Cache API used for here, and what limits it?

03

How does a resumable download work, and what must the server support?

04

How do you make sure users get a new model version after a deploy?

Practice

01

Wrap the first example's download in a loop that retries up to 3 times, resuming each time. Throw "giving up" if it still fails.

02

Write ensureSpace(neededBytes) for the real browser: use navigator.storage.estimate() and return false if quota - usage is smaller than neededBytes.

Trade-offs

  • ↔

    Caching makes every visit after the first instant, at the cost of the user's disk space and the need to version and clean up. Not caching is simpler but makes each visit pay the download.

  • ↔

    Resuming saves the user's data but adds code and needs a server that supports ranges. Sharding is more flexible and also needs a manifest of the pieces.

Done when you can

  • I can show a download progress bar from a stream and Content-Length.

  • I can explain Range requests and 206, and write a resume that checks its result.

  • I can save files in the Cache API and handle QuotaExceededError.

  • I can put a version in a cache name and delete old caches.

  • I can name what I show the user before, during and after a model download.