Learning Log
Continuity file. Append-only in spirit; prune only when something is genuinely resolved. Four sections: struggles, open questions, source readings, architecture decisions & revisits.
1. Concepts struggled with
Record the shape of the confusion, not just the topic. "Didn't get microtasks" is
useless in three months. "Believed await yields to the event loop the way
setTimeout does" is a diagnosis.
| Date | Module | Concept | Nature of the confusion | Resolved? | How |
|---|---|---|---|---|---|
| 2026-08-10 | fe-01 | Microtask checkpoint placement | Natural but wrong model: "checkpoint runs at the end of each task." Predicts interleaving for setTimeout(() => el.click()), which does not happen. | yes | Correct rule is stack-emptiness. Measured 4 dispatch paths. |
| 2026-08-11 | fe-02 | Leak detection statistics | Assumed a clean workload would show R² near zero. It measured 0.742 — short series make noise look correlated. Slope alone and R² alone are both insufficient. | yes | Report slope AND linearity, plus a control run. |
| 2026-08-11 | fe-02 | Harness isolation | Ran three listener variants on one page; variant 1's leak stayed alive through variants 2 and 3, so all three reported 300 detached nodes. JS-heap column stayed correct, making the table look partly plausible. | yes | Fresh page per variant. Same class as fe-01's realm reuse. |
| 2026-08-10 | fe-01 | Measurement trust | Two harness bugs produced clean, plausible, wrong data (realm reuse; per-event vs per-interaction INP). | yes | Harness now asserts liveness per trial; INP grouped by interactionId. |
2. Open questions
Questions raised but not yet answered. A question that survives three modules is a signal — either it's genuinely hard, or the mental model underneath it is wrong.
| # | Raised in | Question | Status | Answer / where it got resolved |
|---|---|---|---|---|
| 1 | fe-03 | Why does PACKED_DOUBLE sum faster than PACKED_SMI (0.60x)? Accumulator-overflow hypothesis was tested and rejected (constraining values to i % 100 kept the sum in SMI range; ordering unchanged). Leading remaining hypothesis: per-add SMI overflow checks. | open — deliberately not pursued | Magnitude is ~0.6ns/element. Settle with --print-opt-code if ever relevant. |
3. Source-code readings
Per the spec's code-reading requirement and browser-framework-internals.md §44
(Source-Code Reading Ladder). Log what you read, what you were looking for, and
what surprised you. The surprise column is the valuable one.
| Date | Repo / file | Looking for | What I found | What surprised me |
|---|---|---|---|---|
| 2026-08-10 | WHATWG HTML §8.1.7 (event loop) | when a microtask checkpoint runs | "when the JS execution context stack becomes empty" — not "at end of task" | setTimeout(() => el.click()) is its own task yet still shows no checkpoint between listeners. The discriminator is stack emptiness, not task boundary. |
| 2026-08-10 | Chrome 149, measured | rAF vs setTimeout(0) ordering | not a property of the primitives at all | Input dispatch sits immediately before the rendering steps, so rAF is ~0ms away from a click handler and a whole task away from a plain script. Order flips 40/40 either way. |
Verification log (browsers move; re-check anything older than ~6 months)
| Date | Claim | Verdict |
|---|---|---|
| 2026-08-11 | Flex/grid are slow; absolute positioning is fast | False, measured — 1.4x spread across six layout modes over 20,000 items. Absolute was SLOWEST (75.5ms, 2.4x style cost from inline left/top); flex was fastest on relayout (7.3ms). |
| 2026-08-11 | Container queries are expensive | False, measured — +1.4ms layout / +3.0ms style over 8,000 elements. The real cost is the containment constraint (container-type: inline-size implies size containment), not milliseconds. |
| 2026-08-11 | Large DOM is inherently slow to render | False, measured — identical 261,011-node DOM took 401.9ms initial layout with no containment and 7.9ms with content-visibility: auto (−98%). |
| 2026-08-11 | contain speeds up initial rendering | False, measured — contain: strict gave 398.4ms vs 401.9ms baseline. It bounds propagation (−80% on the update path), it does not skip rendering. Different tool, different problem. |
| 2026-08-11 | Specificity decides the cascade | Incomplete, measured — layer order is evaluated first: 0,1,0 in a later layer beat 1,1,0 in an earlier one. Unlayered styles form an implicit final layer. Author !important inverts layer order. |
| 2026-08-11 | minlength/maxlength apply to any value | False, measured — gated on the HTML spec's dirty value flag. el.value='short' reports valid; typing short reports tooShort. Tests that set .value pass while the real form rejects. Same class as fe-01's .click() finding. |
| 2026-08-11 | Semantic HTML is more verbose than divs | False, measured — div soup was 1.38x the markup of semantic HTML with 0 landmarks/headings/controls; div+ARIA 1.69x. |
| 2026-08-11 | Property order is the main hidden-class hazard | False, measured — alternating property order 1.14x; delete 11.93x. The folklore everyone repeats is minor; the practice everyone tolerates is the expensive one. |
| 2026-08-11 | Megamorphic access is a performance crisis | Overstated, measured — 1.9x ratio but only 2.9ns absolute. One forced layout = 1,448 megamorphic reads; one 200KB JSON.parse = 136,552. |
| 2026-08-11 | V8 closures retain the whole Context, not just referenced variables | Confirmed Chrome 149 — an unused, never-called sibling closure caused 38.17MB retention vs 0.01MB. 5744x. V8 implementation detail, not a language guarantee. |
| 2026-08-11 | performance.memory / Runtime.getHeapUsage can detect DOM leaks | False, measured — freeing 507.8KB of detached DOM moved the JS heap by 0.10MB. DOM is in Blink's C++ heap. |
| 2026-08-11 | Heap snapshots expose a per-node detachedness field (0/1/2) | Confirmed Chrome 149 — exact detached counts without name-prefix matching. |
| 2026-08-10 | await null costs 1 microtask tick | Confirmed Chrome 149 — A1 P1 A2 P2 A3 P3. The widely-cited "3 ticks" is pre-2019 and stale. |
| 2026-08-10 | Microtask chunking creates rendering opportunities | False, measured — 0 rAF frames over 400 ms vs 48 for MessageChannel, identical structure. |
| 2026-08-10 | scheduler.yield() available in Chrome | Confirmed 149; same INP as MessageChannel but 3.4× wall clock in the 200k-row lab. |
| 2026-08-10 | Workers reduce main-thread cost | Conditional — naive worker was the worst strategy measured (1502 ms structured clone). True only when the data boundary is designed. |
4. Architecture decisions & revisit queue
ADRs live in decisions/. This table is the index plus the revisit queue.
Decisions made
| # | Date | Decision | Reversibility | Link |
|---|---|---|---|---|
Revisit queue
Topics marked revisit in PROGRESS.md, with the reason and the trigger for revisiting.
| Module | Why flagged | Revisit trigger |
|---|---|---|
5. Heuristics under test
The spec lists 10 Principal heuristics and requires counterexamples and limitations for each. Fill these in as you earn them — a heuristic with no counterexample is a slogan you haven't stress-tested.
| Heuristic | Counterexample found | Limitation |
|---|---|---|
| Prefer platform primitives when the browser already solves the problem. | scheduler.yield() is the platform primitive and gave 3.4× the wall clock of a hand-rolled MessageChannel yield at identical INP (fe-01 lab). | "Prefer" ≠ "always": the primitive optimises the common case (responsiveness), and you pay when your case differs (throughput). |
| Performance problems are often scheduling problems. | Lab s5: pure serialisation cost (1502 ms structured clone), no scheduling error — the fix was a data-boundary decision, not a yield strategy. | Says nothing about where the cost is. s2 and s5 had near-identical INP from completely different causes. |
| Move state to the lowest layer that actually owns it. | ||
| Optimize critical user journeys, not vanity benchmarks. | — | Phase 1 produced three measured folklore corrections (fe-03 delete vs property order; fe-04 div soup 1.38x larger than semantic; fe-06 layout modes 1.4x with "fast" mode slowest). The reflex to build: when advice is repeated confidently and cheaply, measure before designing around it. Each had a real effect somewhere, misremembered as a general rule. |
| Every abstraction creates a future migration. | ||
| Test observable behavior, not implementation details. | ||
| E2E tests protect critical user journeys rather than duplicating unit tests. | ||
| Make invalid states difficult to represent. | — | Applies to lifecycle, not just data: AbortController at registration beats removeEventListener because the teardown handle cannot be forgotten separately (fe-02). An anonymous listener is unrepairable after the fact. |
| Accessibility is architecture, not polish. | ||
| Performance problems are often scheduling problems. | ||
| Frontend architecture frequently reflects organizational architecture. |