swiiifz: IIIF from a zip, served by a Service Worker
2026-10-08 tags: iiif minimal computing static digital libraryI have always been a bit obsessed with serving static web content that is portable, cheap, maintenance-free, and easy to distribute and mirror. My reasons are practical. I often work on non-profit projects in my spare time, with little or no budget. The web is also changing fast: hosting and server prices are rising, and crawlers are devouring resources, making it harder to keep dynamic applications running. There is a political side too. The governance of the web is fragile, and content can be blocked, taken down or lost for political reasons. Content that is easy to copy and mirror is much harder to make disappear.
The idea of serving digitized documents as zipped packages came to me in 2014, with deepzoom-osd-server. I later wrote about it in IIIF, tropy, canopy and SZI. Earlier this year I built mkiiif for my own needs, but one question stayed open: how do you serve this packaged content?
I knew Service Workers and HTTP range requests could be the answer, since Webrecorder uses the same pattern to replay WACZ files. But Service Workers have always been black magic to me, so I asked Claude to help me build a proof of concept. Here it is: github.com/atomotic/swiiifz, with a demo.
sw · iiif · z = Service Worker · IIIF · Zipped (pronounced "swiff-zee")
What it does
A IIIF Image API level 0 tree has one info.json and thousands of pre-cut tiles per image, plus a Presentation manifest. That normally means thousands of small files to upload, version and keep in sync.
swiiifz puts the whole tree inside one uncompressed zip on any static host. A Service Worker of about 170 lines turns it back into ordinary IIIF URLs in the browser. The viewer and IIIF clients don't know the zip exists. They request …/page-001/info.json or …/page-001/0,0,512,512/512,512/0/default.jpg like any other IIIF resource, and the worker answers by reading the file out of the zip with an HTTP Range request.
There is no server code, no build step on the host and no index file. You only need a host that supports Range requests.
How it works
- Routing. A request for
<id>/<path>is answered from the entry<id>/<path>insidezip/<id>.zip. Everything else goes to the network unchanged. - Opening a zip takes two requests. The worker reads the end of the file to find the zip's central directory, then fetches and parses it into an in-memory map of file names to offsets.
- Reading a file takes one request. The worker fetches just that file's bytes with a
Rangerequest, so each tile costs about the same as a normal static tile. - Consistency. If the zip is replaced on the server, the worker notices (via
ETagandIf-Range), reopens it and retries. It never mixes bytes from two versions. - Compression. Tiles are JPEGs, so the zip is built without compression (
zip -0). Compressed entries still work, because the worker can inflate them.
You can watch this in your browser's DevTools. Every tile appears twice in the Network tab: once as the virtual request answered by the Service Worker, and once as the 206 Partial Content request the worker sends to the zip.
The demo
The demo uses a 106-page Italian archival journal, ARCHIVI & COMPUTER (1995), converted with mkiiif:
| Source PDF | 13 MB |
| IIIF tree | 1,910 files, 46 MB |
| Zip | 1 file, 42 MB |
Trade-offs
Pros
- One file to upload, version, checksum, mirror or delete.
- Replacing the zip is atomic.
- No file-count limits, and git and CDNs stay happy.
Cons
- The first view costs a few extra requests, plus the Service Worker's start-up time.
- It only works in browsers with Service Workers. Harvesters,
curland server-side tools see a zip, not a IIIF endpoint. - It is still a proof of concept: no response caching, no ZIP64 (so a zip must stay under 4 GB), and the index is lost when the worker restarts.
The big limitation: interoperability
There is a major limitation. IIIF's interoperability is lost, because the manifest and tiles are not public, referenceable URLs. They exist only inside the browser, through the Service Worker.
Could this be solved? Perhaps the manifest should be served outside the zip. A new IIIF extension, something like "IIIF Level 0 packaged", could then tell viewers and clients how to load the content from the package.
What's next
The current demo is not truly portable. IIIF ids are absolute URLs, so the manifest is tied to the address it was built for. I have a branch where I am testing a rewrite so that the same zip works on any host.