FullStack Course LogoFullStack Course

JavaScript and Browser

JavaScript and Browser Prerequisites Complete 031–65 and understand modules, closures, promises, Fetch, the DOM, and Web Storage. Topics - prototypes, private fields, iterators, generators, and async iterators; - memory

Prerequisites

Complete 031–65 and understand modules, closures, promises, Fetch, the DOM, and Web Storage.

Topics

  • prototypes, private fields, iterators, generators, and async iterators;
  • memory retention, garbage collection concepts, and CPU profiling;
  • Web Workers, structured cloning, and cancellation protocols;
  • IndexedDB, service workers, Cache API, offline conflict policy, and cache invalidation;
  • Streams, WebSockets, WebRTC, observers, and Web Components;
  • critical rendering path, layout, paint, compositing, scheduling, and requestAnimationFrame.

Investigation

Build a worker-backed search screen with an IndexedDB cache and an offline fallback. Measure main-thread work, define stale-data behavior, and test worker failure, cache version changes, reconnects, and duplicate messages.

Interview checkpoint

Explain why async does not move CPU work off the main thread, how a service worker differs from a Web Worker, and when cache invalidation is safer than indefinite freshness.

Worked browser-platform lab

Iterator and stream boundaries

Use an iterator for synchronous pull-based values and an async iterator for values that may require waiting. A ReadableStream adds queue accounting and cancellation:

js
async function collect(stream) {
  const values = [];
  for await (const value of stream) values.push(value);
  return values;
}

const stream = new ReadableStream({
  pull(controller) {
    controller.enqueue(controller.desiredSize > 0 ? "next" : "held");
    controller.close();
  },
});
collect(stream).then((values) => console.assert(values.length === 1));

Do not use desiredSize as a byte quota or enqueue forever. Test a slow consumer, reader.cancel(), source errors, and cleanup in finally.

Service-worker lifecycle and cache policy

Registration, installation, activation, and control are different events. install is the place to populate a versioned precache; activate is the place to delete old caches; a newly installed worker normally waits until older controlled pages finish unless it calls skipWaiting(), and control may begin after a navigation unless clients.claim() is used.

js
const CACHE = "app-shell-v3";
const ASSETS = ["/", "/app.js", "/styles.css"];

self.addEventListener("install", (event) => {
  event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(ASSETS)));
});
self.addEventListener("activate", (event) => {
  event.waitUntil(caches.keys().then((keys) => Promise.all(
    keys.filter((key) => key !== CACHE).map((key) => caches.delete(key)),
  )));
});
self.addEventListener("fetch", (event) => {
  if (event.request.method !== "GET") return;
  event.respondWith(fetch(event.request).catch(() => caches.match(event.request)));
});

Cache is not HTTP cache validation: a cached response can be stale, opaque, or a response for the wrong request unless the policy checks it. Test first install, failed precache, an old worker controlling an open tab, activation after closing the old tab, cache cleanup, offline navigation, non-GET requests, and a server response that must never be served stale. A Web Worker performs application computation; a service worker is an origin-scoped event-driven network/lifecycle proxy with no DOM access.

IndexedDB and rendering

Use one readwrite transaction for a record plus its audit entry, assert oncomplete, and abort on any request error. In a rendering trace, separate input, scripting, style, layout, paint, and compositing; batch geometry reads before writes and use requestAnimationFrame for the latest visual state. Test a forced-layout loop under CPU throttling and verify the Performance panel rather than relying on wall-clock logs.

Interview questions

  1. Why does a new service worker not immediately control every open page? Lifecycle waiting and navigation/control boundaries preserve consistency for existing clients.
  2. Why can cache.match() be unsafe as a universal offline strategy? It may return stale content, omit a cache miss distinction, and bypass freshness/authentication policy.
  3. What does backpressure protect? It bounds queued work by making a producer respect consumer capacity; it does not make an upstream service cancelable.
  4. What proves an IndexedDB write committed? The transaction's complete event, not an individual request's success event.

References

Complete event-rate utilities

Debounce delays work until a quiet period. This version defaults to trailing execution, preserves the latest this and arguments, and exposes cancel() and flush() for teardown and immediate delivery. leading: true runs at the start; trailing: false prevents the later call.

js
function debounce(fn, wait, { leading = false, trailing = true } = {}) {
  let timer = null;
  let lastArgs;
  let lastThis;
  let result;
  const invoke = () => {
    const args = lastArgs;
    const receiver = lastThis;
    lastArgs = lastThis = undefined;
    result = fn.apply(receiver, args);
    return result;
  };
  const wrapped = function (...args) {
    const shouldLead = leading && timer === null;
    lastArgs = args;
    lastThis = this;
    clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
      if (trailing && lastArgs) invoke();
    }, wait);
    if (shouldLead) invoke();
    return result;
  };
  wrapped.cancel = () => {
    clearTimeout(timer);
    timer = null;
    lastArgs = lastThis = undefined;
  };
  wrapped.flush = () => {
    if (timer === null) return result;
    clearTimeout(timer);
    timer = null;
    return trailing && lastArgs ? invoke() : result;
  };
  return wrapped;
}

let searchCount = 0;
const search = debounce((term) => { searchCount += 1; return term; }, 20);
search("a"); search("ab");
console.assert(searchCount === 0);
search.flush();
console.assert(searchCount === 1);

Throttle limits invocation to at most once per interval. This implementation supports leading and trailing calls and provides cancel(); the trailing call uses the latest arguments:

js
function throttle(fn, wait, { leading = true, trailing = true } = {}) {
  let lastTime = 0;
  let started = false;
  let timer = null;
  let lastArgs;
  let lastThis;
  const invoke = (time) => {
    lastTime = time;
    const args = lastArgs;
    const receiver = lastThis;
    lastArgs = lastThis = undefined;
    fn.apply(receiver, args);
  };
  const wrapped = function (...args) {
    const now = performance.now();
    if (!started && !leading) { started = true; lastTime = now; }
    const remaining = wait - (now - lastTime);
    lastArgs = args;
    lastThis = this;
    if (remaining <= 0 || remaining > wait) {
      clearTimeout(timer); timer = null; started = true; invoke(now);
    } else if (trailing && timer === null) {
      timer = setTimeout(() => {
        timer = null;
        if (lastArgs) invoke(performance.now());
      }, remaining);
    }
  };
  wrapped.cancel = () => {
    clearTimeout(timer); timer = null; lastTime = 0; started = false;
    lastArgs = lastThis = undefined;
  };
  return wrapped;
}

Test rate control with fake timers where possible. Assert leading-only, trailing-only, both modes, cancel(), flush(), latest arguments, receiver forwarding, and no callback after teardown. A timer does not cancel a fetch; use an abort signal separately.

Small event emitter

An emitter owns subscriptions and must define snapshot behavior when listeners remove themselves during an emit. Returning an unsubscribe function makes cleanup explicit, and once removes the wrapper before calling the listener:

js
class EventEmitter {
  #events = new Map();
  on(name, listener) {
    const listeners = this.#events.get(name) ?? new Set();
    listeners.add(listener); this.#events.set(name, listeners);
    return () => this.off(name, listener);
  }
  off(name, listener) { this.#events.get(name)?.delete(listener); }
  once(name, listener) {
    const wrapper = (...args) => { this.off(name, wrapper); listener(...args); };
    return this.on(name, wrapper);
  }
  emit(name, ...args) {
    for (const listener of [...(this.#events.get(name) ?? [])]) listener(...args);
  }
}

const bus = new EventEmitter();
let total = 0;
const unsubscribe = bus.on("add", (amount) => { total += amount; });
bus.once("add", () => { total += 10; });
bus.emit("add", 2); bus.emit("add", 3); unsubscribe(); bus.emit("add", 5);
console.assert(total === 15); // 2 + 10 + 3; final listener was removed

Decide whether listener errors stop later listeners, whether event names may be symbols, and whether emit is synchronous. Tests should cover no listeners, duplicate subscriptions, off, self-removal, once, listener order, and errors.

Interview checkpoint: utilities

  1. Compare debounce and throttle for autocomplete, resize, and scroll reporting.
  2. Why must a debouncer clear its timer during teardown?
  3. What does a trailing throttle call receive after five rapid events?
  4. Why emit over a copy of the listener set?
  5. Why does an unsubscribe function reduce event-listener leaks?
Reader page: /guide/advanced-javascript-browser