Skip to main content

Command Palette

Search for a command to run...

⚡️ The 3 Layers of Caching Every Frontend Engineer Should Know

Published
8 min read

Have you ever noticed how some pages load instantly on your second visit? Or wondered why your API call doesn't always hit the server? Or been amazed when an app works perfectly offline?

That's caching at work — and it's happening at multiple layers simultaneously.

As a frontend developer, understanding these caching layers is crucial for building fast, resilient applications. Let's demystify the three types of caching you'll encounter and when to use each one.

Last week, our team spent hours debugging a frustrating issue: users weren't seeing the latest data even though our API was returning updated values. After digging through network logs, we discovered the culprit—a Cache-Control: max-age=360000 header was causing responses to be served directly from the browser's disk cache. The API wasn't even being called!

This incident revealed a knowledge gap: Several team members weren't familiar with how different caching layers work in the frontend. They understood React Query and state management, but browser HTTP caching and Service Workers felt like mysterious black boxes.

Have you ever noticed how some pages load instantly on your second visit? Or wondered why your API call doesn't always hit the server? Or been amazed when an app works perfectly offline?

That's caching at work—happening at three distinct layers simultaneously.

Understanding these caching layers is crucial for building fast, resilient applications and avoiding those multi-hour debugging sessions. Let's demystify the three types of caching you'll encounter and when to use each one.


Layer 1: Application-Level Caching (In-Memory)

What it is: Data stored temporarily in your application's memory while it's running.

Where it lives: JavaScript runtime (browser memory, React state, library cache)

When to Use It

Application-level caching is perfect when:

  • You're fetching the same data multiple times in a short period

  • Users navigate between pages that share common data

  • The data is relatively stable (doesn't change every second)

  • You want to eliminate loading spinners for repeat requests

Real-World Example

Imagine a dashboard with multiple tabs showing user analytics. Without caching, switching between tabs would trigger a new API call each time — annoying loading spinners everywhere!

With application-level caching using React Query (TanStack Query):

const { data, isLoading } = useQuery({
  queryKey: ['analytics', userId],
  queryFn: () => fetchAnalytics(userId),
  staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
  cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
});

What happens:

  1. First visit: API call is made, data is cached

  2. Switch tabs: No API call — instant data from cache

  3. Come back after 3 minutes: Still using cached data (it's "fresh")

  4. Come back after 6 minutes: Shows cached data immediately, refetches in background

  5. Refresh the page: Cache is cleared, starts fresh

The Tradeoff

Pros:

  • Blazing fast — no network latency

  • Reduces server load

  • Smoother user experience

Cons:

  • Data only lives during the session

  • Gone on page refresh

  • Not shared across browser tabs

  • Can show stale data if not configured properly


Layer 2: Service Worker Caching (Browser-Level)

What it is: A programmable proxy that sits between your app and the network, intercepting requests and deciding how to respond.

Where it lives: Browser (separate from your main app thread, survives page refreshes)

When to Use It

Service Workers shine when you need:

  • Offline functionality — app works without internet

  • Faster repeat visits — cache entire pages, images, API responses

  • Custom caching strategies — fine-grained control over what/when/how to cache

  • Background sync — queue requests when offline, send when back online

Real-World Example

A recipe app that lets users browse saved recipes even without internet:

// In service-worker.js
self.addEventListener('fetch', (event) => {
  const { request } = event;

  // Cache-first strategy for images
  if (request.url.includes('/images/')) {
    event.respondWith(
      caches.match(request).then(cached => {
        return cached || fetch(request).then(response => {
          return caches.open('images-v1').then(cache => {
            cache.put(request, response.clone());
            return response;
          });
        });
      })
    );
  }

  // Network-first for API calls, fallback to cache
  if (request.url.includes('/api/')) {
    event.respondWith(
      fetch(request)
        .then(response => {
          const clone = response.clone();
          caches.open('api-v1').then(cache => cache.put(request, clone));
          return response;
        })
        .catch(() => caches.match(request)) // Offline fallback
    );
  }
});

What happens:

  1. User visits a recipe page online → Service Worker caches it

  2. User goes offline → Service Worker serves cached version

  3. User browses other cached recipes → All work offline

  4. User comes back online → Service Worker updates cache in background

Common Caching Strategies

  • Cache First: Check cache, fallback to network (good for static assets)

  • Network First: Try network, fallback to cache if offline (good for dynamic content)

  • Stale-While-Revalidate: Serve cache immediately, update in background (best UX)

  • Cache Only: Never hit network (for truly static content)

  • Network Only: Never cache (for sensitive data)

The Tradeoff

Pros:

  • Survives page refreshes and browser restarts

  • Enables offline experiences

  • Full control over caching logic

  • Can cache anything (HTML, CSS, JS, API responses, images)

Cons:

  • More complex to implement and debug

  • Can serve stale content if not managed properly

  • Requires careful cache invalidation strategy

  • Users might not see updates immediately


Layer 3: HTTP Caching (Browser, CDN, Proxy)

What it is: The foundational caching layer built into HTTP protocol itself, controlled by response headers from the server.

Where it lives: Browser cache, CDN servers (Cloudflare, Fastly), reverse proxies (NGINX, Varnish)

When to Use It

HTTP caching is ideal for:

  • Static assets (JS bundles, CSS files, images, fonts)

  • Shared resources — cached by CDNs and served to millions of users

  • Bandwidth savings — reduces data transfer globally

  • Scalability — reduces server load exponentially

How It Works: The Headers

The server controls caching through HTTP headers:

HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, immutable
ETag: "abc123"
Last-Modified: Mon, 01 Jan 2024 00:00:00 GMT

Key headers explained:

  • Cache-Control: max-age=31536000 — Cache for 1 year (in seconds)

  • Cache-Control: public — Can be cached by CDNs and shared

  • Cache-Control: private — Only browser can cache (not CDNs)

  • Cache-Control: no-store — Never cache (sensitive data)

  • Cache-Control: immutable — Never revalidate (perfect for hashed filenames)

  • ETag — Unique identifier for this version

  • Last-Modified — When this resource was last changed

Real-World Example: Conditional Requests

Let's see how browsers avoid downloading unchanged files:

First request:

GET /app.js HTTP/1.1
Host: example.com

→ 200 OK
Cache-Control: max-age=3600
ETag: "v1.0.0"
Content-Length: 50000
[file content]

Second request (within 1 hour): Browser uses cached copy — no request sent at all!

Third request (after 1 hour):

GET /app.js HTTP/1.1
Host: example.com
If-None-Match: "v1.0.0"304 Not Modified
[no content — browser reuses cached copy]

The server only sends 304 Not Modified (tiny response), saving 50KB of bandwidth!

Modern Best Practices

For immutable assets (with hash in filename):

Cache-Control: public, max-age=31536000, immutable

Example: app.a3f2c1d.js — cache forever, filename changes when content changes

For HTML (entry point):

Cache-Control: no-cache

Always revalidate — ensures users get latest version

For API responses:

Cache-Control: private, max-age=300

Cache for 5 minutes, only in browser (not CDN)

The Tradeoff

Pros:

  • Works everywhere — browsers, CDNs, proxies

  • Zero code required (just configure headers)

  • Scales globally (CDNs distribute cached content)

  • Saves massive amounts of bandwidth

Cons:

  • Less flexible — can't implement complex logic

  • Hard to invalidate caches once set

  • Shared caches (CDN) can serve stale content

  • Users might not see updates until cache expires


How These Layers Work Together: A Complete Journey

Let's trace what happens when a user visits your news app:

First Visit

  1. HTTP Cache (Browser): Checks cache — miss (first visit)

  2. Service Worker: Intercepts request — no cache yet

  3. Network: Fetches from server

  4. Server responds with: Article data + Cache-Control: max-age=300

  5. Service Worker: Stores copy in cache

  6. Application (React Query): Caches in memory with staleTime: 5 minutes

  7. Browser: Stores copy in HTTP cache for 5 minutes

User Switches Tabs (10 seconds later)

  1. Application Cache: Returns data instantly from memory — no network request!

User Refreshes Page (1 minute later)

  1. Application Cache: Cleared (page refresh)

  2. Service Worker: Has cached copy — serves instantly

  3. HTTP Cache: Also has copy, but Service Worker served first

  4. Application: Re-caches in memory

User Returns (10 minutes later, offline)

  1. Application Cache: Cleared (new session)

  2. Service Worker: Serves cached copy — app works offline!

  3. HTTP Cache: Would have expired anyway

User on Different Device (same city)

  1. CDN (HTTP Cache): Serves from nearby edge server — no origin server hit!

💡 Choosing the Right Layer

ScenarioBest ChoiceWhy
Smooth tab switchingApplication CacheInstant, no network
Offline supportService WorkerSurvives page refresh
Static assets (JS/CSS)HTTP CacheCDN distribution
Real-time dataNo cachingData changes constantly
User-specific dataApplication + HTTP privateFast + secure
Public imagesHTTP Cache (CDN)Global distribution

Quick Decision Framework

Start with this thought process:

  1. Does the data change frequently?

    • Yes (every few seconds) → Minimal or no caching

    • No → Continue...

  2. Should it work offline?

    • Yes → Service Worker caching

    • No → Continue...

  3. Is it user-specific?

    • Yes → Application cache + HTTP private cache

    • No → HTTP public cache (CDN-friendly)

  4. Is it a static asset?

    • Yes → Aggressive HTTP caching with immutable flag

    • No → Shorter cache durations with revalidation


Key Takeaways

  1. Application caching = Speed during the session (in-memory, React Query, SWR)

  2. Service Worker caching = Offline resilience + custom strategies

  3. HTTP caching = Global scalability + bandwidth savings

Each layer complements the others. Master all three, and you'll build apps that are:

  • ⚡️ Fast — users see instant responses

  • 🔌 Resilient — work offline when needed

  • 📈 Scalable — handle millions of users efficiently

  • 💰 Cost-effective — reduce server and bandwidth costs

Remember: Caching is not just about speed — it's about creating better user experiences while reducing costs and improving reliability.


Further Reading


What caching challenges have you faced in your projects? Share your experiences in the comments! 👇