Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

27 chapters
⌕
Beginner9›
B1Setup & mental modelB2Types & valuesB3Operators & flowB4Functions (first half)B5Objects & arrays (first half)B6DOM & eventsB7Basic asyncB8Errors & tools★Cheat page
Intermediate10›
I1Scope & functions, properlyI2Objects deeplyI3Prototypes & OOPI4Async, properlyI5Modules & toolingI6Regex, dates & APIsI7Error handlingI8Real-time connectionsI9Offline & storage★Cheat page
Advanced10›
A1Engine & memoryA2Advanced asyncA3MetaprogrammingA4Types & dataA5Patterns & architectureA6PerformanceA7SecurityA8EcosystemA9Testing★Cheat page
/ search[ ] chaptert top

JavaScript levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
I9

Offline & storage

localStorage was the whole story two chapters ago. Past a few MB, it stops being enough.

Why localStorage runs out

Already covered: localStorage and sessionStorage hold roughly 5-10MB, strings only, and every operation is synchronous — a big read or write briefly blocks the main thread. Fine for a theme preference or a small cache; the wrong tool the moment "small" stops being true.

IndexedDB

A real, asynchronous, transactional database built into the browser — structured records (not just strings), indexes to query by, and a storage ceiling set by available disk space rather than a fixed few megabytes.

const request = indexedDB.open("my-app-db", 1);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  db.createObjectStore("notes", { keyPath: "id" });   // runs once, on first open or version bump
};

request.onsuccess = (event) => {
  const db = event.target.result;
  const tx = db.transaction("notes", "readwrite");
  tx.objectStore("notes").put({ id: 1, text: "buy milk" });
  tx.oncomplete = () => console.log("saved");
};
⚠ Everything here is event-based, not promise-based The raw API predates promises entirely — onsuccess/ onerror callbacks, not await. Most real projects reach for a small wrapper library (idb is the common one) that wraps the same operations in real promises, rather than hand-rolling callback plumbing for every query.

The Cache API

const cache = await caches.open("v1");
await cache.put("/api/products", new Response(JSON.stringify(products)));

const cached = await cache.match("/api/products");
if (cached) console.log(await cached.json());

Stores actual Request/Response pairs — built specifically to cache network responses, not arbitrary data. Used directly, or (far more often) driven from inside a service worker's fetch handler below, intercepting real network requests and serving a cached response instead of hitting the network at all.

Service Worker lifecycle

A service worker is a script that runs separately from any page, sitting between the app and the network — it can only be installed over HTTPS (or localhost), and it goes through a fixed sequence of events every browser follows the same way.

EventHappens
installonce, the first time — the usual place to pre-cache the app shell
activateonce this version takes control — clean up old caches from a previous version here
fetchfires for every network request the page makes, for as long as the worker is active — this is the hook that makes offline actually work
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open("v1").then((cache) => cache.addAll(["/", "/app.js", "/style.css"]))
  );
});

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => cached || fetch(event.request))
  );   // cache-first: serve cached if we have it, otherwise hit the network
});

event.waitUntil() tells the browser "don't finish this lifecycle step until this promise settles" — without it, the worker could finish installing before the cache is actually populated. event.respondWith() is the equivalent for fetch: it hijacks the response the page will actually receive.

PWA basics

{
  "name": "My App",
  "short_name": "MyApp",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#1f3a73",
  "background_color": "#fffdf6",
  "icons": [{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }]
}

A manifest.json, linked from the page's <head>, plus a registered service worker, is the entire minimum bar for "installable" — the browser's own criteria, not a separate framework. display: "standalone" is what drops the browser chrome (address bar, tabs) once installed, so it opens looking like a real app rather than a browser tab.

Being online-aware

if (!navigator.onLine) {
  showOfflineBanner();
}
window.addEventListener("online", () => console.log("back online"));
window.addEventListener("offline", () => console.log("connection lost"));
⚠ navigator.onLine is optimistic, not reliable It reports whether the device has a network connection at all — wifi connected, but the actual internet down, still reads true. Treat it as a hint for UI (show a banner), never as proof a request will actually succeed; still handle a failed fetch regardless of what navigator.onLine said a moment earlier.

Background sync is the piece that closes the loop: register a sync event from the page (registration.sync.register("send-queued-posts")), and the browser holds onto it, firing the service worker's sync event once connectivity actually returns — even if the page itself has been closed the whole time. It's how "your message will send once you're back online" gets implemented for real, instead of just queuing in memory and hoping the tab stays open.

Storage, side by side

localStorage sessionStorage IndexedDB Cache API Cookies
Size~5-10MB~5-10MBdisk-space limiteddisk-space limited~4KB each
Data shapestringsstringsstructured objects, binaryRequest/Response pairsstrings
Sync or asyncsyncsyncasyncasyncsync (via document.cookie)
Survives tab close?yesnoyesyesyes, until expiry
Sent to the server automatically?nonononoyes, every matching request

That last row is the one with real consequences — it's exactly why the security chapter's CSRF discussion is about cookies specifically and not localStorage: only a cookie rides along on a request automatically, whether your own JavaScript asked for that or not.

Practice this layer

Opens in the editor — write it, run it, and check it against real tests.

A cache-first lookup, the shape a service worker uses2 tests · intermediatePick the right storage for the job4 tests · intermediate
←previousReal-time connections↑ CovernextEngine & memory→