A static website doesn't have to stop working when the internet connection disappears.
I wanted PocketPDF to keep working after its first load, including when someone opens it somewhere with no connection.
That meant building an offline strategy around a service worker.
The important distinction is that a service worker isn't just a way to cache the homepage. It needs to understand which resources are essential, which can be downloaded later, and which should never be stored.
Start with the application shell
The first thing I wanted available offline was the application itself.
That includes the HTML, CSS, JavaScript needed to navigate the site, and the basic resources required to open the tools.
These are relatively small files and make sense to precache.
The vendor libraries are another important part of the application shell.
If a tool depends on a local JavaScript library, having the interface cached but not the library doesn't help much.
For example, a PDF tool needs its PDF-related dependencies available before it can perform the operation.
Don't download everything immediately
There is a difference between caching a resource and downloading every resource during the first visit.
Some libraries are large and only needed by particular tools.
I use lazy loading for heavy dependencies, so someone opening a basic image utility doesn't need to download a PDF engine.
The service worker can cache these resources when they are first requested.
The general idea looks something like this:
const CACHE_VERSION = "pocketpdf-v3";
const SHELL_FILES = [
"/",
"/css/style.css",
"/js/core.js",
"/js/tools-index.js"
];
self.addEventListener("install", event => {
event.waitUntil(
caches.open(CACHE_VERSION)
.then(cache => cache.addAll(SHELL_FILES))
);
});
self.addEventListener("fetch", event => {
const request = event.request;
if (request.method !== "GET") return;
event.respondWith(
caches.match(request).then(cached => {
return cached || fetch(request);
})
);
});
This is a simplified example, not the complete production caching strategy. In practice, I also need to handle cache updates, failed requests, and different resource types.
What should not be cached blindly
One mistake with service workers is treating every request as something worth storing.
I avoid blindly caching API responses because they can contain changing or user-specific information.
I also avoid caching the service worker core itself as an ordinary application asset. The browser needs to be able to fetch and install an updated worker when I release changes.
Caching the worker incorrectly can make updates difficult to deliver.
The same applies to dynamic content. A cached response might be outdated even when the user expects fresh information.
Stale caches are a real maintenance problem
This is probably the most frustrating part.
You fix a bug, deploy the updated JavaScript, open the website, and the browser continues using an older cached file.
The code is correct on the server, but the user is still executing the previous version.
Versioned cache names help:
const CACHE_NAME = "pocketpdf-v4";
When the version changes, the new worker can populate a new cache and old caches can be cleaned up during activation.
However, changing a cache name alone isn't a complete update strategy. The worker lifecycle, existing tabs, and resource loading all need to be considered.
I have to be particularly careful when changing vendor libraries because an old cached application file combined with a new dependency can create unexpected problems.
What offline actually means for the user
After the initial visit and the necessary resources have been cached, the tools can continue working without an internet connection.
Someone on a plane can open the site and merge two PDFs, provided the relevant application files and dependencies have already been downloaded.
OCR has an additional requirement: its Tesseract WebAssembly engine and language data need to be downloaded before the first offline OCR session.
That is an important distinction. Offline support doesn't mean every possible resource is magically available before it has ever been requested.
Why I bothered
I didn't want PocketPDF to depend on a permanent connection just to run JavaScript that is already stored in the browser.
A static site, local processing, lazy loading, and service worker caching work well together.
There are still edge cases to handle, especially around updates and large dependencies, but the result is a tool collection that can keep working after the first load.
The project is available at https://pocketpdf.site