Overview
Progressive Web Apps (PWAs) represent a web development approach that combines traditional web technologies with native application capabilities. A PWA delivers an app-like experience through standard web technologies—HTML, CSS, and JavaScript—while providing features historically exclusive to native applications: offline functionality, push notifications, home screen installation, and background synchronization.
The term "Progressive Web App" emerged from Google engineers Alex Russell and Frances Berriman in 2015, describing web applications that progressively enhance based on browser capabilities. The core premise centers on graceful degradation: applications function on all browsers but take advantage of advanced features where available.
PWAs operate through three fundamental technologies: service workers for offline functionality and background processing, web app manifests for installation metadata, and HTTPS for secure contexts. Service workers function as programmable network proxies, intercepting and controlling network requests. The web app manifest provides JSON configuration defining application metadata, icons, display modes, and theme colors. HTTPS ensures secure communication required for service worker registration and sensitive API access.
// Service worker registration in application entry point
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker registered:', registration.scope);
})
.catch(error => {
console.error('Service Worker registration failed:', error);
});
}
The progressive enhancement model means PWAs work across all browsers and devices while offering enhanced experiences on supporting platforms. On Chrome for Android, users can install PWAs to their home screen with full-screen launch capabilities. On iOS Safari, PWAs function with limited service worker support but still provide offline capabilities and home screen installation.
PWAs address several web development challenges: eliminating app store gatekeepers, reducing deployment friction, enabling instant updates, decreasing application size, and improving engagement through features like push notifications and offline access. Companies adopt PWAs to unify web and mobile experiences under single codebases while maintaining native-like performance characteristics.
Key Principles
PWAs adhere to specific architectural principles distinguishing them from traditional web applications. These principles form the foundation for reliable, performant, app-like experiences.
Progressive Enhancement: Applications function on all browsers regardless of capability level. Basic functionality remains accessible on older browsers while modern browsers receive enhanced features. This approach prevents feature detection from blocking core application access. The service worker API checks for support before registration; applications continue functioning without it.
Responsive Design: PWAs adapt to any screen size, orientation, and input method. The single codebase serves mobile phones, tablets, desktops, and emerging form factors. Responsive design extends beyond viewport adaptation to include input methods (touch, mouse, keyboard) and connection quality (offline, slow 3G, broadband).
Connectivity Independence: Service workers enable operation regardless of network conditions. Applications cache critical resources during installation, intercept network requests, and serve cached responses when offline. This principle extends to background synchronization, deferring actions until connectivity returns.
App-like Interface: PWAs present immersive experiences through full-screen display modes, app-style navigation patterns, and platform-appropriate visual design. The web app manifest defines display preferences while application architecture implements navigation stacks and gesture handling characteristic of native applications.
Fresh Content: Despite aggressive caching, PWAs maintain content freshness through strategic cache invalidation and background updates. Service workers implement cache-first or network-first strategies based on resource criticality. Background sync APIs update content during idle periods.
Secure Contexts: PWAs require HTTPS for service worker registration and sensitive API access. This requirement prevents network-level attacks on service worker code and protects sensitive user data. Development environments permit localhost service worker registration without HTTPS.
Discoverable: Search engines index PWAs as standard websites while supporting enhanced metadata through web app manifests. This discoverability contrasts with native applications requiring app store searches and installations.
Re-engageable: Push notifications and background synchronization maintain user engagement without requiring open applications. Web Push API enables server-initiated notifications while background sync handles deferred operations.
Installable: Users add PWAs to home screens or application launchers through browser prompts or manual menu actions. Installation creates standalone application experiences with dedicated windows, task switcher entries, and operating system integration.
Linkable: PWAs maintain web fundamentals through URL-based navigation and deep linking. Specific application states map to URLs, enabling sharing, bookmarking, and search engine indexing. This linkability differentiates PWAs from native applications requiring custom URL schemes.
Service worker lifecycle management implements these principles. Service workers transition through installation, activation, and idle states. During installation, applications pre-cache critical resources. Activation phase performs cleanup of outdated caches. Idle state handles fetch events, intercepting network requests for offline functionality.
// Service worker lifecycle events
self.addEventListener('install', event => {
event.waitUntil(
caches.open('app-v1').then(cache => {
return cache.addAll([
'/',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
]);
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.filter(name => name !== 'app-v1')
.map(name => caches.delete(name))
);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
The manifest file declares application metadata and behavior preferences. Browser implementations parse manifests to generate installation prompts, configure standalone windows, and define theme colors. Manifest properties include name, short name, icons at multiple resolutions, start URL, display mode, orientation preferences, and theme colors.
Ruby Implementation
Ruby web frameworks implement PWA capabilities through service worker generation, manifest configuration, and asset pipeline integration. Rails applications particularly benefit from built-in asset management and caching strategies that align with PWA requirements.
Rails applications serve PWAs by generating service worker JavaScript files, configuring web app manifests, and implementing offline-capable API endpoints. The serviceworker-rails gem provides Rails integration for service worker generation and serving. This gem generates service workers from ERB templates with access to Rails routing and asset pipeline helpers.
# Gemfile
gem 'serviceworker-rails'
# config/initializers/serviceworker.rb
Rails.application.configure do
config.serviceworker.routes.draw do
match '/service-worker.js'
match '/manifest.json'
end
end
Service worker templates access Rails routing helpers and asset paths, ensuring correct URL generation across environments. The template system supports ERB preprocessing, enabling dynamic cache list generation based on compiled assets.
# app/assets/javascripts/service-worker.js.erb
var CACHE_VERSION = 'v1';
var CACHE_NAME = CACHE_VERSION + ':sw-cache-';
function onInstall(event) {
event.waitUntil(
caches.open(CACHE_NAME + 'assets').then(function(cache) {
return cache.addAll([
'<%= asset_path "application.js" %>',
'<%= asset_path "application.css" %>',
'<%= asset_path "logo.png" %>'
]);
})
);
}
self.addEventListener('install', onInstall);
Rails applications generate web app manifests through JSON templates with Ruby string interpolation. The manifest controller responds to manifest requests with appropriate content types and caching headers.
# config/routes.rb
get '/manifest.json', to: 'pwa#manifest'
# app/controllers/pwa_controller.rb
class PwaController < ApplicationController
def manifest
render json: {
name: 'Application Name',
short_name: 'App',
start_url: root_path,
display: 'standalone',
background_color: '#ffffff',
theme_color: '#000000',
icons: [
{
src: view_context.asset_path('icon-192.png'),
sizes: '192x192',
type: 'image/png'
},
{
src: view_context.asset_path('icon-512.png'),
sizes: '512x512',
type: 'image/png'
}
]
}
end
end
Sinatra applications implement PWAs through route handlers serving service workers and manifests. The lightweight framework requires manual implementation of caching strategies and asset serving.
require 'sinatra'
require 'json'
get '/service-worker.js' do
content_type 'application/javascript'
cache_control :public, max_age: 0
erb :'service-worker.js'
end
get '/manifest.json' do
content_type 'application/json'
{
name: 'Sinatra PWA',
short_name: 'SPWA',
start_url: '/',
display: 'standalone',
icons: [
{
src: '/images/icon-192.png',
sizes: '192x192',
type: 'image/png'
}
]
}.to_json
end
Rails API applications serving PWA frontends implement offline-capable endpoints through conditional request handling and ETag support. The stale? method checks conditional request headers, returning 304 Not Modified responses when cached resources remain valid.
class Api::ArticlesController < ApplicationController
def index
@articles = Article.all
if stale?(etag: @articles, last_modified: @articles.maximum(:updated_at))
render json: @articles
end
end
def show
@article = Article.find(params[:id])
if stale?(@article)
render json: @article
end
end
end
Background job processing integrates with PWA push notifications through web push gems. The webpush gem sends push notifications to subscribed clients using VAPID authentication.
# Gemfile
gem 'webpush'
# app/models/push_subscription.rb
class PushSubscription < ApplicationRecord
def send_notification(payload)
message = {
title: payload[:title],
body: payload[:body],
icon: payload[:icon]
}.to_json
Webpush.payload_send(
message: message,
endpoint: endpoint,
p256dh: p256dh_key,
auth: auth_key,
vapid: {
subject: 'mailto:contact@example.com',
public_key: ENV['VAPID_PUBLIC_KEY'],
private_key: ENV['VAPID_PRIVATE_KEY']
}
)
end
end
Rails applications manage push subscription lifecycle through dedicated models and controllers. The subscription controller handles client registration and unregistration, storing subscription endpoints and encryption keys.
class PushSubscriptionsController < ApplicationController
def create
subscription = current_user.push_subscriptions.create!(
endpoint: params[:endpoint],
p256dh_key: params[:keys][:p256dh],
auth_key: params[:keys][:auth]
)
render json: subscription, status: :created
end
def destroy
subscription = current_user.push_subscriptions.find(params[:id])
subscription.destroy
head :no_content
end
end
Practical Examples
PWA implementations demonstrate offline functionality, installation flows, and background synchronization through concrete scenarios. These examples progress from basic service worker caching to complex offline-capable applications.
Basic Offline Caching: A news reading application caches articles for offline consumption. The service worker intercepts article requests, serving cached versions when offline while attempting network requests first when online.
// service-worker.js
const CACHE_NAME = 'news-reader-v1';
const ARTICLE_CACHE = 'articles';
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// Cache articles with network-first strategy
if (url.pathname.startsWith('/articles/')) {
event.respondWith(
fetch(event.request)
.then(response => {
const responseClone = response.clone();
caches.open(ARTICLE_CACHE).then(cache => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
return caches.match(event.request);
})
);
return;
}
// Default cache-first strategy for static assets
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
The corresponding Rails controller sets appropriate cache headers enabling service worker caching while maintaining content freshness.
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
fresh_when(@article, public: true)
expires_in 1.hour, public: true
end
end
Installation Prompt Management: Applications control installation timing through beforeinstallprompt event handling. This example defers the prompt until users complete specific actions, improving installation conversion rates.
// app.js
let deferredPrompt;
const installButton = document.getElementById('install-button');
window.addEventListener('beforeinstallprompt', event => {
// Prevent automatic prompt
event.preventDefault();
// Store event for later use
deferredPrompt = event;
// Show custom install button
installButton.style.display = 'block';
});
installButton.addEventListener('click', async () => {
if (!deferredPrompt) return;
// Show installation prompt
deferredPrompt.prompt();
// Wait for user response
const { outcome } = await deferredPrompt.userChoice;
console.log(`User response: ${outcome}`);
// Clear the deferred prompt
deferredPrompt = null;
installButton.style.display = 'none';
});
// Detect successful installation
window.addEventListener('appinstalled', () => {
console.log('PWA installed successfully');
deferredPrompt = null;
});
Background Synchronization: A task management application defers task creation when offline, synchronizing with the server when connectivity returns. The background sync API registers sync events during offline periods.
// service-worker.js
self.addEventListener('sync', event => {
if (event.tag === 'sync-tasks') {
event.waitUntil(syncTasks());
}
});
async function syncTasks() {
const db = await openDatabase();
const pendingTasks = await db.getAll('pending_tasks');
for (const task of pendingTasks) {
try {
const response = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(task)
});
if (response.ok) {
await db.delete('pending_tasks', task.id);
}
} catch (error) {
console.error('Sync failed for task:', task.id);
}
}
}
// Client-side code
async function createTask(taskData) {
try {
const response = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(taskData)
});
if (response.ok) {
return await response.json();
}
} catch (error) {
// Store task for background sync
const db = await openDatabase();
await db.add('pending_tasks', taskData);
// Register sync event
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-tasks');
}
}
The Rails API endpoint handles task creation with idempotency keys preventing duplicate submissions during synchronization.
class Api::TasksController < ApplicationController
def create
@task = current_user.tasks.find_or_initialize_by(
idempotency_key: params[:idempotency_key]
)
if @task.new_record?
@task.assign_attributes(task_params)
@task.save!
end
render json: @task, status: :created
end
private
def task_params
params.require(:task).permit(:title, :description, :due_date)
end
end
Push Notification Integration: An e-commerce application sends order status updates through push notifications. The implementation handles subscription management, notification delivery, and user interaction.
// app.js - Subscription management
async function subscribeToPushNotifications() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
});
// Send subscription to server
await fetch('/api/push_subscriptions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
}
// service-worker.js - Notification handling
self.addEventListener('push', event => {
const data = event.data.json();
const options = {
body: data.body,
icon: data.icon,
badge: '/images/badge.png',
data: {
url: data.url
},
actions: [
{ action: 'open', title: 'View Order' },
{ action: 'close', title: 'Dismiss' }
]
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.action === 'open') {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});
Rails background jobs trigger push notifications for relevant events.
class OrderStatusNotificationJob < ApplicationJob
queue_as :default
def perform(order_id)
order = Order.find(order_id)
order.user.push_subscriptions.each do |subscription|
subscription.send_notification(
title: 'Order Update',
body: "Your order ##{order.id} has been #{order.status}",
icon: '/images/notification-icon.png',
url: Rails.application.routes.url_helpers.order_url(order)
)
end
end
end
Performance Considerations
PWA performance directly impacts user engagement and conversion rates. Service worker caching strategies, asset optimization, and runtime performance optimizations determine application responsiveness under varying network conditions.
Service Worker Caching Strategies: Cache strategy selection affects both initial load performance and content freshness. Cache-first strategies prioritize speed by serving cached resources immediately, falling back to network requests on cache misses. Network-first strategies ensure content freshness at the cost of slower responses during network availability. Stale-while-revalidate strategies balance both concerns, serving cached content while fetching updates.
// Implementing multiple caching strategies
const CACHE_STRATEGIES = {
CACHE_FIRST: 'cache-first',
NETWORK_FIRST: 'network-first',
STALE_WHILE_REVALIDATE: 'stale-while-revalidate'
};
function getCacheStrategy(url) {
if (url.includes('/api/')) return CACHE_STRATEGIES.NETWORK_FIRST;
if (url.includes('/static/')) return CACHE_STRATEGIES.CACHE_FIRST;
return CACHE_STRATEGIES.STALE_WHILE_REVALIDATE;
}
self.addEventListener('fetch', event => {
const strategy = getCacheStrategy(event.request.url);
switch (strategy) {
case CACHE_STRATEGIES.CACHE_FIRST:
event.respondWith(cacheFirst(event.request));
break;
case CACHE_STRATEGIES.NETWORK_FIRST:
event.respondWith(networkFirst(event.request));
break;
case CACHE_STRATEGIES.STALE_WHILE_REVALIDATE:
event.respondWith(staleWhileRevalidate(event.request));
break;
}
});
async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(request);
const fetchPromise = fetch(request).then(response => {
cache.put(request, response.clone());
return response;
});
return cachedResponse || fetchPromise;
}
Cache size management prevents storage quota exhaustion. Service workers implement cache eviction policies based on access patterns or timestamps.
const MAX_CACHE_SIZE = 50;
const CACHE_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7 days
async function trimCache(cacheName) {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
if (requests.length > MAX_CACHE_SIZE) {
await cache.delete(requests[0]);
await trimCache(cacheName); // Recursive trim
}
}
async function removeExpiredCache(cacheName) {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
for (const request of requests) {
const response = await cache.match(request);
const dateHeader = response.headers.get('date');
const cachedDate = new Date(dateHeader).getTime();
if (Date.now() - cachedDate > CACHE_EXPIRY) {
await cache.delete(request);
}
}
}
Asset Optimization: Rails asset pipeline optimization reduces initial load times and cache sizes. Precompiling assets, enabling compression, and implementing code splitting minimize bandwidth requirements.
# config/environments/production.rb
Rails.application.configure do
# Enable asset compression
config.assets.compress = true
config.assets.css_compressor = :sass
config.assets.js_compressor = :terser
# Asset fingerprinting for cache busting
config.assets.digest = true
# Precompile additional assets
config.assets.precompile += %w[service-worker.js]
# CDN configuration
config.action_controller.asset_host = ENV['CDN_HOST']
end
Image optimization reduces payload sizes substantially. Rails applications integrate image processing gems for automatic optimization.
# Gemfile
gem 'image_processing'
# app/models/article.rb
class Article < ApplicationRecord
has_one_attached :cover_image
def optimized_cover_image
cover_image.variant(
resize_to_limit: [1200, 630],
format: :webp,
saver: { quality: 85 }
)
end
end
Runtime Performance: Application shell architecture improves perceived performance by caching the application skeleton separately from dynamic content. The shell loads instantly while dynamic content fetches asynchronously.
// Service worker caching application shell
const SHELL_CACHE = 'app-shell-v1';
const SHELL_FILES = [
'/',
'/index.html',
'/styles/shell.css',
'/scripts/shell.js',
'/images/skeleton.svg'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(SHELL_CACHE).then(cache => {
return cache.addAll(SHELL_FILES);
})
);
});
Code splitting defers non-critical JavaScript loading until necessary. Dynamic imports load features on-demand rather than during initial page load.
// Dynamic import for feature modules
async function loadFeature() {
const module = await import('./features/analytics.js');
module.initialize();
}
// Load features after initial render
window.addEventListener('load', () => {
if ('requestIdleCallback' in window) {
requestIdleCallback(loadFeature);
} else {
setTimeout(loadFeature, 1);
}
});
Network Performance: HTTP/2 server push eliminates round-trip latency for critical resources. Rails applications configure server push through Link headers.
class ApplicationController < ActionController::Base
before_action :set_link_headers
private
def set_link_headers
response.headers['Link'] = [
"</assets/application.css>; rel=preload; as=style",
"</assets/application.js>; rel=preload; as=script"
].join(', ')
end
end
Request prioritization ensures critical resources load before less important assets. The Priority Hints API guides browser resource scheduling.
<!-- High priority for above-the-fold images -->
<img src="hero.jpg" fetchpriority="high" alt="Hero">
<!-- Low priority for below-the-fold images -->
<img src="footer.jpg" fetchpriority="low" alt="Footer">
Security Implications
PWA security encompasses service worker integrity, secure contexts, content security policies, and permission management. Service workers execute with significant privileges, requiring careful security consideration.
HTTPS Requirement: Service workers function exclusively over HTTPS, preventing network-level code injection attacks. HTTPS ensures service worker code integrity during transmission and protects sensitive API access. Development environments permit localhost service worker registration without HTTPS for testing purposes.
Rails applications enforce HTTPS through configuration and middleware.
# config/environments/production.rb
Rails.application.configure do
# Force HTTPS for all requests
config.force_ssl = true
# HSTS header configuration
config.ssl_options = {
hsts: {
expires: 1.year,
subdomains: true,
preload: true
}
}
end
Content Security Policy: CSP headers mitigate XSS attacks by restricting resource loading sources. PWAs require CSP configuration permitting service worker registration and web app manifest loading.
class ApplicationController < ActionController::Base
before_action :set_csp_header
private
def set_csp_header
response.headers['Content-Security-Policy'] = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"connect-src 'self'",
"manifest-src 'self'",
"worker-src 'self'"
].join('; ')
end
end
Service workers themselves require careful CSP configuration. Inline event handlers and eval() present security risks within service worker code.
// Avoid inline event handlers in service workers
// BAD: Using eval or Function constructor
const code = "console.log('unsafe')";
eval(code); // Security risk
// GOOD: Direct function calls
function safeOperation() {
console.log('safe');
}
safeOperation();
Cross-Origin Resource Sharing: Service workers intercepting cross-origin requests must respect CORS policies. Opaque responses from cross-origin requests without CORS headers fail storage in cache API.
class Api::BaseController < ApplicationController
before_action :set_cors_headers
private
def set_cors_headers
response.headers['Access-Control-Allow-Origin'] = request.headers['Origin'] || '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
response.headers['Access-Control-Max-Age'] = '3600'
end
end
Permission Management: PWAs request permissions for sensitive features like notifications, geolocation, and camera access. Applications must handle permission states explicitly rather than assuming grants.
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
switch (permission) {
case 'granted':
console.log('Notification permission granted');
await subscribeToPush();
break;
case 'denied':
console.log('Notification permission denied');
// Disable notification features
break;
case 'default':
console.log('Notification permission dismissed');
// Show permission request later
break;
}
}
Service Worker Updates: Service workers update automatically when their code changes, but active service workers continue running until pages close. Malicious service workers potentially persist indefinitely without forced updates.
// Implementing forced service worker updates
navigator.serviceWorker.register('/service-worker.js').then(registration => {
// Check for updates every hour
setInterval(() => {
registration.update();
}, 60 * 60 * 1000);
// Update on visibility change
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
registration.update();
}
});
});
// Service worker self-update
self.addEventListener('message', event => {
if (event.data === 'skipWaiting') {
self.skipWaiting();
}
});
Data Storage Security: IndexedDB and Cache API storage persists across sessions without encryption. Applications storing sensitive data must implement client-side encryption.
async function storeSecureData(key, data) {
const encryptionKey = await getEncryptionKey();
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(JSON.stringify(data));
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedData = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
encryptionKey,
dataBuffer
);
const db = await openDatabase();
await db.put('secure_store', {
key,
iv: Array.from(iv),
data: Array.from(new Uint8Array(encryptedData))
});
}
Tools & Ecosystem
PWA development tools span service worker testing, manifest validation, performance analysis, and framework integration. Rails developers access PWA capabilities through gems, JavaScript libraries, and browser development tools.
Serviceworker-Rails: This gem integrates service worker support into Rails applications. It provides routes configuration, ERB template processing, and asset pipeline integration for service worker files.
# Gemfile
gem 'serviceworker-rails'
# config/initializers/serviceworker.rb
Rails.application.configure do
config.serviceworker.routes.draw do
match '/service-worker.js'
match '/manifest.json'
end
end
# app/assets/javascripts/service-worker.js.erb
var CACHE_VERSION = 'v<%= Time.now.to_i %>';
var CACHE_URLS = [
'<%= asset_path "application.css" %>',
'<%= asset_path "application.js" %>'
];
Webpush: The webpush gem implements Web Push protocol for sending notifications to PWA clients. It handles VAPID authentication, message encryption, and subscription management.
# Gemfile
gem 'webpush'
# Generating VAPID keys
vapid_key = Webpush.generate_key
# Sending push notifications
Webpush.payload_send(
message: JSON.generate({
title: 'Notification Title',
body: 'Notification body text'
}),
endpoint: subscription.endpoint,
p256dh: subscription.p256dh_key,
auth: subscription.auth_key,
vapid: {
subject: 'mailto:contact@example.com',
public_key: vapid_key.public_key,
private_key: vapid_key.private_key
}
)
Workbox: Google's Workbox library simplifies service worker development through pre-built caching strategies, routing, and background sync implementations.
// Using Workbox in service worker
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js');
// Configure precaching
workbox.precaching.precacheAndRoute([
{ url: '/index.html', revision: 'v1' },
{ url: '/styles/app.css', revision: 'v1' }
]);
// Routing with strategies
workbox.routing.registerRoute(
/\.(?:png|jpg|jpeg|svg)$/,
new workbox.strategies.CacheFirst({
cacheName: 'images',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 30 * 24 * 60 * 60
})
]
})
);
workbox.routing.registerRoute(
/\/api\//,
new workbox.strategies.NetworkFirst({
cacheName: 'api',
networkTimeoutSeconds: 3
})
);
Lighthouse: Chrome's Lighthouse tool audits PWA implementation, scoring applications across performance, accessibility, best practices, and SEO categories. Rails applications integrate Lighthouse audits into CI/CD pipelines.
# Running Lighthouse in CI
task :lighthouse do
require 'json'
output = `lighthouse https://example.com --output=json`
results = JSON.parse(output)
pwa_score = results['categories']['pwa']['score']
if pwa_score < 0.9
puts "PWA score below threshold: #{pwa_score}"
exit 1
end
end
PWA Builder: Microsoft's PWA Builder analyzes web applications and generates platform-specific packages for app store distribution. It produces manifests, service workers, and packaging files for Windows, Android, and iOS.
Chrome DevTools: Browser development tools provide service worker debugging, cache inspection, manifest validation, and background sync monitoring. The Application panel displays service worker status, cached resources, and storage quotas.
Service worker debugging requires understanding lifecycle events and cache states. DevTools enables manual service worker updates, unregistration, and skip waiting actions during development.
// Development-only debugging helpers
if (process.env.NODE_ENV === 'development') {
self.addEventListener('message', event => {
if (event.data === 'clearCache') {
caches.keys().then(names => {
names.forEach(name => caches.delete(name));
});
}
});
}
PWA Asset Generator: This command-line tool generates PWA icons and splash screens from source images. It produces multiple resolutions required for various devices and platforms.
npx pwa-asset-generator logo.svg ./public/icons \
--background "#ffffff" \
--manifest ./public/manifest.json
Reference
Web App Manifest Properties
| Property | Type | Description |
|---|---|---|
| name | string | Full application name displayed during installation |
| short_name | string | Short name for home screen and launcher |
| description | string | Application description for app stores |
| start_url | string | URL loaded when launching from home screen |
| display | string | Display mode: fullscreen, standalone, minimal-ui, browser |
| orientation | string | Preferred orientation: any, portrait, landscape |
| theme_color | string | Browser theme color in hex format |
| background_color | string | Splash screen background color |
| icons | array | Icon objects with src, sizes, type, purpose properties |
| scope | string | Navigation scope limiting PWA context |
| categories | array | Application categories for app stores |
Service Worker Lifecycle Events
| Event | Timing | Purpose |
|---|---|---|
| install | First registration or updated file | Pre-cache critical resources |
| activate | After install, when becoming active | Clean up old caches, claim clients |
| fetch | Every network request within scope | Intercept and handle requests |
| sync | When connectivity restored | Process deferred background tasks |
| push | When push notification received | Display notifications to user |
| notificationclick | User clicks notification | Handle notification interactions |
| message | When client sends message | Communicate with active service worker |
Caching Strategies
| Strategy | Behavior | Use Case |
|---|---|---|
| Cache First | Check cache before network | Static assets, infrequently changing content |
| Network First | Attempt network before cache fallback | API data, frequently updated content |
| Cache Only | Serve exclusively from cache | Pre-cached content, offline-only resources |
| Network Only | Always use network, never cache | Real-time data, uncacheable resources |
| Stale While Revalidate | Serve cache while fetching update | Balance freshness and speed |
Rails Service Worker Integration
| Gem | Purpose | Key Features |
|---|---|---|
| serviceworker-rails | Service worker integration | Routes, ERB templates, asset pipeline support |
| webpush | Push notifications | VAPID authentication, message encryption |
| pwa_rails | PWA configuration | Manifest generation, icon processing |
Cache API Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
| caches.open | name | Promise Cache | Opens or creates named cache |
| cache.add | request | Promise void | Fetches and caches single request |
| cache.addAll | requests | Promise void | Fetches and caches multiple requests |
| cache.put | request, response | Promise void | Stores request-response pair |
| cache.match | request | Promise Response or undefined | Retrieves cached response |
| cache.matchAll | request, options | Promise Array Response | Retrieves all matching responses |
| cache.delete | request | Promise boolean | Removes cached response |
| cache.keys | request, options | Promise Array Request | Lists cached requests |
| caches.delete | name | Promise boolean | Deletes entire cache |
| caches.keys | none | Promise Array string | Lists all cache names |
Background Sync API
| Method | Purpose | Requirements |
|---|---|---|
| registration.sync.register | Register sync event | Service worker registration |
| sync event handler | Process deferred operations | Service worker context |
| registration.sync.getTags | List pending sync tags | Service worker registration |
Push API Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
| pushManager.subscribe | options | Promise PushSubscription | Creates push subscription |
| pushManager.getSubscription | none | Promise PushSubscription or null | Retrieves active subscription |
| subscription.unsubscribe | none | Promise boolean | Cancels push subscription |
| pushManager.permissionState | options | Promise string | Checks permission state |
Installation Best Practices
| Recommendation | Implementation | Benefit |
|---|---|---|
| Defer installation prompt | Store beforeinstallprompt event | Improved conversion rates |
| Show custom UI | Display contextual install button | Better user experience |
| Detect installation | Listen to appinstalled event | Track installation success |
| Handle dismissal | Clear deferred prompt reference | Prevent repeated prompts |
| Test add-to-home-screen | Verify manifest and HTTPS | Ensure installability |
Performance Metrics
| Metric | Target | Measurement |
|---|---|---|
| First Contentful Paint | Under 1.8s | Time to first content render |
| Largest Contentful Paint | Under 2.5s | Time to main content render |
| Time to Interactive | Under 3.8s | Time until fully interactive |
| Total Blocking Time | Under 200ms | Sum of blocking task time |
| Cumulative Layout Shift | Under 0.1 | Visual stability score |
Security Headers
| Header | Value | Purpose |
|---|---|---|
| Content-Security-Policy | worker-src 'self' | Restrict service worker sources |
| Strict-Transport-Security | max-age=31536000 | Enforce HTTPS |
| X-Content-Type-Options | nosniff | Prevent MIME sniffing |
| X-Frame-Options | DENY | Prevent clickjacking |
| Referrer-Policy | strict-origin-when-cross-origin | Control referrer information |