Skip to content

PWA Architecture Patterns β€” Full App Shell, Server-Only, and Hybrid

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about PWA Architecture Patterns. We cover key concepts, practical examples, and best practices to help you master this topic.

PWA architecture patterns range from full app shell to server-only with hybrid approaches that balance offline capability and complexity for different application needs.

What You'll Learn

By the end of this tutorial, you will understand three main PWA architecture patterns, when to use each, and how to choose the right architecture for your project.

Why It Matters

There is no one-size-fits-all PWA architecture. A news app needs different offline behavior than a weather app or a form-heavy enterprise app. Choosing the wrong architecture leads to over-engineering or poor offline experience. Understanding the spectrum helps you pick the right balance.

Architecture Spectrum

PWA Architecture Patterns
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚                    Architecture Spectrum                      β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚  App Shell β”‚   Full       β”‚   Hybrid     β”‚   Server-Only    β”‚
    β”‚  (Full     β”‚   Static     β”‚   App Shell  β”‚   (Thin SW)     β”‚
    β”‚  Offline)  β”‚   Content    β”‚   + API      β”‚                  β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚  Max       β”‚  High        β”‚  Moderate    β”‚  Low offline     β”‚
    β”‚  offline   β”‚  offline     β”‚  offline     β”‚  capability      β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚  High      β”‚  Moderate    β”‚  Moderate    β”‚  Low complexity  β”‚
    β”‚  complexityβ”‚  complexity  β”‚  complexity  β”‚                  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Think of architecture patterns like vehicles. App shell is a fully-equipped RV β€” everything you need for extended offline travel. Full static is a reliable car with a full tank. Hybrid is a car with a spare gas can. Server-only is a bicycle β€” great for short trips, limited for long journeys.

Pattern 1: App Shell (Full Offline)

Ideal for: Apps where most content should work offline.

// sw.js β€” App Shell implementation
const SHELL_CACHE = 'shell-v2';
const DYNAMIC_CACHE = 'dynamic-v2';

// Pre-cache shell on install
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(SHELL_CACHE).then(cache => {
            return cache.addAll([
                '/',
                '/index.html',
                '/styles/shell.css',
                '/scripts/main.js',
                '/scripts/router.js',
                '/images/logo.svg',
                '/fonts/inter.woff2',
                '/offline.html'
            ]);
        }).then(() => self.skipWaiting())
    );
});

// Cache-first for shell, network-first for content
self.addEventListener('fetch', event => {
    const url = new URL(event.request.url);

    const isShellAsset = url.pathname.match(/\.(css|js|woff2?|svg)$/) ||
                         SHELL_ASSETS.includes(url.pathname);

    if (isShellAsset) {
        // Cache-first for shell
        event.respondWith(
            caches.match(event.request).then(cached => {
                return cached || fetch(event.request).then(response => {
                    caches.open(SHELL_CACHE).then(cache => cache.put(event.request, response));
                    return response.clone();
                });
            })
        );
    } else if (event.request.mode === 'navigate') {
        // Network-first for pages
        event.respondWith(
            fetch(event.request).then(response => {
                caches.open(DYNAMIC_CACHE).then(cache => cache.put(event.request, response));
                return response;
            }).catch(() => caches.match(event.request).then(cached => {
                return cached || caches.match('/offline.html');
            }))
        );
    } else {
        // Network-first for API
        event.respondWith(
            fetch(event.request).catch(() => caches.match(event.request))
        );
    }
});

Pattern 2: Full Static Content

Ideal for: Blogs, documentation, marketing sites with content that changes infrequently.

// sw.js β€” Full static content pre-caching
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open('static-v3').then(cache => {
            return cache.addAll([
                '/',
                '/index.html',
                '/about/',
                '/docs/',
                '/blog/',
                '/styles/main.css',
                '/scripts/app.js',
                '/images/',
                '/offline.html'
            ]);
        }).then(() => self.skipWaiting())
    );
});

self.addEventListener('fetch', event => {
    // Cache-only: everything was pre-cached
    event.respondWith(
        caches.match(event.request).then(cached => {
            if (cached) return cached;

            // If not cached, fetch and cache for next time
            return fetch(event.request).then(response => {
                if (response.ok) {
                    caches.open('static-v3').then(cache => cache.put(event.request, response));
                }
                return response;
            }).catch(() => caches.match('/offline.html'));
        })
    );
});

Pattern 3: Hybrid (App Shell + Dynamic API)

Ideal for: Social media, news, dashboards with dynamic content.

// sw.js β€” Hybrid approach
self.addEventListener('fetch', event => {
    const url = new URL(event.request.url);

    if (isShellAsset(url)) {
        // Shell assets: cache-first
        event.respondWith(cacheFirst(event.request));
    } else if (url.pathname.startsWith('/api/')) {
        // API: stale-while-revalidate
        event.respondWith(staleWhileRevalidate(event.request));
    } else if (event.request.mode === 'navigate') {
        // Navigation: network-first
        event.respondWith(networkFirst(event.request));
    }
});

function cacheFirst(request) {
    return caches.match(request).then(cached => {
        return cached || fetch(request);
    });
}

function staleWhileRevalidate(request) {
    return caches.match(request).then(cached => {
        const fetchPromise = fetch(request).then(response => {
            if (response.ok) {
                caches.open('api-cache').then(cache => cache.put(request, response));
            }
            return response;
        });
        return cached || fetchPromise;
    });
}

function networkFirst(request) {
    return fetch(request).catch(() => caches.match(request));
}

Pattern 4: Server-Only (Thin Service Worker)

Ideal for: Simple sites where offline is a nice-to-have, not critical.

// sw.js β€” Minimal service worker
self.addEventListener('install', event => {
    // Cache only the offline fallback page
    event.waitUntil(
        caches.open('minimal-v1').then(cache => {
            return cache.addAll([
                '/offline.html'
            ]);
        }).then(() => self.skipWaiting())
    );
});

self.addEventListener('fetch', event => {
    // If offline, show offline page for navigations
    event.respondWith(
        fetch(event.request).catch(() => {
            if (event.request.mode === 'navigate') {
                return caches.match('/offline.html');
            }
            // For assets, return empty
            return new Response('', { status: 408 });
        })
    );
});

Choosing the Right Architecture

// Architecture decision helper
const projectCriteria = {
    type: 'news-app', // 'content', 'app', 'game', 'ecommerce'
    offlineCritical: true,
    contentChangesFrequently: true,
    hasUserAuth: true,
    teamSize: 3,
    targetAudience: 'emerging-markets'
};

function recommendArchitecture(criteria) {
    if (criteria.offlineCritical && criteria.contentChangesFrequently) {
        return 'Hybrid (App Shell + Dynamic API)';
    } else if (criteria.offlineCritical && !criteria.contentChangesFrequently) {
        return 'Full Static Content';
    } else if (criteria.teamSize <= 2 && !criteria.offlineCritical) {
        return 'Server-Only (Thin SW)';
    } else {
        return 'App Shell (Full Offline)';
    }
}

Common Mistakes

  1. Over-engineering for the first version. Start simple (thin SW) and add offline capability as needed. Full app shell is overkill for a 5-page site.
  2. Choosing app shell for content that rarely changes. Full static pre-Caching is simpler and provides better offline experience for static content.
  3. Not considering user data sensitivity. Apps with user authentication need careful cache strategies to avoid serving stale or wrong-user data.
  4. Ignoring team capabilities. App shell architecture requires more frontend engineering. Choose based on what your team can maintain.
  5. Assuming one pattern fits all pages. Different routes in the same app can use different patterns. Blog posts can be static, while the dashboard is hybrid.

Practice Questions

  1. What are the four main PWA architecture patterns?
  2. When would you choose full static content over app shell?
  3. What is the advantage of the hybrid approach?
  4. When is a thin service worker (server-only) appropriate?
  5. How do you decide which architecture to use for a new project?

Challenge: Evaluate three different PWA types (recipe app, stock trading dashboard, company blog) and recommend an architecture for each. Justify each choice based on offline requirements, content freshness needs, and development complexity.

FAQ

Can I switch architecture patterns later?

Yes. Start simple and add offline capability incrementally. Adding caching to a thin service worker is easier than removing unnecessary complexity from a full app shell.

Does the architecture affect SEO?

Yes. App shell can hurt SEO because crawlers see empty content. Use SSR or dynamic rendering for search engines. Full static is best for SEO.

What architecture do most production PWAs use?

Most successful PWAs use a hybrid approach: app shell with static assets cached aggressively and API data cached with stale-while-revalidate or network-first.

Can different routes use different architectures?

Yes. Your homepage might use full static, while the dashboard uses hybrid. The service worker can apply different strategies based on the URL pattern.

Does architecture affect development time significantly?

App shell adds 2-4 weeks of initial development. Thin SW adds 1-2 days. Choose based on whether offline is core to your value proposition.

Mini Project

Create three different service workers for the same app (a news reader): one using full app shell (offline everything), one using hybrid (shell + API caching), and one using thin SW (offline fallback only). Compare the offline capability and complexity of each. Document which you would use for a production deployment.

What's Next

Architecture is chosen. Now learn about PWA performance optimization β€” making your PWA load fast, run smoothly, and conserve resources.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro