PWA & Offline Capabilities
This document describes the Progressive Web App (PWA) implementation and offline capabilities of the JIIT Time Table website. It covers the service worker architecture, caching strategies, precaching manifest generation, and how the application achieves full offline functionality.
For information about the Next.js application structure and routing, see Frontend Architecture & Routing. For details on how Python modules are loaded and cached for offline use, see Pyodide WASM Integration.
Purpose and Scope
The JIIT Time Table website is designed as a fully-functional offline-first Progressive Web App. Once installed, users can:
- Generate personalized timetables without internet connectivity
- Access all previously loaded timetable data
- Execute Python timetable processing logic via cached Pyodide WebAssembly runtime
- View the timeline/calendar interface with locally stored schedules
This is achieved through a sophisticated service worker implementation that caches static assets, timetable JSON data, Python modules, and the multi-megabyte Pyodide runtime.
PWA Architecture Overview
The PWA implementation uses @ducanh2912/next-pwa, a Next.js plugin that wraps Workbox strategies to generate an optimized service worker at build time.

Service Worker Configuration
The service worker is configured in next.config.ts using the withPWA wrapper function. The configuration is disabled during development and only active in production builds.
Configuration Options
| Option | Value | Purpose |
|---|---|---|
dest |
"public" |
Output directory for generated SW files |
register |
true |
Auto-register service worker on page load |
skipWaiting |
true |
Activate new SW immediately without waiting |
disable |
process.env.NODE_ENV === "development" |
Disable during local development |
Workbox Runtime Caching Configuration

The Pyodide CDN cache rule at website/next.config.ts21-36 implements an aggressive CacheFirst strategy:
runtimeCaching: [
{
urlPattern: /^https:\/\/cdn\.jsdelivr\.net\/pyodide\/v0\.27\.0\/full\/.*/,
handler: "CacheFirst",
options: {
cacheName: "pyodide-cache",
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
]
Precache Manifest
At build time, the PWA plugin generates a precache manifest containing all static assets that should be available offline. The generated service worker includes this manifest inline.
Precached Asset Categories

Sample Precache Entries
The precache list in website/public/sw.js1-2 includes entries with revision hashes for cache invalidation:
| Resource Type | Example Path | Revision Hash |
|---|---|---|
| Python module | /_creator.py |
51e7cbe1d5819fbb969373faecb24a03 |
| Python module | /modules/BE128_creator.py |
179b16b198ee6f34988320b9ec3f0942 |
| Next.js chunk | /_next/static/chunks/125.9d1280b435bd664c.js |
9d1280b435bd664c |
| CSS | /_next/static/css/688463ca9d49603e.css |
688463ca9d49603e |
| Font | /_next/static/media/747892c23ea88013-s.woff2 |
a0761690ccf4441ace5cec893b82d4ab |
| PWA asset | /icon.png |
51cb25401a45c2e1b666fd401d2a6f51 |
| Manifest | /manifest.json |
5af93678251ec28139ff157755eb78a2 |
Caching Strategies
The service worker implements three distinct caching strategies optimized for different resource types.

Strategy Breakdown
1. NetworkFirst Strategy
Applied to: Root path (/)
This strategy prioritizes fresh content while maintaining offline fallback capability. Defined in website/public/sw.js1-2:
e.registerRoute("/",new e.NetworkFirst({
cacheName:"start-url",
plugins:[{
cacheWillUpdate:async({response:e})=>
e&&"opaqueredirect"===e.type?
new Response(e.body,{status:200,statusText:"OK",headers:e.headers}):
e
}]
}),"GET")
Behavior:
- Always attempt network request first
- On success: cache response and return it
- On failure: serve from
start-urlcache if available - Handles opaque redirects by converting to transparent responses
2. CacheFirst Strategy
Applied to: Pyodide CDN resources (https://cdn.jsdelivr.net/pyodide/v0.27.0/full/*)
This aggressive caching strategy is essential for offline Python execution. Configuration at website/next.config.ts23-35:
Behavior:
- Check
pyodide-cachecache first - On cache hit: return immediately (no network request)
- On cache miss: fetch from CDN and cache for 1 year
- Maximum 10 entries (sufficient for Pyodide runtime files)
- Caches both CORS (
status: 200) and opaque (status: 0) responses
3. Precache Matching
Applied to: All precached assets (Next.js chunks, Python files, fonts, etc.)
Behavior:
- Assets are cached during service worker installation
- Requests are intercepted and matched against precache manifest
- Returns cached asset immediately (fastest strategy)
- No network request is made
- Cache is invalidated when revision hash changes
Service Worker Lifecycle
The service worker follows a standard lifecycle with specific handling for updates and cache management.
Service Worker Registration
The service worker is auto-registered by the PWA plugin. The register: true option at website/next.config.ts17 ensures automatic registration on page load.
Skip Waiting Behavior
The skipWaiting: true option at website/next.config.ts18 causes new service workers to activate immediately without waiting for all tabs to close. This is reflected in the generated code:
self.skipWaiting()
e.clientsClaim()
This ensures users get updates as soon as they refresh the page.
Cache Cleanup
The service worker includes automatic cleanup of outdated caches using cleanupOutdatedCaches() from website/public/sw.js1-2 This removes caches from previous versions that no longer match the current precache manifest.
Offline Functionality Matrix
The following table shows which features are available offline after the initial load:
| Feature | Offline Capable | Requirements |
|---|---|---|
| Schedule generation | ✅ Yes | Timetable JSON must be previously accessed |
| Python timetable processing | ✅ Yes | Pyodide runtime cached from CDN |
| Timeline view | ✅ Yes | Schedule stored in localStorage (see State Management) |
| Academic calendar display | ✅ Yes | Calendar JSON must be previously loaded |
| Edit events | ✅ Yes | All editing happens client-side |
| Custom events | ✅ Yes | Stored in localStorage |
| Google Calendar sync | ❌ No | Requires OAuth and network connection |
| PDF export | ✅ Yes | Uses client-side rendering |
| PNG export | ✅ Yes | Uses client-side canvas rendering |
| Compare timetables | ✅ Yes | If both timetable JSONs are cached |
| Mess menu | ❌ Partial | Requires network for fresh data |
Offline Data Requirements
Note: Timetable and calendar JSON files are NOT precached because they are dynamic and version-specific (e.g., ODD25 vs EVEN25, 2425 vs 2526). They are cached on first access via the browser's HTTP cache or explicit runtime caching.
PWA Manifest Configuration
The PWA manifest defines how the application appears when installed on a device. It is referenced in the application layout at website/app/layout.tsx44
Manifest Metadata
The manifest file at /public/manifest.json contains:
| Field | Value/Purpose |
|---|---|
name |
"JIIT Time Table Simplified" |
short_name |
Abbreviated name for home screen |
description |
Application description from website/app/layout.tsx26-27 |
start_url |
"/" (root path) |
display |
"standalone" (full-screen app mode) |
background_color |
Theme color |
theme_color |
"#F0BB78" (specified at website/app/layout.tsx21) |
icons |
512x512 PNG icon |
Icon Configuration
Multiple icon declarations ensure proper display across platforms:
icons: {
icon: [
{ url: "/icon.png", sizes: "512x512", type: "image/png" },
{
url: "/icon.png",
sizes: "512x512",
type: "image/png",
media: "(display-mode: standalone)",
},
],
apple: "/icon.png",
}
iOS-Specific Configuration
Additional metadata for iOS devices at website/app/layout.tsx57-62:
appleWebApp: {
capable: true,
statusBarStyle: "black-translucent",
title: "JIIT Timetable",
startupImage: ["/icon.png"],
}
Security Headers for WASM
The application requires specific security headers to enable SharedArrayBuffer support for Pyodide WebAssembly execution:
other: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
}
These headers are defined at website/app/layout.tsx63-66 and are critical for:
- Enabling
SharedArrayBufferAPI required by Pyodide - Isolating the document in its own browsing context
- Preventing cross-origin resource access vulnerabilities
Note: These headers must be set at the server level during deployment to take effect.
Cache Storage Structure
When the service worker is active, the following caches are created in the browser:
Cache Inspection
Developers can inspect these caches in browser DevTools:
- Open DevTools → Application tab
- Navigate to Cache Storage
- Expand to see
workbox-precache-v2,start-url, andpyodide-cache - Click each cache to inspect stored entries
Cache Size Considerations
| Cache | Approximate Size | Notes |
|---|---|---|
workbox-precache-v2 |
2-4 MB | Varies with Next.js bundle size |
pyodide-cache |
8-10 MB | Largest cache, contains WASM runtime |
start-url |
< 100 KB | Single HTML document |
| Total | ~10-15 MB | Acceptable for PWA standards |
Development vs Production Behavior
The PWA functionality is environment-aware:
Development Mode (npm run dev)
When process.env.NODE_ENV === "development":
disable: process.env.NODE_ENV === "development"
Behavior:
- Service worker is NOT registered
- No caching occurs
- Hot module replacement works normally
- Changes reflect immediately
- Easier debugging without cache interference
Production Mode (npm run build + deployment)
When built for production:
Behavior:
- Service worker is generated at build time
- All precache assets are included with revision hashes
- Service worker registers automatically on first page load
- Caching strategies activate immediately
- Application becomes fully offline-capable after first visit
Testing PWA Locally
To test PWA functionality locally:
- Build the production version:
npm run build - Serve the build output (use a static server)
- Open in browser and install PWA
- Disconnect network to test offline mode
Build Configuration Details
The complete Next.js configuration showing PWA integration:
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: "/ph/:path*",
destination: "https://eu.posthog.com/:path*",
},
];
},
turbopack: {},
};
const withPWA = require("@ducanh2912/next-pwa").default({
dest: "public",
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === "development",
workboxOptions: {
runtimeCaching: [
{
urlPattern: /^https:\/\/cdn\.jsdelivr\.net\/pyodide\/v0\.27\.0\/full\/.*/,
handler: "CacheFirst",
options: {
cacheName: "pyodide-cache",
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
],
},
});
export default withPWA(nextConfig);
The withPWA wrapper:
- Takes the base
nextConfigobject - Adds service worker generation to the build process
- Injects service worker registration code into the HTML
- Generates precache manifest from build output
- Creates workbox configuration files
Troubleshooting Offline Issues
Common issues and solutions:
| Issue | Cause | Solution |
|---|---|---|
| Service worker not registering | Development mode active | Build for production with npm run build |
| Assets not cached | First visit hasn't completed | Wait for all assets to load on first visit |
| Python execution fails offline | Pyodide runtime not cached | Visit the schedule creator page while online first |
| Old version persists | Service worker not updating | Hard refresh (Ctrl+Shift+R) or unregister SW in DevTools |
| Timetable data not available offline | JSON not previously accessed | Access timetable for desired semester while online |
| Large cache size warning | Normal for Pyodide | 10-15 MB is expected and acceptable |
Manual Service Worker Unregistration
To force a clean state during debugging:
// Run in browser console
navigator.serviceWorker.getRegistrations().then(function(registrations) {
for(let registration of registrations) {
registration.unregister();
}
});
Summary
The PWA implementation provides:
- Full offline functionality after initial load
- Intelligent caching with three strategies optimized for different resource types
- Automatic updates via service worker lifecycle with skipWaiting
- Minimal cache footprint (~10-15 MB total)
- One-year Pyodide cache to avoid re-downloading 8-10 MB WASM runtime
- Installability with proper manifest and icons
- Development-friendly with automatic disable during local development
The service worker ensures that all critical resources (Python modules, application code, Pyodide runtime) are available offline, enabling the unique browser-based Python execution model to work without network connectivity.