The mobile speed win that broke a vibe coded site

I shipped a site that felt instant. On my monitor it snapped from section to section, the scroll animation locked cleanly to each pin point, everything smooth. Then someone opened it on a three-year-old Android over cell service and sent back a video. The hero sat blank for about four seconds, the whole page lurched once, and when they tried to scroll it did nothing. The page just ate the gesture. Same build, same commit, two completely different products depending on whose hand it was in.

If your vibe coded site looks fine on your screen and breaks on phones, the fix is almost never the first thing you reach for, and the reaching is the trap. The honest move is to measure on the device that actually matters and to map what a change can break before you apply it, because the most common optimization, deferring the heavy animation to buy back a mobile score, has its own failure surface. I walked straight into it, and it cost me a working scroll, three bad hotfixes, and most of a day.

why does it look fine on my screen and break on a phone

You author and preview on a fast machine. Local network, a CPU that never throttles, a big viewport. The generator previews against that same window, so "looks great here" gets measured on the one device that will never be the problem. Your visitors are somewhere else entirely, on a mid-range phone a few years old, on cell service, with a browser already juggling ten other tabs. The gap between those two worlds is where a vibe coded build quietly falls apart, and it is invisible to you precisely because your environment is the flattering one. This is the same root behind why AI websites load slow: the build was judged on hardware that was never going to struggle.

the fix I reached for, and the failure surface I never mapped

The mobile number came back mediocre, so I went looking for the heaviest thing on the page. That was the hero scroll animation, a pinned sequence doing real work while the page loaded. The obvious lever was to defer its trigger until after the largest paint, so the big content lands sooner and the score goes up. I did it. The number climbed a few points. Felt like a win, so I shipped it.

Then the scroll died. On the phone a swipe moved nothing, on desktop the wheel spun without the page responding, and the pinned panels bled past their boundaries and overlapped the section below. Deferring the trigger meant the scroll logic initialized after the layout had already settled, so it bound to the wrong positions and never recovered. The animation I moved to buy a metric took the entire scroll experience down with it. I had optimized one number and broken the one interaction the whole page was built around.

why did three hotfixes each fix the wrong thing

I treated it as a timing bug first, then a styling bug, then a library-version bug. Each hotfix made the symptom move instead of making it stop. Reorder the init and the pins settle but the wheel stays dead. Patch the wheel and the overlap comes back on a different section. I was chasing the blast radius around the page because I had never written down what deferring the trigger could break before I did it. Every patch was a guess aimed at whatever broke last, which is what debugging looks like when you skipped mapping the failure surface. The change had a knowable set of consequences the moment I made it, and I was discovering them one painful symptom at a time instead of listing them up front.

Eventually I did the only honest thing left and reverted the whole change. Animation back where it started, scroll alive again, mobile number back to mediocre. A full day spent to return to the starting line, minus the illusion that the starting line had been the problem.

the win was measurement noise the whole time

When I finally measured properly, the win evaporated. Mobile lab scores are bimodal: a throttled phone CPU lands in one of two rough clusters depending on thermal state and background contention, so a single run is a sample, not a reading. The few points I had traded a working scroll for were inside that run-to-run swing. I had caught one friendly run and built a regression on top of it. The steady way to measure is a median of several runs, gated on Total Blocking Time rather than the composite score, on a throttled phone profile. I go deeper on that instrument in why your Lighthouse score keeps changing. The short version: I optimized against a number that was never real, and the site got worse while the dashboard said better.

what I do now: build the failure mode first

The principle I should have started with is simple to say and easy to skip. Before you apply a performance fix, write down what it can break, and ship the guard for that failure in the same change. Not after users find the hole. In the same commit.

Deferring an animation past the largest paint can break scroll binding and pin math, so the guard is a deterministic reconcile after hydration plus a pin-interaction check on a real phone. Lazy-loading below the fold can break layout height and scroll anchoring, so the guard is a reserved space and a scroll-position test. Dropping a dependency to shed weight can break a soft navigation path, so the guard is a route walk before merge. The point is not that deferring is wrong. It is that a fix with an unmapped failure surface is a bet you are making blind, and the feedback loop in vibe coding flatters you by default, so blind bets tend to look like wins right up until a stranger opens the thing on a phone. Mapping the surface first is what turns a guess into a change you can trust, and it pairs with the ordinary discipline of fixing an AI built site before you rebuild it.

If you're a vibe coder scaling personal site work into client builds, and you keep trading behavior your users can feel for mobile numbers you measured once, and you want a sparring partner on the production jump, /work-with-us.

questions that keep coming up

So I should never defer animation on mobile? No. Deferring is a fine tool. The mistake is deferring without mapping what it breaks and without shipping the guard in the same change. The trigger is the easy part; the reconcile after it is the part that keeps the scroll alive.

Which mobile number do I actually trust? None of them alone. Take a median of several runs, gate on Total Blocking Time instead of the single composite score, and measure on a throttled phone profile rather than the desktop the generator previewed against. One run is a coin flip you can talk yourself into believing.

How do I catch this before shipping? On a real mid-range phone, not the emulator. The desktop preview and even a good device emulator will not show you a scroll gesture getting eaten, because the thing that breaks is timing against a slow CPU, and the one machine that is never slow is the one you build on.

The site that felt instant on my monitor was never the product. The product is whatever loads in a stranger's hand on a slow phone with a weak signal. Build for that device, measure on that device, and know what your fix can break before you hand it over.

// part of the ai websites topic

// grab the free spec template that stops AI slop before it starts: get it →

// building with AI? the field manual has the structured lessons.

// hitting this on a real build? this is what I fix →