0%
Reading Settings
Font Size
18px
Line Height
1.5
Letter Spacing
0.01em
Font Family
Table of contents
PWA and Service Worker: Making Your Web App Feel Native
Software Engineer
Software Engineer
Frontend
Frontend
You might have seen browsers allow you to install a website like an app. Building a native application takes significant effort, and not many common applications support it. Google Calendar and Gemini are examples. To use them like a native app, I just install them via Chrome as a web application.
The advantage of installing a web app is that it runs in its own window, appears in your taskbar or dock, loads faster on repeat visits, and can work offline. You get most of what a native app offers without going through an app store. That's what a Progressive Web App (PWA) enables. And the Service Worker is the piece of technology that makes most of it possible.
What is a Progressive Web App (PWA)?
A progressive web app (PWA) is an app that's built using web platform technologies, but that provides a user experience like that of a platform-specific app.
Ref: Progressive web apps
A PWA meets a set of criteria to unlock native-like capabilities. Not a framework, not a library, just a set of standards your app can progressively adopt. The three pillars are:
- Reliable: loads instantly, even on flaky networks or offline.
- Fast: responds quickly to user interactions.
- Engaging: installable on the home screen, can send push notifications.
To be installable, a PWA needs at minimum: a manifest.json , a registered Service Worker, and HTTPS.
The Web App Manifest
The manifest is a JSON file that tells the browser how your app should behave when installed: its name, icons, theme color, and launch behavior.
// language: javascript
{
"name": "My Blog",
"short_name": "Blog",
"start_url": "/",
"display": "standalone",
"theme_color": "#0f0f0f",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}The display: standalone key is important. It removes the browser chrome (address bar, back button) when launched from the home screen, making it feel like a real app.
Link it in your HTML:
Link it in your HTML:
// language: html <link rel="manifest" href="/manifest.json" /> <meta name="theme-color" content="#0f0f0f" />
What is a Service Worker?
A Service Worker is a JavaScript file that runs in a separate thread, with no DOM access, no blocking the UI. It acts as a programmable proxy between your app and the network.
// language: text
Your App ── fetch() ──> Service Worker ── maybe ──> Network
│
└──── or ──> CacheEvery network request passes through the Service Worker first. You decide: hit the network, serve from cache, or both.
Registering a Service Worker
// language: javascript
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then(reg => console.log('SW registered', reg.scope))
.catch(err => console.error('SW failed', err));
}The file path matters. A
- Putting SW in
/assets/sw.js : scope becomes/assets/ (almost useless) - Expecting
/blog/sw.js to control the entire site - Forgetting leading
/ : relative path issues
Every time you load a page that calls the above registering script, the browser:
- Fetches
/sw.js - Compares it with the cached version
- If byte-different, it will trigger an update
Service Worker Lifecycle
Install
Fires the first time the browser sees your SW, or when the SW file changes. Pre-cache your shell here.
// language: javascript
const CACHE = 'blog-v1';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE).then(cache => cache.addAll(['/', '/offline.html']))
);
self.skipWaiting(); // activate immediately
});Activate
Fires after install. Clean up old caches from previous deploys here.
// language: javascript
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
)
);
self.clients.claim();
});Fetch
Fires on every network request. Your caching strategy lives here.
// language: javascript
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cached => cached || fetch(event.request))
);
});Caching Strategies:
- Cache First: Serve cache, fall back to network
- Network First: Try network, fall back to cache
- Stale While Revalidate: Return cache immediately, update in background
- Network Only: Always hit the network
Cache clears when:
- You bump the cache name: changing
blog-v1 toblog-v2 means the activate event deletes the old one on the next deploy. This is the standard pattern. - The user clears browser data: DevTools or browser settings.
- Browser storage pressure: if the device is critically low on disk, the browser may evict your cache. No guarantee of permanence.
- You delete it explicitly:
caches.delete('blog-v1') orcache.delete(specificRequest)
Conclusion
You don't have to go all in. Start with a manifest and a minimal Service Worker that shows an offline page. Then layer in smarter caching as you understand your app's traffic patterns.
Related blogs
Optimizing Bulk Create Operations in Rails
Recently, my team ran into performance issues while handling bulk CUD (Create, Update, Delete) operations in a Rails application.Active Record provides validations and callbacks, which are excellent tools for maintaining data consistency. Rails contr...
Software Engineer
Software Engineer
Ruby on Rails
Ruby on Rails
CORS and CSRF: How Attackers Exploit the Gaps
I used to think CORS was a security feature. It is, partially. But the more I understood it, the more I realized how narrow its protection actually is. This post walks through how CORS works under the hood, where it breaks down, what CSRF is, and how...
Software Engineer
Software Engineer