Now I Get It!'s homepage has a carousel that auto-scrolls through recent explanations. It worked great on desktop. On iPhone, it crashed Chrome and didn't appear at all in Safari. Issue #154 was filed, and I sat down with Claude Code to fix it.
The root cause was straightforward: the library API did a full DynamoDB table scan, the frontend created a DOM node for every result (400+ items), applied GPU-intensive backdrop-filter: blur(20px) to each one, then cloned them all for an infinite scroll loop. That's 800+ composited elements. Desktop GPUs shrug at this. Mobile GPUs do not.
The Fix: Don't Send Everything
The backend fix was a new DynamoDB Global Secondary Index keyed on status + record_entry_ts, letting the library Lambda query completed items in reverse chronological order without scanning the whole table. The response changed from a flat array to a paginated {items, next_cursor} shape with 35 items per page, matching the existing gallery API pattern. Added Cache-Control: public, max-age=60 since everyone sees the same data.
On the frontend, I gutted the setupInfiniteScroll() function that cloned every card. The carousel now fetches one page at a time and appends more when you scroll within 500px of the end. No cloning, no DOM explosion.
For mobile CSS, I disabled backdrop-filter inside the existing 540px breakpoint and swapped in a solid background color. Same visual feel, zero GPU compositing cost.
The Bug That Wouldn't Die
All of this deployed cleanly to the test environment. Desktop looked perfect. Chrome DevTools mobile emulation -- perfect. Actual iPhone -- no auto-scroll. The carousel loaded cards fine, but they just sat there, motionless.
What followed was a humbling sequence of wrong guesses. I tried removing the mobile detection guard. Didn't work. Removed scroll-snap. Didn't work. Wrapped startAutoScroll() in a double requestAnimationFrame to wait for layout. Didn't work. Built a float accumulator with Math.floor to handle integer rounding. Didn't work -- and introduced jitter and snap-back as bonus bugs.
After five failed deploys, I stopped guessing and added instrumentation. Since connecting Safari Web Inspector requires a USB cable (which wasn't handy), I deployed a visible debug overlay -- a green-on-black bar at the bottom of the page showing auto-scroll state on every frame. No devtools required, just look at the screen.
The overlay immediately showed: stuck=true, paused=false. The requestAnimationFrame loop was running. It wasn't paused. But every time it wrote to scrollLeft, the browser silently rejected the change. The value went in and came right back out.
Calling in a Second Opinion
At this point I'd been staring at the same code for too long. Rather than throw another guess at the wall, I spun up a Claude Code subagent -- a separate Claude instance with its own context window -- and gave it everything: the old working code, the new broken code, the diagnostic results, and a strict instruction to research, not guess.
The subagent read both versions of the code, compared them line by line, and came back with a clear answer: CSS scroll-behavior: smooth was the culprit.
Here's what happens: when scroll-behavior: smooth is set on an element, every scrollLeft assignment becomes a smooth animation request (per the CSSOM View spec). The auto-scroll loop was writing scrollLeft += 0.25 sixty times per second. Each frame would abort the previous frame's animation before it produced any visible movement, then start a new one that would also get aborted next frame. The scroll position never actually changed.
Desktop Chrome is lenient about this and handles it gracefully. Mobile Safari and Chrome follow the spec strictly. And Chrome DevTools mobile emulation uses the desktop rendering engine, so it never reproduces the problem -- which is why I kept seeing it work in emulation but fail on real hardware.
The Actual Fix
Two changes:
Remove
scroll-behavior: smoothfrom the carousel track CSS. The arrow buttons already pass{ behavior: 'smooth' }directly toscrollBy(), so user-initiated smooth scrolling still works.Switch from frame-based to time-based scrolling. Instead of
scrollLeft += 0.25on every animation frame (which produces sub-pixel values that mobile rounds to zero), the new code usesperformance.now()timestamps to accumulate fractional pixels at 30px/sec and only writes whole-pixel increments. Smooth on every device, no jitter.
Lessons
Chrome DevTools mobile emulation is not mobile. It fakes the viewport and user agent but uses the desktop rendering engine. CSS behavior differences won't show up. Test on real devices.
Stop guessing, start measuring. Five speculative deploys failed. The first diagnostic deploy -- a visible debug overlay that didn't require devtools -- pointed straight to the root cause in seconds.
A second pair of eyes works, even when both pairs are AI. The subagent pattern -- spinning up a fresh Claude with the full problem context and an explicit research mandate -- broke me out of a loop of increasingly desperate guesses. Sometimes you need a clean context window more than you need a new idea.