Part 1 · 9 chapters · ~60 min

Algorithms You Write

The algorithms a frontend engineer actually writes are few, and each is a scan replaced by a lookup, a batch, or a bound. This part is debounce and throttle as timers, virtualisation as prefix sums and a binary search, list diffing with keys and the LIS trick, LRU and LFU with O(1) eviction, search with inverted indexes and ranking, sorting at UI scale, graph traversal for dependencies and trees, and scheduling with a heap.

6

Debounce, throttle, and coalescing

the question

"Search fires on every keystroke and the list stutters. Debounce or throttle, and what is the difference under the hood?"

Both are one timer and a closure. Debounce resets the timer on every call and fires once the calls stop; throttle fires at most once per interval and delivers the last call at the end. The complexity is O(1) per call for both; the choice is about what the user is doing: debounce for "when they stop" (typing a query), throttle for "while they do it" (scrolling, dragging). For visual work, the throttle interval should be the frame: coalesce to one call per requestAnimationFrame.

code
// debounce: run after the input goes quiet for `wait` ms. one timer, reset on every call
function debounce(fn, wait) {
  let timer = null, lastArgs
  const run = () => { timer = null; fn(...lastArgs) }
  const d = (...args) => { lastArgs = args; clearTimeout(timer); timer = setTimeout(run, wait) }   // O(1) per call; one pending timer
  d.cancel = () => { clearTimeout(timer); timer = null }
  d.flush = () => { if (timer) { clearTimeout(timer); run() } }
  return d
}
// cost model: the closure holds lastArgs until it fires (retention of the last payload). cancel on unmount.

// throttle: run at most once per `wait` ms, with the trailing call delivered. a timestamp and one timer
function throttle(fn, wait) {
  let last = 0, timer = null, lastArgs
  return (...args) => {
    lastArgs = args; const now = Date.now(); const remaining = wait - (now - last)
    if (remaining <= 0) { clearTimeout(timer); timer = null; last = now; fn(...args) }           // leading edge
    else if (!timer) timer = setTimeout(() => { last = Date.now(); timer = null; fn(...lastArgs) }, remaining)  // trailing edge
  }
}
// rAF throttle for visual work: coalesce to one call per frame; the browser course part 6 has why
function rafThrottle(fn) { let id = null, a; return (...args) => { a = args; if (id === null) id = requestAnimationFrame(() => { id = null; fn(...a) }) } }
// which: debounce for "when they stop" (search-as-you-type, resize end, autosave); throttle for "while they do it" (scroll position, drag, progress)
the details that matter
  1. Retention: the closure holds the last arguments until it fires. For a large payload (a file, a DOM node) that is retention of the payload for wait ms, and forever if the component unmounts without cancelling. Expose cancel; call it on unmount.
  2. Leading and trailing edges: debounce normally fires on the trailing edge (after quiet). A leading-edge option fires immediately on the first call and then suppresses: right for "submit" buttons. Throttle with both edges gives immediate feedback plus a final state.
  3. Timers are host-scheduled (the browser course, part 6): clamped to 4 ms when nested, throttled to 1 s in background tabs, coarsened in some privacy modes. A 16 ms throttle via setTimeout is not frame-aligned; requestAnimationFrame is.
  4. Async work inside: a debounced search that awaits a fetch can resolve out of order (an older query's response arriving after a newer one). Pair the debounce with cancellation (AbortController per call, abort the previous) or a sequence number that drops stale responses.
  5. Scheduler-aware variants: React's useDeferredValue and startTransition are debounce-like at the render level (part 5): the input updates immediately, the expensive derived render is deferred and interruptible. The scheduler.postTask API gives priority-based deferral at the platform level.
the sizes
Keystrokes arrive every 50 to 150 ms; a debounce of 150 to 300 ms is invisible to the user and cuts work by 5 to 10×. Scroll events arrive at up to the display's refresh rate; an rAF throttle makes "handle every event" cost at most one handler per frame. Neither changes the algorithm inside; both change how many times it runs.
7

Virtualisation and windowing

A list of 100,000 rows cannot be 100,000 DOM nodes (the browser course's layout and paint costs scale with nodes). Virtualisation renders the thirty that are visible and positions them inside a container of the full height. The algorithm is a prefix-sum array and a binary search; the engineering is in measurement and scroll compensation.

the algorithm
  1. Fixed heights: start = floor(scrollTop / rowHeight), end = start + ceil(viewportHeight / rowHeight). O(1). Total height n × rowHeight.
  2. Variable known heights: prefix sums offsets[i] (O(n) once; a Float64Array). start by binary search for scrollTop (O(log n)); end by walking forward until offsets[i] >= scrollTop + viewportHeight (O(visible)). Total height offsets[n].
  3. Overscan: render a few extra rows above and below so fast scrolling does not show blank space before the next render commits. 3 to 10 rows; more on slow devices.
  4. Positioning: each rendered row absolutely at offsets[i], or the whole window translated by offsets[start] with rows in flow. Translation is one compositor-friendly transform; absolute positioning is simpler to reason about.
  5. Keys: rows keyed by item id so the diff between windows (chapter 3) touches only the rows that entered or left.
unknown heights
  1. Estimate each height (an average, or a per-type guess); build offsets from estimates.
  2. Render the window; measure rendered rows in one batch (getBoundingClientRect after the commit, or a ResizeObserver); correct the heights array; rebuild offsets (O(n), fine) or update a Fenwick tree (O(log n) per change).
  3. Compensate: if a row above the viewport was mis-estimated by Δ, the content under the cursor shifted by Δ; adjust scrollTop by Δ in the same frame so the visible content stays put. Missing this is the jitter in bad virtual lists.
  4. Anchor: scroll anchoring (the browser does it for normal content) does not apply to absolutely positioned rows; you are the anchor.
when not to virtualise
  1. Under ~500 simple rows: the DOM can hold them; content-visibility: auto on each row lets the browser skip rendering offscreen ones with no JavaScript.
  2. Find-in-page and accessibility: virtualised rows are not in the DOM, so Ctrl+F and screen readers cannot see them. Provide search and announce counts.
  3. Variable, unmeasurable heights with frequent changes: the measure-correct-compensate loop can thrash. Fix heights per row type instead.
VIRTUALISATION AS A BINARY SEARCH
which rows are visible at scrollTop = 48,213
swipe the figure sideways, or tap expand for full screen
1/6
prefix sums
Heights: an array of 100,000 numbers (variable: 24, 48, 36, …). Prefix sums: offsets[i] = sum of heights[0..i): offsets[0] = 0, offsets[1] = 24, offsets[2] = 72, … Built once in O(n), stored as a Float64Array. The total height is offsets[n], which sizes the scroll container.
8

Diffing lists: LCS, keys, and the LIS trick

Every framework, every list sync, and every "apply this update to that collection" is a diff between two sequences. The optimal diff is a longest-common-subsequence problem and costs O(n × m); nobody pays that in a UI. With stable keys, the diff becomes two linear passes plus an O(k log k) move computation, and that is what the reconcilers do.

the keyed algorithm
  1. Index the target by key: Map<key, index>. O(m).
  2. Walk the source: for each item, look up its key. Absent → delete. Present → record the target index. O(n). Target keys never seen → insert.
  3. Moves: the kept items, in source order, have target indices. The longest increasing subsequence of those indices is the largest set that can stay in place; everything else moves. LIS by patience sorting is O(k log k). React uses a cheaper heuristic (track the highest target index placed so far; any item with a lower index moves) that is O(n) and occasionally makes one extra move.
  4. Apply: deletions, then moves and insertions in target order, so that each operation's reference node ("insert before X") already exists.
keys, precisely
  1. Stable: the same logical item gets the same key across renders. An id from the data. Not an index; not Math.random(); not a key that changes when the item is edited.
  2. Unique within the list: duplicates make the index lookup ambiguous; frameworks warn and then behave unpredictably.
  3. Index keys are position pairing: fine for a static list that only appends; wrong for anything that inserts, removes or reorders, because every subsequent position "changes" and component state attached to those positions is lost or misattributed.
  4. Composite keys (`${type}:${id}`) when ids collide across types; the string concatenation is one cons node per key (the JS course, part 6), fine at list sizes.
beyond the UI
  1. Text and file diffs: Myers' algorithm, O((n + m) × d) where d is the edit distance: fast when the sequences are similar, which they usually are. git, and every code review tool.
  2. DOM morphing: libraries that diff a new HTML string against the live DOM (morphdom, idiomorph, Turbo) use keys where ids exist and position heuristics otherwise.
  3. Your own syncs: reconciling a fetched list with a local one, applying server deltas, keeping two models aligned. The same three steps; the Map is the whole trick.
DIFFING TWO LISTS
the LCS you refuse to compute, and the keyed shortcut
swipe the figure sideways, or tap expand for full screen
1/6
the LCS
A = [a, b, c, d, e], B = [a, c, b, f, e]. The optimal edit script: keep a, move b after c (or c before b), delete d, insert f, keep e. Finding "optimal" in general is LCS: a table of (n+1)(m+1) cells, each O(1): 36 cells here, 25 million for two 5,000-item lists. Not per frame.
9

Caches: LRU, LFU, and bounds

A cache without a bound is a leak (the JS course, part 13). A bound needs an eviction policy, and the policy needs a data structure that makes eviction O(1), or the cache's bookkeeping costs more than the recomputation it saves.

the structures
  1. LRU with a Map: insertion order is recency; get deletes and re-sets; set at capacity deletes the first key. Ten lines, O(1) amortised, the right default. The deletes leave holes that the Map's rehash compacts (the JS course, part 12).
  2. LRU with a hash and a doubly linked list: the textbook version; O(1) without holes; better constants under heavy churn; forty lines.
  3. LFU: counts per entry; a map from frequency to a list of entries; a minimum-frequency pointer. O(1) for all operations with the right structure; naive versions (scan for the minimum) are O(n) per eviction.
  4. TTL: a timestamp per entry checked on read; expired entries removed lazily on access or by a periodic sweep. Combine with LRU for a bound on both age and size.
  5. Size-aware: a size function per entry (bytes of an image, length of a string) and a byte budget instead of an entry count. Eviction loops until under budget.
choosing
  1. Recency (LRU) for the usual UI case: what the user just looked at, they will look at again. Viewed records, rendered thumbnails, fetched pages, computed layouts.
  2. Frequency (LFU) when a few keys are hot forever and a scan of cold keys should not evict them. Rare in a UI; common in servers.
  3. Size one is a real cache: memoised selectors (reselect) keep the last input and output; a size-one LRU is a single equality check.
  4. Key by object identity with a WeakMap when the cached value should die with its key (per-node layout caches, per-request derived data). No eviction policy needed; the collector is the policy.
worked numbers
what a bound costs per operation:

  LRU (Map)          get: 2 hash ops (delete + set)  · set: 2 to 3  · evict: 1   → ~50 to 100 ns
  LRU (list)         get: 1 hash op + 4 pointer writes  · evict: 1 + 2         → ~30 to 60 ns
  LFU (O(1))         get: 2 hash ops + relink  · evict: 1                     → ~80 to 150 ns
  "scan for oldest"   evict: O(n)                                               → the quadratic in a cache

any of the first three is fine. the fourth is what an object-with-timestamps gives you by default.
LRU AND LFU
the structures that make eviction O(1)
swipe the figure sideways, or tap expand for full screen
1/6
LRU with a Map
LRU with a Map: get(k): if present, delete and re-set (moves the entry to the end of insertion order); set(k, v): if at capacity, delete map.keys().next().value (the oldest); then set. Every operation is O(1) amortised because Map iteration order is insertion order (JS course part 12).
10

Search and ranking

Search-as-you-type over an in-memory collection is three problems: finding candidates (filtering), ordering them (ranking), and doing both within a keystroke. The naive linear filter is fine to about ten thousand short records with a debounce; past that, an index turns O(n) per query into O(hits).

code
// search over 50,000 records, three tiers of effort
// 1. linear filter: O(n × query) per keystroke. fine to ~10k short records; debounce it
const hits = records.filter(r => r.name.toLowerCase().includes(q))

// 2. inverted index: token → Set of record ids. build O(total tokens); query O(hits) per token, intersected
const index = new Map()                                   // 'ade' → Set{12, 977, …}
for (const r of records) for (const t of tokenize(r.name)) (index.get(t) ?? index.set(t, new Set()).get(t)).add(r.id)
const hits = intersect(tokenize(q).map(t => index.get(t) ?? new Set()))   // smallest set first

// 3. prefix search: a trie over tokens, or a sorted token array with two binary searches for the range [q, q + '￿')
const tokens = allTokens.sort()                           // once
const lo = lowerBound(tokens, q), hi = lowerBound(tokens, q + '￿')   // O(log n) each; tokens[lo..hi) share the prefix

// ranking: score = Σ (field weight × match quality): exact > prefix > substring > fuzzy; recency and popularity as tie-breakers
// fuzzy (part 9): bitap for short patterns, edit distance ≤ 2 via a precomputed deletion index (SymSpell) for speed
// libraries: MiniSearch, FlexSearch, Fuse.js (fuzzy, slower), Lunr. all build one of the above
finding candidates
  1. Linear filter: includes per record per query. O(n × |record|). Up to ~10k records of a few words: single-digit milliseconds; debounced, invisible. The right first version.
  2. Inverted index: token → set of record ids. Build once per data change in O(total tokens); query by intersecting the sets for the query's tokens, smallest set first. O(hits) per query. The structure under every search library.
  3. Prefix search: a trie (O(prefix length) to find the node, then enumerate), or a sorted token array with two binary searches giving the range of tokens sharing the prefix (O(log n) each; no extra structure beyond a sort). The sorted array is usually enough and far smaller than a trie.
  4. Substring search: harder: an inverted index of n-grams (every 3-character window → ids) handles "contains" queries at the cost of index size. Or accept linear scan over the candidate set from a coarser index.
  5. Fuzzy: typo tolerance. Edit distance per candidate is O(|a| × |b|); too slow across a whole collection. Candidate generation first (n-grams, or a deletion index like SymSpell), then exact scoring on the candidates. Part 9 has the string algorithms.
ranking
  1. Match quality: exact match > prefix > word-boundary > substring > fuzzy, with field weights (title over body). A score per hit; sort by score (a partial sort or a heap when only the top 20 are shown).
  2. Tie-breakers: recency, popularity, the user's own history (a small LRU of selected items boosts them).
  3. Highlighting: record the match positions during scoring; do not re-search the text to highlight it.
  4. Stability: keep results stable as the user types (the same item should not jump around); a stable sort with a deterministic tie-breaker.
the sizes
An inverted index over 50,000 records with 5 tokens each is 250,000 Set entries: a few megabytes, built in tens of milliseconds, queried in microseconds. Build it in a worker if the build blocks a frame; query it on the main thread.
11

Sorting at UI scale

code
// sorting at UI scale: Array.prototype.sort is TimSort (stable, O(n log n), adaptive: near-sorted input is ~O(n))
rows.sort((a, b) => a.price - b.price)                          // 100k rows: ~15 ms. 1k rows: ~0.1 ms
// the comparator runs n log n times: ~1.7M calls for 100k. keep it allocation-free and monomorphic:
rows.sort((a, b) => a.name.localeCompare(b.name))              // localeCompare per call: ~10× slower than a numeric compare
const collator = new Intl.Collator('en', { sensitivity: 'base' })
rows.sort((a, b) => collator.compare(a.name, b.name))          // one collator; ~3× faster than localeCompare per call
// precompute the sort key once when it is expensive (a normalised string, a parsed date), then compare keys:
const keyed = rows.map(r => [normalise(r.name), r]); keyed.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
// multi-column: compare the first field, then the next on ties. stable sort means: sort by the secondary first, then the primary also works
// when NOT to sort: on every keystroke over 100k (sort once, filter the sorted array: filter preserves order); when only top-k is shown (a heap)
// toSorted() returns a copy (ES2023); sort() is in place and returns the same array (a mutation React will not see)
what the engine gives you
  1. Array.prototype.sort is TimSort (V8 since 2018): stable, O(n log n) worst case, O(n) on already-sorted or nearly-sorted input (it detects runs). Implemented in Torque with fast paths for packed Smi and double arrays with the default comparator.
  2. The comparator is called ~n log n times. For 100,000 rows, about 1.7 million calls. A comparator that allocates (builds a string, creates a Date) or calls into ICU (localeCompare) dominates the sort.
  3. Default comparator converts to strings and compares UTF-16 units: [10, 9, 1].sort() is [1, 10, 9]. Always pass a comparator for numbers.
  4. toSorted, toReversed, with: copying variants; the right ones for immutable state (a sorted-in-place array is the same reference and React will not re-render).
the techniques
  1. Precompute keys (decorate-sort-undecorate) when the key is expensive: normalise the string once, parse the date once, then sort pairs by the precomputed key. O(n) key computations instead of O(n log n).
  2. Intl.Collator once, reused, instead of localeCompare per call: several times faster; same results.
  3. Multi-column: a comparator that checks fields in order; or sort by the secondary key first and then the primary (stability makes this correct), which is sometimes clearer.
  4. Top-k: when only the first page is shown, a min-heap of size k over the stream is O(n log k) and no full sort; for k = 20 over 100k that is ~5× less work and no 100k-element copy.
  5. Sort once, filter many: filtering preserves order; keep the master array sorted and filter per keystroke instead of sorting per keystroke.
  6. Typed arrays: Float64Array.prototype.sort with no comparator is a numeric sort in native code; for pure numeric columns, sort an index array by the typed column.
the sizes
1,000 rows: ~0.1 ms with any comparator. 100,000 rows: ~15 ms numeric, ~150 ms with localeCompare, ~50 ms with a Collator, ~20 ms with precomputed keys. Past 100k, sort in a worker and transfer the index array back.
12

Graphs: dependencies, trees, and traversal

code
// graph traversal in a UI: dependencies (build graphs, form field dependencies, derived state), layout (trees), routing
// adjacency as Map<node, Set<node>>; BFS with a queue (shortest path in hops), DFS with a stack or recursion (reachability, ordering)
function topoSort(deps) {                                  // Kahn's algorithm: O(V + E). detects cycles
  const indeg = new Map(), order = []
  for (const [n, outs] of deps) { indeg.set(n, indeg.get(n) ?? 0); for (const m of outs) indeg.set(m, (indeg.get(m) ?? 0) + 1) }
  const q = [...indeg].filter(([, d]) => d === 0).map(([n]) => n)
  while (q.length) { const n = q.shift(); order.push(n); for (const m of deps.get(n) ?? []) { indeg.set(m, indeg.get(m) - 1); if (indeg.get(m) === 0) q.push(m) } }
  if (order.length !== indeg.size) throw new Error('cycle')
  return order
}
// where: computed/derived state (recompute in topological order so each node sees fresh inputs: signals do this incrementally),
// module graphs (part 8's tree shaking is reachability from entries), component trees (DFS = render order), undo graphs
// q.shift() is O(n) on arrays; for large graphs use an index pointer (let head = 0; q[head++]) to keep BFS O(V + E)
graphs you already have
  1. Derived state: computed values that depend on other computed values form a DAG. Recompute in topological order (Kahn's algorithm, O(V + E)) so each node sees fresh inputs exactly once. Signal libraries do this incrementally: a change marks dependents dirty along the edges and recomputes lazily on read, in dependency order.
  2. Component trees: a tree is a graph; render is a depth-first traversal; React's fiber (part 7) is a linked representation (child, sibling, return pointers) that makes the traversal resumable without a recursion stack.
  3. Module graphs: imports are edges; evaluation order is a post-order DFS (the JS course, part 11); tree shaking is reachability from entry points (part 8).
  4. Form dependencies: field B's options depend on field A's value; validation of C depends on A and B. A DAG; a cycle is a bug to detect at definition time.
  5. Routing and navigation: a route tree; matching is a trie walk over path segments.
  6. Layout constraints: in constraint-based layouts (Cassowary, as in Apple's Auto Layout), a graph solved iteratively; in CSS, grid and flex are not general graphs but their algorithms (part 3) have a dependency structure (track sizes depend on item sizes depend on track sizes) resolved by fixed passes.
the traversals
  1. BFS with a queue: shortest path in edges; level-order; "everything within k hops". Use an index pointer, not shift(), to keep it O(V + E).
  2. DFS with recursion or an explicit stack: reachability, ordering (pre-order for render, post-order for evaluation), cycle detection (a node on the current stack reached again).
  3. Topological sort: Kahn's (BFS over in-degrees) or DFS post-order reversed. Both O(V + E); both detect cycles.
  4. Dijkstra with a heap (chapter 8): weighted shortest paths. Routing on a map, cost-based layout, "cheapest path through a state graph".
  5. Union-find: connected components with near-O(1) union and find; for "are these two nodes connected" over many queries (grouping selections, merging regions).
the sizes
UI graphs are small: hundreds of derived values, thousands of components, a few thousand modules. O(V + E) is instant; the thing to avoid is recomputing the whole graph when one node changes, which is what incremental (dirty-marking) evaluation solves.
13

Scheduling with priority queues

A scheduler has work items with priorities or deadlines and must always run the most urgent one next, while letting new items arrive at any time. That is a priority queue, and the structure that makes it O(log n) per operation with no pointers is a binary heap in an array.

the heap
  1. Layout: an array; index 0 is the minimum; children of i at 2i+1 and 2i+2; parent at (i-1)>>1. The tree is always complete, so no pointers and no balancing.
  2. push: append, then sift up (swap with the parent while smaller). O(log n).
  3. pop: take the root, move the last element to the root, sift down (swap with the smaller child while larger). O(log n).
  4. peek: the root. O(1).
  5. Numeric keys: a Float64Array for the keys and a parallel array for the payloads is cache-friendly and allocation-free; for ~30 lines you have a scheduler core.
schedulers that use one
  1. React's scheduler package: a taskQueue heap by expiration time and a timerQueue heap by start time. The work loop pops the most urgent task, runs its callback until the 5 ms slice is used (shouldYield), and re-queues a continuation if the callback returned one. Priorities map to expiration offsets (Immediate: −1, UserBlocking: 250 ms, Normal: 5 s, Low: 10 s, Idle: never), so a starved task eventually becomes the most urgent. Part 5 puts the lane model on top.
  2. Node's timers: a priority queue of timer lists keyed by duration, each list a linked list in insertion order: O(1) insert for the common case of many timers with the same duration; the queue of lists is a heap.
  3. The browser's task sources: not a single heap; per-source queues with host-defined priorities and the Prioritized Task Scheduling API (scheduler.postTask with user-blocking, user-visible, background) exposed to pages. Part 5.
  4. Your own: a request queue that runs the most recently requested image first (LIFO-ish priority), a job queue with deadlines, an animation system picking the next keyframe time, a game loop's event list.
related structures
  1. Deadline scheduling (EDF): a min-heap by deadline; always run the earliest deadline; optimal for single-processor feasibility.
  2. Aging: raise the priority of waiting items over time so nothing starves; React's expiration times are aging in disguise.
  3. Cooperative yielding: the scheduler cannot pre-empt; tasks must return to the loop. The slice length (5 ms in React, up to 50 ms before the long-task threshold) is the trade between throughput and responsiveness.
A PRIORITY QUEUE, AS A HEAP
scheduling tasks by deadline in O(log n)
swipe the figure sideways, or tap expand for full screen
1/6
the invariant
The heap invariant: every parent's key is <= its children's (a min-heap). Stored in an array: index 0 is the root; children of i are 2i+1 and 2i+2; parent of i is (i-1)>>1. No pointers; the shape is always a complete tree.
14

Reading your own algorithms: tools

QuestionToolWhat it shows
Is this loop quadratic?A scaling test at n, 2n, 4n (part 0)×2 linear, ×4 quadratic
How often does the handler run?console.count in the handler, or the Performance panel's event countWhether a debounce or throttle is needed and what interval
Is the virtual list measuring correctly?Rendering → Layout Shift Regions; a log of estimate vs measured per rowJitter sources; estimate quality
Is the diff touching only what changed?React DevTools Profiler (components rendered, why); Rendering → Paint flashingWhole-list re-renders from missing or unstable keys
Is the cache bounded and hitting?Hit/miss counters; heap snapshot comparison (JS course part 5)Hit rate; growth over time
Is the search index worth it?Time per query at the real collection size, linear vs indexed, in a worker vs mainThe crossover for your data
Is the comparator the sort's cost?CPU profile during a sort; count comparator callslocaleCompare or allocation inside the comparator
Is the derived-state graph recomputing too much?Instrument compute functions with counters per changeNodes recomputed per change versus nodes actually affected
Is the scheduler starving something?Long tasks in the Performance panel; a log of queue depth and wait timesSlice length and priority mapping problems
the pointer
Parts 2 to 9 are the algorithms the platform runs for you. Part 2 starts with the CSSOM: selector matching, the bloom filter, and invalidation sets, which is the "unscoped invalidation" quadratic from part 0 explained from the inside. Each remaining part names the browser or JS course chapter where the same machinery is drawn as mechanism rather than algorithm.