An installed React app on a phone with no signal. GET requests to your API use Network First and show the last copy offline; POST, PUT and DELETE use Network Only, because a write must reach the server or fail visibly.

How to build a React Progressive Web App with Vite that installs, works offline and updates safely

One word hides three jobs. Installing, working offline and taking a new deploy each need their own setting, and you can check every one of them in the browser before you ship.

How do you build a React Progressive Web App with Vite?#

A React Progressive Web App on Vite is a normal Vite build plus vite-plugin-pwa, which adds a manifest and a Workbox service worker, served over HTTPS with a reload prompt for new versions. The vite-plugin-pwa guide names the two parts: "a web application manifest" and "a service worker".

So the work splits three ways. First, the manifest and HTTPS make the browser offer install. Second, the service worker and its caching rules let the app open with no network. Finally, the update flow decides what an open tab does when you deploy.

Three jobs, three settingsThree jobs, three settings: the manifest and HTTPS for install, the Workbox service worker for offline, the reload prompt for updates. vite-plugin-pwa guide, read October 2026.

What does an offline, installable React app change in practice?#

In a worked example of a 128 GiB phone, MDN's storage rules (January 2026) let Chromium keep 76.8 GiB for one origin and Firefox 10 GiB, so caching rules decide offline use. Those figures are arithmetic on MDN's January 2026 rules. Chromium allows 60% of disk, and Firefox best-effort mode allows 10% of disk or 10 GiB, whichever is smaller.

What one origin may store on your device

Enter the device's disk size in GiB to see how much one origin may store in Chromium and in Firefox best-effort mode.

Your device

an assumption, change it

Counts the two rules MDN states: Chromium allows 60% of disk, and Firefox best-effort mode allows 10% of disk or 10 GiB, whichever is smaller.

GiB one origin may store in Chromium

76.8

GiB one origin may store in Firefox best-effort mode
10

A worked example on MDN's January 2026 rules, not a measurement of any device.

In practice, an installed app opens from its own cache, even on a train with no signal. However, the quota is rarely what stops it. Instead, the risk is a runtime cache that grows with every image and API reply. MDN's guide says eviction deletes all of an origin's data at once. Therefore, every runtime cache in the config below gets an expiration.

Show data table
GiB one origin may store at four disk sizes, in Chromium and in Firefox best-effort mode. Source: arithmetic on MDN, Storage quotas and eviction criteria, 5 January 2026.
Dimension Chromium Firefox best-effort
64 GiB disk 38.4 GiB6.4 GiB
128 GiB disk 76.8 GiB10 GiB
256 GiB disk 153.6 GiB10 GiB
512 GiB disk 307.2 GiB10 GiB

Chromium's ceiling grows with the disk, while Firefox best-effort mode stops at 10 GiB.

Per-origin ceiling by disk size GiB one origin may store at four disk sizes, in Chromium and in Firefox best-effort mode. Source: arithmetic on MDN, Storage quotas and eviction criteria, 5 January 2026. Arithmetic from MDN, Storage quotas and eviction criteria (5 January 2026). Modelled, not measured

What is PWA compliance, and what does Chrome check before it offers install?#

PWA compliance means meeting the browser's installability criteria, which Google's web.dev list (September 2024) gives as HTTPS plus a manifest with a name, start_url, a display mode and 192px and 512px icons. In short, a compliant PWA uses HTTPS, ships a full manifest and meets the install checks. A service worker then adds offline use.

The six things Chrome checks before it offers install, and what each one gates. Source: Google, web.dev, What does it take to be installable?, last updated 19 September 2024.

Chrome install criterionWhat web.dev asks forWhat it gates
HTTPSthe app is served over HTTPSinstall, and every service worker
Manifest nameshort_name or namethe label on the home screen
Manifest iconsa 192px and a 512px iconthe install prompt and the launcher icon
start_urlpresent in the manifestwhere the installed app opens
displayfullscreen, standalone, minimal-ui or window-controls-overlaythe app window without browser chrome
Engagementone click or tap, and 30 seconds on the pagewhen the install promotion appears

Source: Google, web.dev, "What does it take to be installable?", last updated 19 September 2024.

Notice what is missing: Chrome's current list does not name a service worker. So the worker makes the app useful offline, not installable. MDN's service worker guide adds that workers run only over HTTPS, and browsers treat your own machine as a secure origin for testing. Also, web.dev notes that other browsers differ slightly, so test in each one. The MDN manifest reference lists every member the file may carry.

Which Workbox caching strategy should each request in a React app use?#

Precache the built app shell, serve fonts and images with Stale-While-Revalidate or Cache First, read API data with Network First, and send anything that writes through Network Only. The rule behind the choice is simple: pick by how stale each response may be.

A strategy per requestRequest type to Workbox strategy in a Vite React app. Chrome for Developers, workbox-strategies, last updated 27 November 2017.

The Workbox strategy for each kind of request in a Vite React app, and why. Source: Chrome for Developers, workbox-strategies, last updated 27 November 2017.

Request in a Vite React appWorkbox strategyWhy
index.html, JS and CSS from the buildprecachehashed files change only on deploy
ImagesStale-While-Revalidateshow the cached copy, refresh it behind the scenes
Web fontsCache Firstthey almost never change
GET requests to your APINetwork Firstfresh data online, the last copy offline
POST, PUT and DELETENetwork Onlya write must reach the server or fail visibly

Source: Chrome for Developers, workbox-strategies, last updated 27 November 2017; strategy choice per request is our reading of it.

The vite-plugin-pwa generateSW page takes these as runtimeCaching entries. Each row becomes one object with a urlPattern, a handler and options, and each cache gets maxEntries and maxAgeSeconds. That follows the Workbox runtime caching guide, which says "Be aware of storage quotas". Finally, navigateFallbackDenylist keeps /api/ paths out of the app shell fallback.

What happens to open tabs when you deploy a new build?#

A new service worker installs beside the old worker and waits, so an open tab keeps running the old build until the app asks to reload, which is the job of registerType prompt. MDN's service worker guide calls this the default, and notes that a worker can call skipWaiting() to take over at once.

That wait is a feature, because a person halfway through a long form keeps their typing while the new build sits ready. The vite-plugin-pwa prompt for update page says prompt is the default for the registerType option. However, most quick guides switch it to autoUpdate, which reloads the page as soon as the new worker takes over.

In React, the useRegisterSW hook returns needRefresh, offlineReady and updateServiceWorker. So a small ReloadPrompt component shows a message when a new build is waiting. Then it calls updateServiceWorker(true) only when the person clicks Reload. In short, the deploy reaches every tab, but on the person's terms.

  1. The new service worker installs and waits

    An open tab keeps running the old build.

  2. useRegisterSW sets needRefresh

    With registerType prompt, your component shows a Reload button.

  3. The person clicks Reload

    updateServiceWorker(true) swaps in the new build.

Which official docs do you build a React PWA from, and in what order?#

Build from these pages in order: the Vite getting started guide, the vite-plugin-pwa guide, its React page, its generateSW page, and the Workbox strategies reference.

  1. Vite getting started: the scaffold command, and the Node.js version Vite needs as of October 2026, "20.19+, 22.12+".
  2. vite-plugin-pwa guide: the install command and the VitePWA plugin options.
  3. vite-plugin-pwa for React: useRegisterSW, the ReloadPrompt pattern and the type declarations.
  4. generateSW options: runtimeCaching and navigateFallbackDenylist.
  5. Workbox strategies: what each handler does on a hit and on a miss.
  6. web.dev install criteria: the checklist to test the finished build against.

What is the smallest working Vite config for a React PWA?#

The smallest working setup is a VitePWA block in vite.config.ts with a manifest and runtimeCaching, plus a ReloadPrompt component built on useRegisterSW. In TypeScript, five steps turn a fresh Vite React project into an installable, offline, updatable app.

Step 1 scaffolds the app and installs the plugin. The React page says workbox-window must be a dev dependency too.

bash
npm create vite@latest field-notes -- --template react-ts
cd field-notes
npm install
npm install -D vite-plugin-pwa workbox-window

Step 2 replaces vite.config.ts. Put pwa-192x192.png and pwa-512x512.png in the public folder first, since the manifest points at them.

ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: 'prompt',
      manifest: {
        name: 'Field Notes',
        short_name: 'Notes',
        start_url: '/',
        display: 'standalone',
        theme_color: '#0f172a',
        icons: [
          { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
          { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
        ],
      },
      workbox: {
        globPatterns: ['**/*.{js,css,html,svg,png,woff2}'],
        navigateFallbackDenylist: [/^\/api\//],
        runtimeCaching: [
          {
            urlPattern: ({ request }) => request.destination === 'image',
            handler: 'StaleWhileRevalidate',
            options: {
              cacheName: 'images',
              expiration: { maxEntries: 60, maxAgeSeconds: 60 * 60 * 24 * 30 },
            },
          },
          {
            urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
            handler: 'CacheFirst',
            options: {
              cacheName: 'fonts',
              expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 },
              cacheableResponse: { statuses: [0, 200] },
            },
          },
          {
            urlPattern: ({ url }) => url.pathname.startsWith('/api/'),
            handler: 'NetworkFirst',
            options: {
              cacheName: 'api',
              networkTimeoutSeconds: 3,
              expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 },
            },
          },
          {
            urlPattern: ({ url }) => url.pathname.startsWith('/api/'),
            handler: 'NetworkOnly',
            method: 'POST',
          },
        ],
      },
    }),
  ],
})

Step 3 adds src/pwa.d.ts with one line, /// <reference types="vite-plugin-pwa/react" />, so TypeScript knows the virtual module. Step 4 adds the prompt and renders <ReloadPrompt /> once inside App.

tsx
import { useRegisterSW } from 'virtual:pwa-register/react'
export default function ReloadPrompt() {
  const {
    offlineReady: [offlineReady, setOfflineReady],
    needRefresh: [needRefresh, setNeedRefresh],
    updateServiceWorker,
  } = useRegisterSW()
  if (!offlineReady && !needRefresh) return null
  return (
    <div role="status">
      <span>{needRefresh ? 'A new version is ready.' : 'Ready to work offline.'}</span>
      {needRefresh && <button onClick={() => updateServiceWorker(true)}>Reload</button>}
      <button onClick={() => { setOfflineReady(false); setNeedRefresh(false) }}>Close</button>
    </div>
  )
}

Step 5 runs npm run build and then npm run preview. Then open the Application panel in Chrome DevTools, check the manifest and the worker, and go offline to test a reload. The plugin builds both only on a build, unless you turn on its devOptions.

What should you do with an old Create React App PWA?#

The React team deprecated Create React App for new apps on react.dev on 14 February 2025, so an old CRA service worker moves to Vite and vite-plugin-pwa rather than being patched in place. The react.dev post says, "Today, we’re deprecating Create React App for new apps". It points existing apps to a framework or to a build tool such as Vite. The Create React App docs now open with "Create React App is deprecated."

In practice, an older React JS Progressive Web App built on CRA needs a move, not a patch. Instead, move the app to Vite with the react.dev guide to building React from scratch. Then remove the old worker and its register call, and apply the five steps above.

Move, not patchMigrate or patch an old Create React App PWA. React, Sunsetting Create React App, 14 February 2025.

How much can an installed React app store offline?#

MDN's storage guide (January 2026) lets one origin keep up to 60% of total disk in Chromium and in Safari, against 10% or 10 GiB in Firefox best-effort mode. The ceiling is generous on every engine, even for a large app.

Show data table
Per-origin storage as a share of disk, MDN, Storage quotas and eviction criteria, 5 January 2026.
Item Value
Chromium (Chrome, Edge) 60
WebKit browser app (Safari) 60
Firefox, persistent mode 50
WebKit embedded web view 15
Firefox, best-effort mode 10

Chromium and Safari let one origin keep up to 60% of the disk, so an unbounded runtime cache, not the quota, is the limit.

Figure Per-origin storage as a share of disk, MDN, Storage quotas and eviction criteria, 5 January 2026. MDN, Storage quotas and eviction criteria, 5 January 2026

Because the ceiling is that high, the limit an offline React app meets first is its own unbounded runtime cache. When storage does run out, MDN says a write fails with a QuotaExceededError. So the expiration rules from the config are the real budget, not the browser.

Why should offline data live in IndexedDB rather than localStorage?#

MDN's storage guide (January 2026) caps localStorage at 5 MiB per origin, so offline records and API responses belong in IndexedDB or the Cache API. Web Storage is small by design, and it was never meant for app data.

Web Storage limits per originsmall by design

5 MiB

localStorage

10 MiB

Web Storage maximum

localStorage holds 5 MiB per origin, so offline records belong in IndexedDB or the Cache API.

Web Storage limits per origin (MiB per origin)
OptionMiB per origin
localStorage5 MiB
Web Storage maximum10 MiB

Source: MDN, Storage quotas and eviction criteria, 2026

IndexedDB and the Cache API, by contrast, share the large origin quota above. So in a React app, keep small settings in localStorage. However, put drafts, queued records and cached lists in IndexedDB. MDN describes it as storage for large, indexed data.

When is a Progressive Web App the wrong tool for a React project?#

A PWA is the wrong tool when the product depends on app store distribution or on device features the browser does not expose; a native or cross-platform app serves those better. Three cases come up most, and each has a better route.

First, if people must find the app in a store, ship a store app. A cross-platform build can still reuse your React skills. Second, if the core feature needs hardware the browser does not offer, build native and keep the web app as the light front door. Third, if most visits are one-off reads, skip the install work and make the site fast. The site optimization guide covers that. If store reach decides it, a mobile app development team can scope the native route.

PWA, native or a fast siteWhen a PWA, a native or cross-platform app, or a plain fast site is the right route.

Where do you go after the app installs and works offline?#

Once the app installs, works offline and updates on a prompt, the next steps are push notifications, search behaviour and overall speed. The Web Push with VAPID guide adds push to the same worker. Then what a PWA does for SEO covers search. For the case of a PWA over a native app, see PWA development in Northern Ireland.

  1. Push

    Push notifications

    The Web Push with VAPID guide adds push to the same worker.

  2. Search

    Search behaviour

    What a PWA does for SEO covers search.

  3. Speed

    Overall speed

    The site optimization guide covers making the site fast.

If the build needs more hands, React developers can take it on. Still, the six pages listed above are enough to build every step here on your own.

Questions this post answers

What is PWA compliance?
The PWA compliance meaning is simple: an app meets the browser's install criteria. That is HTTPS, a manifest with a name, icons, start_url and a display mode, and some user engagement. A service worker then adds offline use.
Does a React Progressive Web App need a service worker to be installable?
Chrome's install criteria on web.dev, last updated 19 September 2024, do not list a service worker. You still want one, because it is what makes the installed app open offline and pick up new builds.
How does a React PWA pick up a new build after a deploy?
The new service worker installs and waits. With registerType prompt, useRegisterSW sets needRefresh, your component shows a Reload button, and updateServiceWorker(true) swaps in the new build.
Can you still turn a Create React App project into a PWA?
Create React App is deprecated, per react.dev on 14 February 2025. Move the project to Vite, then add vite-plugin-pwa with the same PWA React setup shown above.
Can a PWA replace converting a React web app to a mobile app?
Often, yes, when the app needs install, offline use and push. When it needs a store listing or device features the browser lacks, go native or cross-platform.

Keep reading