← Back to Tutorials Chapter 21

Architecture & Design Patterns

App Shell Pattern

The App Shell pattern separates the minimal HTML, CSS, and JavaScript needed to render the user interface from the dynamic content. This shell is cached on first load for instant rendering on subsequent visits.

// App Shell Architecture
// 1. Cache the shell on install
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('shell-v1').then(cache => {
      return cache.addAll([
        '/shell/',
        '/shell/styles.css',
        '/shell/app.js',
        '/shell/header.html',
        '/shell/footer.html',
        '/shell/nav.html',
        '/offline.html'
      ]);
    })
  );
});

// 2. Serve shell from cache, content from network
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);

  // Shell resources: cache first
  if (url.pathname.startsWith('/shell/')) {
    event.respondWith(caches.match(event.request));
    return;
  }

  // Content pages: network first with cache fallback
  if (url.pathname.startsWith('/content/')) {
    event.respondWith(
      fetch(event.request)
        .catch(() => caches.match('/offline.html'))
    );
    return;
  }

  // API calls: network only
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(fetch(event.request));
    return;
  }
});

PRPL Pattern

PRPL is a pattern for structuring and serving PWAs for fast initial load:

RAIL Model

RAIL is a user-centric performance model:

LetterMeaningTarget
RResponse50ms for input response
AAnimation10ms per frame (60fps)
IIdleUse idle time for deferred work
LLoad1s for content-first paint

Scalability Considerations

// Dynamic imports for code splitting
const button = document.getElementById('load-chart');
button.addEventListener('click', async () => {
  const { renderChart } = await import('./charts.js');
  renderChart('revenue-data');
});

// Intersection Observer for lazy loading
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});
Exercise: Refactor a simple SPA to use the App Shell pattern. Cache the shell (header, footer, navigation) on install, serve it from cache, and lazy-load the content pages on demand. Measure the performance improvement using Lighthouse.