Making My Blog Work Offline with PWA
PWA is a buzzword. For this blog, PWA means two things: manifest.json and Service Worker. manifest.json tells the browser “this is an app”, Service Worker makes it work offline.
This article focuses on Service Worker since it’s the complex part. manifest.json is straightforward, covered at the end.
What is Service Worker
A Service Worker is a script that runs in the background of your browser. It’s independent of the web page, working silently behind the scenes.
Think of it as a middleman between browser and network. Normally, browser requests a resource (HTML, JS, image) directly from the server. With Service Worker, the request goes through it first:
Browser → Service Worker → Network (or cache)
Service Worker decides: fetch from network, return from local cache, or both.
This blog’s Service Worker is generated by Workbox, about 5KB.
What It Does
Offline Access
The core feature. After a user visits the blog once, the Service Worker caches static resources. Next time, even without network, previously viewed articles are accessible.
Faster Loading
Cached resources load from local storage, no network wait. On second visit, most resources load instantly.
Independent from Pages
Service Worker runs independently. Even after closing the page, it can still work in the background — background sync, push notifications, etc. This blog doesn’t use those features though.
One frustration: SW is great in concept, but debugging it is painful. Every time I changed caching strategy and deployed, I couldn’t tell if it worked in the browser. Eventually discovered Chrome DevTools Application panel has a manual unregister button. That helped a lot.
How It’s Generated
Not hand-written. Using Workbox’s generateSW mode, auto-generated during Gulp build.
In gulpfile.js:
gulp.task('generate-service-worker', () => {
return workbox.generateSW({
globDirectory: './public',
globPatterns: ['**/*.{woff2,woff,js,css,png.jpg}'],
globIgnores: ['sw.js'],
swDest: `./public/sw.js`,
clientsClaim: true,
skipWaiting: true,
runtimeCaching: [
{ urlPattern: /.*\.js/, handler: 'NetworkFirst' },
{ urlPattern: /.*\.css/, handler: 'StaleWhileRevalidate' },
{ urlPattern: /.*\.(?:png|jpg|jpeg|svg|gif)/,
handler: 'CacheFirst',
options: { cacheName: 'images', expiration: { maxEntries: 50 } } },
{ urlPattern: /.*\.html/, handler: 'NetworkFirst' }
]
})
.then(() => console.info('SW generated.'))
.catch(error => console.warn('SW generation failed: ' + error));
});
globPatterns tells Workbox which files to precache. After build, Workbox scans public/, writes matching file list into SW. User caches everything on first visit.
Generated sw.js is about 5KB, placed in public/ root.
Caching Strategies
Four resource types, four handling methods.
JS: NetworkFirst
{ urlPattern: /.*\.js/, handler: 'NetworkFirst' }
JS files update frequently. Try network first for latest code. Fall back to cache if network unavailable.
CSS: StaleWhileRevalidate
{ urlPattern: /.*\.css/, handler: 'StaleWhileRevalidate' }
CSS doesn’t change often. Return from cache immediately, update in background. User doesn’t wait for network.
Images: CacheFirst
{ urlPattern: /.*\.(?:png|jpg|jpeg|svg|gif)/,
handler: 'CacheFirst',
options: { cacheName: 'images', expiration: { maxEntries: 50 } } }
Images rarely change. Serve from cache. Max 50 entries, evict oldest when full.
HTML: NetworkFirst
{ urlPattern: /.*\.html/, handler: 'NetworkFirst' }
Article content updates dynamically. Users should see latest version. Try network first, cache fallback when offline.
Registration
Service Worker needs to be registered in the page to activate. In layouts/partials/js.html:
if ('serviceWorker' in navigator) {
if (location.host !== 'time-friend.com') return;
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js')
.catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
Three conditions:
- Browser support:
serviceWorker in navigator - Production only:
location.host !== 'time-friend.com'— prevents SW registration in development - After page load:
window.addEventListener('load')— doesn’t block initial render
After registration, browser downloads sw.js, installs, activates. Nothing is cached on first visit. It works starting from the second visit.
There’s a subtle problem though: after updating article content and redeploying, if a user visited before, SW might still serve old cache. NetworkFirst strategy helps for HTML, but if the network is slow, the user might see old content briefly before the new one replaces it. No perfect solution for this.
manifest.json
The other half of PWA is manifest.json. Stored at static/manifest.json, copied to public/ root during build:
{
"name": "WEB BANG! BANG!! BANG!!!",
"short_name": "前端大爆炸",
"theme_color": "#1d1f21",
"background_color": "#1d1f21",
"display": "standalone",
"Scope": "/",
"start_url": "/",
"icons": [
{ "src": "https://time-friend.com/images/50x50.png", "sizes": "144x144", "type": "image/png" },
{ "src": "https://time-friend.com/images/50x50.png", "sizes": "128x128", "type": "image/png" },
{ "src": "https://time-friend.com/images/50x50.png", "sizes": "96x96", "type": "image/png" }
]
}
Referenced in head.html:
<link rel="manifest" href="/manifest.json">
What it does: when a user visits on a PWA-supporting browser, the browser prompts “add to desktop”. After adding, it looks like a native app with icon and splash screen.
The icons are all scaled from a single 50x50 image, pretty lazy. But honestly, not many people actually add a blog to their desktop.
Performance Impact
Positive:
- Second visit onward, most resources load from cache, significantly faster
- Works offline, readable without network
- SW file only 5KB, negligible bandwidth
Negative:
- First visit downloads extra SW and cache (but the total is small)
- After SW update, user needs to close and reopen page to fully switch to new version (
skipWaitinghelps but can’t force refresh) - With NetworkFirst, slow network might show cached old content briefly before updating
For this blog, the positives far outweigh the negatives.