This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.
Existing codebase: primaworkflows.com, one 321,920-byte HTML
file, SHA-256 06a768bbβ¦. All index.html line numbers below are against that published
file. prima-visual.js line numbers are against the file the live site serves today
(8,773 bytes, SHA-256 dc78c46dβ¦). Every number is measured, and the places where I got
one wrong are left in. Production has not been swapped for the after. A same-session
A-vs-C table is still empty on purpose.
Project Overview
My site lags on my phone. It is the only page I own that strangers and clients actually load,
and it feels slow in the hand. That was the bug. I had the fix written before I opened the file:
five stacked canvases, a pile of animation loops, collapse them into one clock and move on.
Then I read the file instead of my notes about the file.
// index.html:3824
const WORLD_CANVAS_ENABLED = false;
Two of the four persistent animation loops I was about to collapse never start. The flag gates
the call that kicks them off, at :7103.
It gates only that. I want to be exact, because my first draft of this paragraph said the
flag also gated their 2D contexts, and then I checked the published file instead of my working
copy:
// index.html:3832, :3834, :3875 β no guard on any of them
const ctx = canvas.getContext('2d');
const vctx = visitorCanvas.getContext('2d');
const visitorPreviewCtx = visitorPreview.getContext('2d');
So production still creates three 2D contexts for canvases nothing
ever draws to. The loops are dead; the memory is not. Guarding those three lines is a separate
one-line-each change that is in my working tree and has not shipped, and I am not counting it
in anything below.
A third loop, the custom cursor, exits immediately on touch devices:
// index.html:6656
(function primaCursor() {
if (window.matchMedia('(hover: none)').matches) return;
Mobile is the measurement that matters here β it is a phone-facing page. So on the device I
was optimizing for, the page had one persistent JS loop, not four. The optimization I had
specified had almost nothing to collapse. If I had built it and measured, I would have seen
nothing move and had no way to tell a failed fix from a fix with no room to work.
I had written the plan from a document I wrote myself, about a file I wrote myself.
Bug Fix or Performance Improvement
The actual bug
Once I was reading rather than remembering, I found this.
// index.html:3188
function canRunWebGLHero() {
return window.matchMedia('(hover: hover)').matches; // β returns here. always.
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return false;
if (window.matchMedia('(max-width: 820px)').matches) return false;
if ((navigator.hardwareConcurrency || 4) < 4) return false;
if (navigator.deviceMemory && navigator.deviceMemory < 4) return false;
try {
const c = document.createElement('canvas');
return !!(c.getContext('webgl2') || c.getContext('webgl'));
} catch (error) {
return false;
}
}
Five guards under an unconditional return. Not one of them executes in the published build I
audited, and that build is the one live right now. I have not audited every prior deployment,
so I am not claiming a start date for it. The patch is deleting that one
return. I am not writing the fix in the past tense until the line is gone from the file a
visitor loads.
What that silently switched off:
| Guard | Intended | Actual, today |
|---|---|---|
prefers-reduced-motion |
no WebGL hero | ignored |
max-width: 820px |
skip small screens | ignored |
hardwareConcurrency < 4 |
skip weak CPUs | ignored |
deviceMemory < 4 |
skip low-RAM machines | ignored |
webgl2 / webgl probe |
skip unsupported devices | ignored |
The reduced-motion one is the part I am least comfortable with. Someone sets that preference at
the operating system level, for reasons that are theirs, and my page has been overriding it since
the day I shipped it. Not because I disagreed with them. Because a return was on the wrong
line.
The capability probe is the expensive one. A hover-capable machine with no WebGL support
downloads and compiles Three.js, tries to start, throws, and the failure lands in a
.catch(err => console.warn(...)) where nobody sees it. It pays full price for a feature it
cannot run.
Code
Exact diffs of both changed files:
gist.github.com/keniel13-ui/3c05e11202d048ad78a11ce2de215e8d
β two unified diffs against the deployed 06a768bb file, plus a README of what each hunk does
and what is deliberately excluded from the claims. Production has not been swapped, so the
before in that diff is what you get if you load the site right now.
Three scripts, and the one I was wrong about
The page declares three third-party scripts on every load:
<!-- index.html:129-131, verbatim from the live page. Note what the first one is missing. -->
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js" defer
onload="window.dispatchEvent(new Event('gsap-ready'))"></script>
The first tag has no defer. The two GSAP tags do. So Supabase is a plain script in
<head>: the browser stops building the DOM, fetches it, parses it, executes it, and only then
continues. Parser-blocking, in the head, for a library whose every call site is behind a flag
that is off.
I had a defer on that line in an earlier version of this post. I put it there. It is not in the
file β I had copied the tag out of my own working branch, where a previous change had added one,
and pasted it as though it were production. If you are going to quote code, quote the deployed
bytes.
Measured from the URLs the production page resolved during this audit. GSAP is pinned to
3.12.5. @supabase/supabase-js@2 is not a pin β jsDelivr treats @2 as a moving
major-version alias, and it resolved to 2.112.3 at the time of measurement
(x-jsd-version: 2.112.3). If you re-run this later and get a different Supabase number,
that is why:
| gzip transfer | uncompressed JS source | |
|---|---|---|
supabase-js@2 |
54,607 B | 212,199 B |
gsap.min.js |
28,200 B | 72,214 B |
ScrollTrigger.min.js |
17,681 B | 43,380 B |
| total | 100,488 B | 327,793 B |
Those are three different units and I want to keep them apart. 327,793 B is uncompressed
JavaScript source β slightly more source than the 321,920-byte HTML document itself.
100,488 B is the measured gzip transfer at the moment of the audit. I re-fetched the same
three URLs while finishing this post and got 100,148 B β the uncompressed source did not
move, the compressed transfer did. That is the moving @2 alias and CDN recompression showing
up in my own numbers, which is exactly why the alias is disclosed above rather than presented as
a pin. Both of these are cold-load figures; a repeat visitor
with a warm cache does not retransmit them.
The honest headline is the source figure: three libraries carrying more JavaScript source than
the entire document they were decorating.
Supabase is straightforward. Every one of its call sites β loadWorldState,
loadVisitorAvatars, subscribeToVisitorAvatars β is invoked only inside
if (WORLD_CANVAS_ENABLED). That flag is false. In the published build audited here
those call sites are unreachable β I have not audited every prior deployment, so I am not
claiming they never fired in the site's history. The library was still being fetched and
parsed on every cold load regardless.
GSAP is where I was wrong, and I want it on the record because it nearly became the headline
of this post.
I grepped index.html for gsap, found nothing outside the script tags, and wrote a comment
into the working file stating the libraries had zero call sites and that the gsap-ready event
had no listener. Both sentences were false. I had grepped one file on a site with several.
// prima-visual.js (live, dc78c46dβ¦):226
const { gsap, ScrollTrigger } = window;
gsap.registerPlugin(ScrollTrigger);
gsap.to({ progress: 0 }, {
progress: 1,
ease: 'none',
scrollTrigger: { trigger: document.documentElement, start: 'top top',
end: 'bottom bottom', scrub: 1.2,
onUpdate: (self) => { scrollTarget = self.progress; } },
});
// prima-visual.js:248
window.addEventListener('gsap-ready', wireGSAP, { once: true });
Line 248 is the listener I said did not exist. And scrub: 1.2 was doing real work: it is the
weight in the scroll, the reason the background trails your finger instead of snapping to it.
Deleting the tags did not remove dead code. It removed a feature, quietly, in a way no
performance score would ever have shown me.
So the honest version of this optimization is not "I deleted libraries nothing called." It is
"I used one behaviour out of 115,594 bytes of library source, and I replaced that one
behaviour."
Reading the render loop showed the replacement was smaller than expected, because half of it
already existed:
bgUniforms.uScroll.value += (scrollTarget - bgUniforms.uScroll.value) * 0.04;
There were always two easing stages. ScrollTrigger's scrub was the first. That line was the
second, and it was mine. Removing the tags took out stage one only β the page never went
un-eased, it just lost the lag.
// replaces gsap + ScrollTrigger. this is what is in the after file, not a cleaned-up retelling.
const SCRUB_SECONDS = 1.2; // the name is a lie. it is a time constant, not a catch-up.
let scrollRaw = 0, scrollEased = 0;
const readScroll = () => {
const max = document.documentElement.scrollHeight - window.innerHeight;
scrollRaw = max > 0 ? Math.min(1, Math.max(0, window.scrollY / max)) : 0;
};
readScroll();
window.addEventListener('scroll', readScroll, { passive: true });
window.addEventListener('resize', readScroll, { passive: true });
// inside the existing rAF loop:
const dt = Math.min(0.1, (now - lastFrame) / 1000); // clamped for tab-switch gaps
scrollEased += (scrollRaw - scrollEased) * (1 - Math.exp(-dt / SCRUB_SECONDS));
GSAP's scrub: 1.2 means caught up in 1.2 seconds. Mine is an exponential time constant:
at Ο = 1.2 it is 63% there at 1.2 s and needs about 3.6 s to reach 95%. If I define "caught
up" as ~95% settled β and that is my definition, not one GSAP supplies β then Ο β 0.4
puts the settling time near 1.2 s. That is a heuristic mapping, not an equivalence. I left the wrong number in the file, under a name that claims I matched
the library, because the only test that settles the feel is a thumb on a phone. I have not
done that pass yet. This approximates scrub. It does not reproduce it.
One property claim, narrowed after review, because my first version of this sentence was
wrong. The new first stage is time-based rather than a fixed per-frame lerp. But the
second stage β the * 0.04 line above, which was always mine β is still frame-based. So the
combined visual response is not refresh-rate invariant, and I am not claiming it is. Only
the stage I replaced is.
One more deletion, named so I do not take credit for it: the published page fires a WebGL intro
at load (index.html:6576, fireIntro). I had already rejected that motion as the owner. The
after-build does not call it. Clear the Lineup does not let me score a deletion as an
optimization, so it is not in any delta I will publish.
My Improvements
The standing cost nobody counted
The frozen contract I wrote counted canvases and rAF loops. It did not count CSS.
Gemini counted "15+ infinite" and I copied it into a draft before checking. Then I did the
thing this whole article is about and went and looked.
The published file has 15 infinite animation declarations: 14 written as CSS rules, one
injected from JavaScript. Of the 14 CSS rules, six target classes that appear nowhere in
the DOM and are never injected by JavaScript: .feed-pulse-dot, .feed-line, .sig-dot,
.hero-scan, .hero-console, and .scroll-cue. That last one is easy to miss because
.prima-scroll-cue is live in the hero. .scroll-cue is a different selector. There is no
id="scroll-cue" either β a scroll handler looks for one and finds nothing. They are dead
rules. They cost nothing at runtime because there is nothing to animate.
So the real standing set is eight live CSS rules plus ambientDrift, one of which is
seedBreathe β the Seed of Life β which stays. Everything else is decoration running whether
or not anyone can see it.
All eight live CSS targets sit inside <main>, so main > section[id] reaches every one. I
checked that rather than assuming it, because the selector does not match pseudo-elements
either, and I had already been wrong once about what a selector covered.
That is the third time on this one page that a count of declarations got reported as a count
of things happening: four loops of which two never start, fourteen CSS infinite rules of
which six target nothing, and my own contract that warned against exactly this in writing
before doing it twice.
The page already had an IntersectionObserver, and I nearly claimed credit for it. It is not a
brake:
obs.unobserve(entry.target); // one-shot card reveal, then it lets go
It reveals cards once and unhooks, and the whole block exits early under prefers-reduced-motion.
So a second observer, doing an actual pause:
[data-ctl-offscreen], [data-ctl-offscreen] * { animation-play-state: paused !important; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-play-state: paused !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
const obs = new IntersectionObserver(entries => {
entries.forEach(e => e.isIntersecting
? e.target.removeAttribute('data-ctl-offscreen')
: e.target.setAttribute('data-ctl-offscreen', ''));
}, { rootMargin: '200px 0px 200px 0px', threshold: 0 });
document.querySelectorAll('main > section[id]').forEach(s => obs.observe(s));
document.addEventListener('visibilitychange', () => { /* whole-tab pause */ });
One check before claiming that gates the standing cost broadly: [data-ctl-offscreen] * does
not match pseudo-elements, so an infinite animation on a ::before or ::after would keep
running off-screen. I went and looked β zero of the infinite animations in this file live on
a pseudo-element, so the selector covers them. If yours do, add ::before and ::after.
The fifteenth is ambientDrift, injected from JavaScript rather than written in the
stylesheet, which is why it survives a grep of the CSS. Its elements are appended into the
sections the observer already watches, so the descendant selector catches it.
Nothing is deleted. Fifteen infinite-animation declarations before, fifteen after. Only the
play state is gated, and only where nobody can see it. The selector is main > section[id] β eleven
sections, including #world, which is where the hero lives. Off-screen the Seed rests.
On-screen it still breathes.
Numbers
Both sides were deployed as previews in the same Vercel project, alternating A/C in one
session, lighthouse 13.4.1, mobile, default simulated throttle. That matters: Vercel injects
vercel.live/feedback.js into previews and not into production, so measuring production against
a preview compares two different pages. Both of these carry it.
What I can report
Total transfer weight is not a CPU measurement. It is a sum of response sizes, so it does not
move with host speed:
| A β before | C β after | |
|---|---|---|
| Total byte weight, median of 3 | 568,605 B | 467,962 B |
| Run-to-run spread | 1,616 B | 952 B |
| Resources | 10 | 10 |
Delta: 100,643 B β 98.3 KB, 17.7% of the before.
Worst-case A still beats best-case C by 98,993 B. Every A run transferred more than every C run,
with no overlap. And it independently agrees with the figure I got by curl-ing the three CDN
URLs directly (100,148β100,488 B) β two different methods, same answer, which is the only reason
I trust either.
What I cannot report yet
Nothing CPU-dependent. My own gate refused all six runs.
| run | benchmarkIndex | verdict |
|---|---|---|
| A-01 / A-02 / A-03 | 833 Β· 827 Β· 783 | below floor |
| C-01 / C-02 / C-03 | 623 Β· 214 Β· 660 | below floor |
The floor is 1500. A single calibration run half an hour earlier scored 1711 β then ten
back-to-back Lighthouse runs on an 8 GB laptop drove the host underneath its own threshold. So I
have LCP, TBT and main-thread numbers from those six runs, and by the rule I wrote before I saw
them, they do not go in this post.
I want to be exact about what that costs the submission: the headline performance claim here is
a 17.7% transfer reduction and a real bug fix, not a Lighthouse score. The CPU numbers need a
machine that is not also running the thing doing the measuring, and I do not have one today.
Every discarded run is published in the gist anyway β measurements.md β with its
benchmarkIndex in the same row, so you can audit the discard instead of taking my word that
one happened.
Why the before-number is not the worst one I have.
The first Lighthouse run on this page, 2026-08-17, scored 27, with LCP 10.4 s and TBT 13,310 ms.
It is the most flattering baseline available and I am not using it. Seven later runs of the
same URL, same tool version (lighthouse 13.4.1), same declared throttle, said LCP 3.67β5.48 s
and TBT 216β709 ms. Run one has never reproduced. Using it as the baseline would make any
before-and-after look dramatically larger than the reproducible baseline does. I am not putting
a multiple on that until the table below exists.
I also cannot compare a measurement taken on a quiet host to one taken on a dying one. Every
Lighthouse report carries environment.benchmarkIndex, the host speed it saw. One set of my
runs on 2026-08-17 sat at 302β1113. Every other production set that day sat at 1705β2342. No
overlap. Those slow runs were taken while the laptop was running out of memory; the session
died 27 minutes later. The tool exited cleanly and wrote valid JSON the whole time.
So, my inclusion rule β 1500 is a threshold I chose, not a Lighthouse validity boundary:
same session, both sides, benchmarkIndex β₯ 1500 on every run, or the numbers do not go in.
What I am not claiming
- The loop collapse is not the win. On mobile there was one loop. I left it alone rather than ship a change I could not attribute.
- The 173,280 bytes of inline JavaScript inside the 321,920-byte HTML document are untouched. That is the largest remaining LCP cost and splitting it is a restructure, not an optimization. Gemini called the inline JS "168 KB." Measured off the published file it is 173,280 bytes.
- Three.js is still on the page.
prima-visual.js(8,773 bytes, live) imports it after first paint. On hover-capable desktops,hero-webgl.js(12,852 bytes, live) imports the same module. One download, two call sites. Lighthouse flags 151 KB of it as unused. Gating the visual layer on mobile would flatten the page, so it stays until I have something better than a smaller file. - The
prefers-reduced-motionguard does not work today. It will work when that earlyreturnis gone. I do not get to write that sentence in the past tense until a visitor's browser actually takes the other path. - Removing
fireIntrois owner-directed deletion. It is disclosed. It is not the optimization. - This is not live on
primaworkflows.comyet. The hashes above are the before. The after is still a local preview candidate.
The part I would tell someone starting
I wrote a careful specification of my own site, from memory of my own site, and it was wrong in
three places: two loops that never ran, a whole class of animation I had not counted, and a
library I called dead that was drawing the scroll. Every check I ran was correct. Every one of
them was narrower than the claim I made from it.
The bug in the code was five guards under a return. The bug in me was the same shape one level
up: a real result, reported as if it covered more ground than it did.
Best Use of Google AI
I gave Gemini the frozen before-state and asked it to name the contention before I implemented
anything, so the diagnosis could not be written backwards from the fix. The raw stdout is
timestamped. I did not get to edit what it said.
It got the thing I had missed: competing infinite CSS keyframe animations, which my own
contract had not counted at all because I had been counting canvases and rAF. It also separated the two
hypotheses: the inline parser-blocking JS (173,280 bytes, not the 168 KB it printed) as the
more plausible cold-load and LCP cost, and the persistent animations as candidates for the
standing main-thread cost. I have not measured that split, so those are hypotheses and not
causes, and I am not going to promote them past what the table below supports. The distinction
still changed what I measured: collapsing loops and then judging the result on LCP would have
shown a real fix as a failure.
It also told me the page had no IntersectionObserver. The published HTML has one, at line
-
hero-webgl.jshas another. I checked before believing it, which is the only reason that error is a footnote instead of a paragraph in this post.
If Gemini had added nothing I would have dropped this category rather than backfill a prompt
after the fix. It added the CSS count. That was enough to keep the section, and not enough to
let it write the article.
Top comments (10)
On the second half of that, a profiler reports the code that ran and coverage reports the code that did not, which is the same question asked from the other side. The five guards under that unconditional return sit in a block that never executes, so a coverage run should come back marking them unexecuted even though the function around them is hot, and DevTools has a Coverage panel that does this against a single HTML file with no build step. It will not tell you a line is unreachable in principle, only that it did not run in the session you recorded, so the read is still what confirms it. What changes is that you are reading a handful of flagged lines instead of the whole file.
coverage catches two of my three. the loops that never ran come back unexecuted. the library i called dead comes back covered, which kills that claim from the other direction. the one it misses is the class of animation i never counted, because an execution report cannot tell you a category exists that you did not think to look for. that one was a completeness failure in my spec, not a reachability question.
and this case is unusually friendly to your caveat. the five guards sit under an unconditional return inside the same function, so the read after the flag is three lines up rather than a reachability argument across the program. session rather than principle costs almost nothing here.
where i want to push is the guard that runs and never fires. the devtools panel is byte coverage, so a line that executed reads green whether or not the condition was ever true. branch coverage does ask that, and would show the untaken side. but branch coverage means instrumentation, which is the build step your suggestion was specifically avoiding.
so on a single html file the cheap tool answers did it run, and the tool that answers did it do anything costs the thing that made the cheap one attractive.
if canRunWebGLHero had been reachable and the reduced motion query simply never matched in my session, byte coverage says covered, the guard protects nobody, and nothing in the report looks wrong. thats the residue after the dead blocks are gone.
Byte coverage and branch coverage are not the only two points on that line. A conditional breakpoint carrying the guard's own expression pauses only when the predicate is true, so a session where it never pauses is the negative result you are after, and a logpoint on the same line prints the evaluated value on every pass without stopping anything. Both are set in the Sources panel against the inline script, so the file stays untouched and there is still no build step. It is session-scoped exactly like coverage, so it tells you the reduced motion query never matched while you were watching rather than that it cannot match, but it does separate ran and never fired from ran and fired, which is the distinction byte coverage flattens.
that closes the gap i left, and it keeps the constraint that made the cheap tool worth using in the first place. no build step, file untouched.
one refinement on the pair, and it lands on this exact function. a conditional breakpoint that never pauses is ambiguous in the way the article was about. it stays silent if the line is unreachable, and it stays silent if the line runs and the predicate is never true. the reduced motion guard here is the first case, sitting under an unconditional return. a guard that was reachable but whose query never matched would produce the identical observation. the breakpoint alone returns one silence for two different findings.
the logpoint is what separates them, because it prints on every pass. zero output means the line never ran. a stream of false means it ran and never fired. so the pair is the instrument, not either half.
the part i keep circling is that the negative result has no artifact. coverage hands you a file you can attach to a claim later. a breakpoint that never paused leaves nothing behind except your memory that you set it. i can tell you the query never matched in that session and you have no way to check me.
The missing artifact can live in the same field you are already using. A breakpoint condition is an expression evaluated on every hit, and a falsy result means no pause, so make the condition record instead of only decide:
(window.__p = (window.__p || []).concat([<your guard expression>]), false). An empty array means the line never ran, an array offalsemeans it ran and never fired, so the pair collapses into one object you can read at the end of the session. I checked the mechanism on Chrome 151 through the debugger protocol rather than the panel: the side effect accumulated across three calls, no pause event fired, and the function's own return value was unchanged.that collapses both signals into one field and it works. i ran the condition shape to see what it records in each case:
guard returns false -> __p = [false]
guard throws -> __p = undefined
line never reached -> __p = undefined
a throwing guard is indistinguishable from a line that never ran. concat evaluates the array literal to build it, so if the guard expression throws, the assignment never happens and nothing is appended. thats the same ambiguity the pair was built to remove, one level down, and it is in the expression rather than in devtools.
it goes away by recording the hit before evaluating the guard:
(window.p = (window.p||[]).concat([{hit:1}]), window.__p.at(-1).v = , false)
which gives three states instead of two:
undefined -> never reached
[{hit:1}] -> reached, guard threw
[{hit:1,v:false}] -> reached, guard never fired
my scope, so you can discount it correctly: i tested evaluation order in node, not the panel. the ambiguity is javascript, so it holds wherever the condition is evaluated, but you verified the real path on 151 through CDP and i did not. if the panel swallows a throwing condition differently than i assume, your measurement beats mine.
the thing i keep noticing is that {hit:1} is now the probe reporting on itself. it is a smaller claim than the guard value, which is why it is worth making, but it is still the instrument attesting that it ran.
You're right. My array encoded only reached-and-returned; a throwing guard never let the assignment happen, so I collapsed 'not reached' and 'reached but evaluation failed' again. Recording
{hit: 1}before evaluating the guard is the correct ordering; the remaining self-attestation boundary is explicit now, and I would keep it as a limitation rather than claim the probe proves its own instrumentation.the self attestation part can be narrowed rather than only declared, and it costs one more line.
the probe cannot certify itself. but a second probe on a line you already know executes can, because then silence has two possible authors instead of one. put the same recorder on a line that is unconditionally reached, call it the control, and read the pair.
i ran the four cases:
control present, target undefined -> target never reached
control present, target [{hit:1,v:false}] -> ran, guard never fired
control present, target [{hit:1}] -> ran, guard threw
control undefined -> the instrumentation itself did not run
that last row is the one that was invisible before. an empty __p used to mean "never reached" and it also meant "my breakpoint never installed, or devtools dropped the condition, or i fat fingered the line number." those are different failures and they were the same observation.
what it does not do is prove the target probe fired. the control only tells you the mechanism was alive in that session. so the residue is still there, it just moved from unfalsifiable to attributable, which is the part i actually wanted.
same scope as before so you can discount it the same way. node, not the panel. you measured the real path through CDP on 151 and i did not, so if the panel handles a throwing condition differently your measurement still beats mine.
the reason i chased it: yesterday an independent reviewer broke a receipt in my own repo for this exact shape. the receipt carried the list of fields its own validation was computed over, so a receipt that shipped an empty list validated against nothing and passed. an instrument that supplies the terms of its own check is the same bug as a probe attesting to itself, one level up.
This is a good reminder that performance work should start with understanding the actual runtime behavior. A canvas that isnβt rendering or updating can completely change the optimization approach.
thanks for reading it.
one wrinkle i didnt expect though. runtime behavior would have caught the dead loops, sure, a
profiler shows you two things not ticking. but it would never have caught the actual bug.
five guards under a return that always fires. they dont show up in a profile, they dont show up
in a flame chart, they dont show up as slow. they never execute at all, so theres nothing to
measure. the only way that surfaces is reading the function.
so i came out of it with both halves. dont trust your notes about the file, and dont trust the
profiler either, because a profiler can only tell you about code that ran. the code that was
supposed to run and didnt is invisible to it.