Skip to main content
Progressive Web App Essentials

Why Your PWA Still Feels Fragile Offline: Common Missteps and Kryton’s Fixes

Who Needs to Decide, and Why Now? If your PWA shows a blank page or spinning loader when the network drops, you have an offline problem—and you are not alone. Teams often invest heavily in making an app installable and fast, only to discover that offline behavior feels like an afterthought. The core issue is not whether the app works offline at all, but whether it works reliably offline: users expect cached content to render instantly, forms to queue intelligently, and navigation to feel natural without a connection. This guide is for developers, technical leads, and product owners who have a working PWA but notice gaps in offline resilience. Maybe the app loads a shell but fails to show actual data. Or the cache grows without bound until the browser evicts critical resources. Or background sync attempts fail silently.

Who Needs to Decide, and Why Now?

If your PWA shows a blank page or spinning loader when the network drops, you have an offline problem—and you are not alone. Teams often invest heavily in making an app installable and fast, only to discover that offline behavior feels like an afterthought. The core issue is not whether the app works offline at all, but whether it works reliably offline: users expect cached content to render instantly, forms to queue intelligently, and navigation to feel natural without a connection.

This guide is for developers, technical leads, and product owners who have a working PWA but notice gaps in offline resilience. Maybe the app loads a shell but fails to show actual data. Or the cache grows without bound until the browser evicts critical resources. Or background sync attempts fail silently. These are not exotic edge cases—they are common outcomes of specific design shortcuts. We will walk through eight decision points that determine whether your offline experience feels robust or fragile, and we will show how to fix each one using patterns that have proven effective across many production apps.

The urgency is real: mobile users in areas with patchy coverage, commuters in tunnels, and travelers abroad all judge an app by its offline behavior. If your PWA cannot serve them reliably, they will switch to a native app that does. By addressing these missteps now, you can turn a weak point into a competitive advantage.

Who this is for

This article is written for teams that already have a service worker and a manifest file but are not satisfied with offline performance. We assume familiarity with basic PWA concepts—caching, fetch events, and the app shell model—but we explain the underlying trade-offs so that even intermediate developers can follow and apply the fixes.

The Offline Landscape: Three Common Approaches and Their Pitfalls

Developers typically start with one of three caching strategies, each with a characteristic failure mode. Understanding these patterns is the first step toward choosing the right one for your content type.

Cache-first with network fallback

This is the most popular pattern for static assets: serve from cache immediately, and update the cache from the network in the background. The pitfall? If the initial cache population is incomplete or if a resource expires without a fresh fetch, users see stale or missing content. Many teams forget to implement a cache versioning scheme, so after a deployment, old scripts and new API responses clash, causing render errors.

Network-first with cache fallback

Common for dynamic content like news feeds or user dashboards. The service worker tries the network first; if it fails, it serves a cached copy. The pitfall here is latency: on a slow or intermittent connection, the network request hangs for seconds before falling back, making the app feel unresponsive. Developers often set a generous timeout (or none at all), defeating the purpose of offline support.

Stale-while-revalidate

A hybrid that serves cached content immediately while updating the cache from the network in the background. This works well for content that changes frequently but can tolerate slight staleness. The pitfall is that if the cache is empty—for example, on first visit—the user gets nothing until the network responds. This pattern also requires careful cache cleanup to avoid unbounded growth.

None of these approaches is inherently wrong; each suits a different content profile. The mistake is applying one strategy uniformly across all resources. A robust offline experience uses a mix: cache-first for app shell and static assets, stale-while-revalidate for UI templates, and network-first with a short timeout for API data, with a fallback to a local database (like IndexedDB) for the most recent snapshot.

Criteria for Choosing the Right Caching Mix

To decide which strategy to apply where, evaluate each resource type along three axes: update frequency, criticality for offline use, and size.

Update frequency

Static assets (CSS, JS, fonts) change only on deployment. They are ideal for cache-first with versioned cache names. API responses, on the other hand, may change every few seconds. For those, a network-first approach with a short timeout (e.g., 3 seconds) and a fallback to the last successful response is more appropriate.

Criticality for offline use

Ask: does the user need this resource to perform a core task without a connection? The app shell (HTML, CSS, JS) is critical—without it, nothing works. User profile data might be less critical if the app can show a generic placeholder. Prioritize caching for critical resources, and consider showing a graceful message for non-critical ones.

Size and storage constraints

The Cache API has limits (typically 50–100 MB per origin, depending on the browser). Large assets like images or videos can quickly fill this quota. For media, consider a separate cache with a maximum item count, and use IndexedDB for metadata so you can reconstruct the display even if the media is evicted. Avoid caching everything; be selective based on actual usage patterns.

A practical decision matrix: for each resource, assign a score (1–3) on each axis, then pick the strategy with the best fit. For example, a high-update, high-criticality, small resource (like a live stock ticker) might use network-first with a 2-second timeout and a cached fallback from the last successful poll. A low-update, low-criticality, large resource (like a background image) might use cache-first with a max-age of 7 days and no fallback.

Trade-Offs in Offline Data Synchronization

Offline capability is not just about serving cached pages; it is also about allowing users to perform actions (like submitting a form or saving a preference) while disconnected, and syncing those actions when connectivity returns. This is where many PWAs feel fragile: the UI lets users type and tap, but the data never arrives at the server, or it arrives out of order.

The standard tool for this is the Background Sync API, which defers a network request until the user has a stable connection. However, the API has limitations: it does not guarantee delivery order, and it can be delayed by the browser (sometimes indefinitely on mobile). A common misstep is to rely solely on Background Sync without a fallback, leaving users in the dark about pending changes.

A more robust pattern combines Background Sync with a local queue in IndexedDB. When the user submits data offline, the service worker stores the request in a queue, shows a confirmation message (e.g., “Saved offline—will sync when connected”), and registers a sync event. On the next sync, the worker processes the queue in order, and if any request fails, it retries with exponential backoff. The user can see the queue status and manually trigger a sync if needed.

Another trade-off: conflict resolution. If the same record is modified both offline and online, which version wins? For simple apps, “last write wins” may suffice, but for collaborative or transactional data, you need a more sophisticated strategy—like operational transforms or a server-side merge. This is a deep topic; the key point is to design your sync logic with conflict handling from the start, not as an afterthought.

Table: Sync strategies compared

StrategyProsConsBest for
Background Sync onlySimple to implement, low overheadNo user visibility, potential delays, no ordering guaranteeNon-critical, low-frequency updates (e.g., analytics pings)
IndexedDB queue + Background SyncUser feedback, ordering control, retry logicMore code, storage overhead for queueForm submissions, preference saves, any user-generated data
Service worker fetch interception with replayTransparent to the app, works with existing fetch callsComplex to implement correctly, may interfere with other caching logicLegacy apps where you cannot modify the client code

Implementation Path: From Fragile to Resilient in Five Steps

Moving from a fragile offline experience to a robust one does not require a rewrite. Follow these steps in order, testing at each stage.

Step 1: Audit your current caching strategy

Open DevTools, go to the Application panel, and inspect the Cache Storage. List every cache and its contents. For each resource, note whether it is still needed, whether it is versioned, and whether it has a max-age or max-entries policy. Delete orphaned caches from old service worker versions—they waste space and can cause stale content.

Step 2: Implement cache versioning and cleanup

In your service worker’s install event, use a version string (e.g., v2) in the cache name. In the activate event, iterate over all cache names and delete any that do not match the current version. This ensures a clean slate after each deployment. Example: caches.keys().then(keys => Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))).

Step 3: Add a fallback for dynamic content

For API responses, modify your fetch handler to store successful responses in IndexedDB (or a separate cache) with a timestamp. When the network fails, serve the most recent stored response. This is more reliable than relying on the browser’s HTTP cache, which may have been cleared. Use a library like idb-keyval for simplicity.

Step 4: Implement a sync queue with user feedback

Create an IndexedDB store for pending requests. On form submission, if navigator.onLine is false, serialize the request data and add it to the queue. Show a toast or banner: “Saved offline. Will sync when connected.” Register a sync event with a tag. In the sync handler, process the queue sequentially. If a request fails, keep it in the queue and retry later.

Step 5: Test with real-world network conditions

Use Chrome DevTools’ Network panel to simulate offline, slow 3G, and intermittent connectivity (e.g., using the “Offline” checkbox and custom throttling). Also test on a physical device by turning airplane mode on and off. Verify that cached pages load instantly, that forms queue correctly, and that the app handles the transition from offline to online without errors.

Risks of Skipping Steps or Choosing Wrong

If you skip the audit, you risk accumulating stale caches that consume quota and serve outdated content. If you choose a cache-first strategy for API data without a revalidation mechanism, users will see stale information and may make decisions based on it. If you ignore conflict resolution in your sync queue, data loss or corruption can occur.

Another risk: over-caching. Caching every image, every API response, and every page can quickly exceed the browser’s storage limit, causing the browser to evict the oldest cached resources—which may be the app shell itself. This leads to a situation where the app fails to load offline because the critical shell was evicted to make room for a hundred product photos. The fix is to set explicit limits: use a separate cache for large assets with a max-entries policy (e.g., 50 items), and always cache the app shell with a high priority.

Performance risk: a poorly designed service worker can slow down the app even when online. For example, if your fetch handler waits for a cache lookup before responding to every request, you add latency. The solution is to use the event.respondWith() only for requests you intend to cache, and let all others pass through normally. Also, avoid heavy computation inside the service worker—keep it lean.

Finally, there is the risk of user trust erosion. If a PWA claims to work offline but fails in subtle ways (like showing a loading spinner indefinitely, or silently dropping a form submission), users will lose confidence and may uninstall the app. Offline reliability is a trust signal; getting it right pays dividends in user retention.

Frequently Asked Questions About Offline PWA Fragility

Why does my PWA show a white screen offline?

This usually means the app shell (HTML, CSS, JS) is not cached, or the cache was evicted. Check that your service worker caches the shell during the install event, and that you have a fallback in the fetch event to serve it from cache. Also verify that your cache versioning is correct—if you updated the service worker but the shell URL changed, the old cache might be deleted.

How do I handle forms offline without losing data?

Use the approach described above: intercept the form submission in the client, check navigator.onLine, and if offline, store the serialized data in IndexedDB. Show a confirmation to the user. Register a Background Sync event. In the sync handler, read the queue and POST each entry. For extra safety, keep the data in IndexedDB until the server confirms receipt.

What is the best cache size limit?

There is no single answer, but a common guideline is to reserve 50% of the available storage for the Cache API (which is typically 50–100 MB). Use navigator.storage.estimate() to check the current usage and quota. For large apps, consider using IndexedDB for data and the Cache API only for static assets, since IndexedDB can store larger amounts (up to the full quota) and offers more granular control.

Should I use Workbox or write my own service worker?

Workbox is a mature library that handles many edge cases (cache versioning, cleanup, routing). It is a good choice for most teams, as it reduces boilerplate and common bugs. However, if you need custom sync logic or complex fallback behavior, you may need to write your own handler. Workbox can be extended with custom plugins, so start with it and only drop down to raw service worker code when necessary.

How do I test offline behavior on iOS Safari?

Safari’s PWA support is more limited than Chrome’s. Use the Web Inspector on a connected device, or use the “Develop” menu in Safari on macOS to simulate offline. Note that Background Sync is not supported on iOS, so you will need a different approach for offline data submission—such as storing data locally and syncing when the app is opened again (using visibilitychange or periodic network checks).

Recommendation Recap: Your Path to a Robust Offline PWA

Building a PWA that feels solid offline is not about a single magic setting; it is about making deliberate choices at each layer of the stack. Start by auditing your current caching strategy and clean up old caches. Choose a mix of caching patterns based on update frequency, criticality, and size—not a one-size-fits-all approach. Implement a sync queue with user feedback for offline actions, and test under real-world network conditions.

For most teams, the quickest win is to adopt a caching library like Workbox to handle the common pitfalls automatically, and then layer on custom logic for data synchronization. Avoid over-caching by setting limits and prioritizing the app shell. Finally, treat offline reliability as a feature that requires ongoing maintenance—test after every deployment and monitor for cache eviction or sync failures.

Your users will notice the difference. A PWA that loads instantly and lets them work through a tunnel or a flight builds trust and loyalty. The fixes outlined here are not theoretical; they are proven in production apps that serve millions. Apply them, and your PWA will no longer feel fragile—it will feel like a native app that respects the user’s time and context.

Share this article:

Comments (0)

No comments yet. Be the first to comment!