Skip to main content

Command Palette

Search for a command to run...

Offline Support in Web Apps: Loading the App Without a Network

Updated
6 min read
Offline Support in Web Apps: Loading the App Without a Network
T

I help product teams build quality software and lead engineering efforts. Currently working at OpenSpace as a Senior Software Engineer.

Offline support in web applications often gets discussed as an all-or-nothing feature. Either the app is “offline-capable” or it isn’t. In practice, the app must check quite a few boxes to be able to function without access to a network. Before thinking about syncing, conflict resolution, or background retries, there’s a simpler question to answer: can your application even load when the network is gone?

This post is the second article in a series about offline support in web apps. Here, we're defining what's needed to make the application available when the network is down, in its most basic form. This is not about full offline functionality just yet. It’s a clear baseline we can build on later.

The Offline Availability

For this article, “offline” availability is intentionally scoped to a very specific and narrow case:

  1. The user opens the app while online.

  2. The device loses network connectivity.

  3. The user reloads the page.

Without any support, the browser would show its default offline error screen. Instead, we want the application to load successfully. That’s it. If your app can handle this scenario, you’ve cleared the first real offline hurdle.

We’re not attempting to solve data persistence, handling mutations, synchronisation, conflict resolution or retries just yet. We’ll get to them in later posts.

The Default Offline Failure

Most websites or web applications fail offline in exactly the same way.

  1. The user opens the app while online.

  2. The app loads successfully.

  3. The network disappears.

  4. The user refreshes the page.

  5. The browser tries to re-fetch index.html.

  6. The request fails.

  7. The browser shows a generic offline error page.

Notice how nothing “breaks” in your code — the browser simply refuses to load the app at all.

In practice, this is often the first thing users notice — a refresh on a train, plane, or unstable connection that instantly breaks the app. Importantly, this happens even if the app was previously loaded, JavaScript bundles are cached by the HTTP cache and the app is a SPA with client-side routing.

The key insight here is this: without explicit offline support, the browser treats your app as unavailable, regardless of how much code it already has locally.

App Shell Availability

The smallest meaningful offline capability is simple — the application shell must load without a network connection. If you achieve that, your app moves from “completely broken offline” to “offline-aware”.

For the purposes of this post, the app shell is the minimal set of HTML, CSS, and JavaScript required for the core user interface of a web application such as navigation, layout, and basic styling. It does not include anything that’s required to handle dynamic content.

If these assets load, the user will at least be able to see something — even if it’s just a clear “You’re offline” state. Showing any intentional UI is the first step towards offline-capable web app.

Introducing: PWA

Browsers do not guarantee that index.html will be available offline via normal HTTP caching. If you want reliable offline loading of the app shell, you need explicit control over how the browser serves your application’s core files when the network is unavailable.

This is where Progressive Web Applications (PWAs) come into play. PWAs are web apps that opt into a set of browser features designed to improve reliability, and the most important one for offline access is the service worker. A service worker allows your app to intercept network requests and serve pre-cached assets instead, making it possible to apply a cache-first strategy for the app shell and reliably load the application even when the network is gone.

Here’s the minimal required PWA feature set to make this happen.

  • Service worker registration.

  • Pre-caching of app shell assets.

  • Cache-first fetch handling for those assets.

There are other PWA features, such as the Web App Manifest (which defines icons and allows the application to be installed) or push notifications. While these features enhance the offline experience, you can still have offline app-shell loading without install prompts or home screen icons.

Using vite-plugin-pwa

It’s safe to say Vite is currently the go-to way of building modern single-page web applications, the example here is using that build tool.

For Vite-based apps, vite-plugin-pwa provides a low-friction way to add just enough PWA capabilities without having to hand-write or deeply understand a service worker. It integrates with the Vite build pipeline, takes care of asset hashing at build time, and gives you pre-caching out of the box.

Most importantly, it lets you focus on deciding what should be cached, instead of worrying about how the service worker is implemented. A conceptual configuration might look like this:

import { VitePWA } from 'vite-plugin-pwa'

export default {
  plugins: [
    VitePWA({
      registerType: 'autoUpdate',
      strategies: 'generateSW',
      workbox: {
        globPatterns: ['**/*.{js,css,html,svg,png}'],
      },
    }),
  ],
}

With this in place, the app shell is pre-cached at build time and served from cache when the network is unavailable, making offline page reloads possible without any deeper offline functionality. The same principles apply if you use other build tools, but Vite makes this particularly straightforward.

Lazy-Loaded Routes

It's important to point out that modern web applications are often split into multiple JavaScript chunks that are only loaded when they are actually needed. When code-splitting is done by route (very common in SPAs) the code responsible for rendering a specific page might only be fetched when the user navigates to that route for the first time. With offline support, this becomes a problem because that code may simply never have been downloaded before the user goes offline.

A typical example looks like this:

const SettingsPage = lazy(() => import('./pages/settings'))

This works perfectly fine online. When the user tries to access the settings page for the first time, a new chunk of code is requested, and the user can see the page. However, if the user goes offline and then tries to navigate to /settings, the browser won't have the JavaScript chunk for that route. If it isn't already cached, the navigation will fail, even though the app shell itself loaded successfully.

The practical implication is straightforward: any route you expect users to access while offline must not depend on a chunk that was never cached. If a route must work offline, its code must be available before the network disappears. You can solve this in a couple of ways.

  1. Eagerly import critical routes. This increases your initial bundle size but guarantees the code is available offline.

  2. Include route chunks in your pre-cache configuration. This preserves the benefits of code-splitting but requires a bit more awareness of what your build actually produces.

This is not necessarily about making every route work offline. It’s about identifying the offline-critical path — usually landing pages, dashboards, or explicit offline states — and making sure those parts of the application are ready before the network is gone.

Summary

To wrap it up, for a web app to be accessible offline in the most basic sense:

  • A service worker must exist

  • The app shell must be pre-cached and served cache-first

  • Critical routes must not rely on uncached lazy chunks

With this baseline in place, the next step is deciding how data behaves offline — which is where things start to get more interesting. If you enjoyed the article or have a question, feel free to reach out on Bluesky! 👋

Further Reading and References

More from this blog

T

Tomasz Gil - Software Engineer | Blog

53 posts

I help product teams build quality software and lead engineering efforts. Currently working at OpenSpace as a Senior Software Engineer.