← Back to articles
Performance & Databases

Browser Caching: How to Make Your Site "Remember" Data and Load Instantly

A practical guide to browser caching: Cache-Control headers, ETags with 304 responses, LocalStorage for client state, and Service Workers for advanced offline strategies.

✦
Featured article ↗

Browser Caching: How to Make Your Site "Remember" Data and Load Instantly

Cache-Control, ETag, LocalStorage — not magic, but essential tools. Let's understand when and how to use each.

Imagine this: a user visits your website for the second time, but it loads as if it were the first — all scripts, styles, and images are downloaded again. Frustration, unnecessary requests, extra server load. Sound familiar?

The solution is smart client-side caching. This isn't just about "speeding things up"; it's a fundamental principle for building fast, responsive web applications.

Let's break down three key mechanisms every frontend developer should know and understand when to use each. For clarity, I'll include examples in Laravel (the backend often handles cache configuration).

1. Cache-Control: The Conductor of HTTP Caching

This is an HTTP header that gives the browser strict instructions on how long to store a resource (JS, CSS, images, fonts).

  • Cache-Control: public, max-age=3600 — store in the browser cache and any intermediate proxies for 1 hour.
  • Cache-Control: no-cache — cache it, but always check with the server for freshness.
  • Cache-Control: no-store — do not cache at all (for sensitive data).

When to use: For all static resources whose versions change infrequently (build files, fonts, icons). Use hashed filenames (app.a1b2c3.css) so you can set max-age for years.

Example in Laravel (for static assets): You can set rules in .htaccess (Apache) or Nginx configuration, which is often the preferred method. However, in Laravel, you can send headers when serving assets through a route or middleware:

// In Middleware or a controller for serving a specific asset Route::get('/static/{file}', function ($file) { $path = storage_path('app/static/' . $file); return response()->file($path, [ 'Cache-Control' => 'public, max-age=31536000', // 1 year ]); });

2. ETag: Lightweight Freshness Check

ETag (Entity Tag) is a hash digest of a resource generated by the server. The browser saves the resource and its ETag. On the next request, it sends the ETag in the If-None-Match header. The server compares: if the ETag matches, the resource hasn't changed — it returns an empty response with status 304 Not Modified. This saves bandwidth and time.

When to use: For data that might change, but checking it from scratch every time is costly (product lists, articles, API responses).

Example in Laravel: Laravel can automatically generate ETags for responses. Or you can do it manually:

use Symfony\Component\HttpFoundation\Response; Route::get('/api/products', function () { $products = Product::all(); $content = $products->toJson(); // Create an ETag based on content $etag = md5($content); // Check if the client's ETag matches ours if (request()->header('If-None-Match') === $etag) { return response(null, 304); // Not Modified } // Return response with ETag return response($content) ->header('Cache-Control', 'no-cache') // Cache, but validate via ETag ->header('ETag', $etag); });

3. LocalStorage / SessionStorage: Application Data Cache

This is not about HTTP, but the Web Storage API. A full key-value store in the browser (up to 5-10 MB). Synchronous, works only within the same origin.

  • LocalStorage — persists forever, until cleared.
  • SessionStorage — lives only during the session (tab).

When to use: For non-critical data needed for the client application's operation, where you can tolerate staleness:

  • Authorization token (be careful! better in HttpOnly cookies)
  • Non-100%-critical data: UI theme settings, form drafts, quick search results.
  • SPA application state after a page reload.

Frontend Example (JavaScript):

// Save API response data async function loadProducts() { const cached = localStorage.getItem('products'); if (cached) { return JSON.parse(cached); // Instant return from cache } const response = await fetch('/api/products'); const products = await response.json(); localStorage.setItem('products', JSON.stringify(products)); return products; } // Remember to invalidate cache on data mutation logic function clearProductCache() { localStorage.removeItem('products'); }

Advanced Pro-Tip: Service Workers for Ultimate Control

For full control over caching and offline functionality, consider Service Workers. They act as a programmable network proxy, allowing you to implement strategies like "Cache First, Network Fallback" or "Stale-While-Revalidate."

Summary: What, When, and Why

  1. Cache-Control + hashed filenames — for static assets (CSS, JS, images). Result: The browser serves them from disk, making zero network requests.
  2. ETag / no-cache — for dynamic data and API. Result: The browser makes a request but gets an empty 304 response if data is unchanged.
  3. LocalStorage — for client application state. Result: Instant data access on revisits or page reloads.
  4. Service Worker — for advanced offline and performance strategies. Result: Full control over the network and cache.

The Golden Rule: Caching must be invalidateable. There must always be a mechanism to purge stale data (via filename change, ETag update, or manual clearing from LocalStorage).

A smart combination of these approaches reduces request counts, saves user bandwidth, and makes interaction with your site feel truly instantaneous. Users will appreciate it, and your Core Web Vitals will improve.

Technologies & topics

Article tags

No articles match these filters.

Have a project or an idea to discuss?

Let's talk ↗