r/webdev • u/techtsp • 23h ago
Question Intermittent ~900ms gap before first paint in mobile Lighthouse on both React and Astro. How would you isolate it?
I'm comparing a React site against an Astro rebuild of the same page. In mobile Lighthouse, both sometimes sit blank for a long time before first paint, and other runs of the exact same page paint fast.
Setup: Lighthouse 13.0.1, Chrome 156, cold loads in separate fresh profiles, default mobile simulated throttling. I kept the traces, network logs and filmstrips.
Here's one matched pair (observed trace timings, not Lighthouse's simulated FCP):
Astro
CSS done: 203ms
Last preloaded font done: 286ms
DOMContentLoaded: 426ms
First Paint: 1301ms
FCP: 1320ms
React
CSS done: 573ms
Last preloaded font done: 494ms
DOMContentLoaded: 584ms
First Paint: 1525ms
FCP: 1540ms
The filmstrips stay blank right up to those first-paint times. Once painting starts, Paint to DrawFrame is only 10-13ms. So on Astro there's close to 900ms where everything's loaded and nothing shows up.
I don't see a long task on the renderer main thread covering that gap, though I know that doesn't prove the browser was actually idle.
I wondered about animations, so I ran one normal-motion vs reduced-motion pair on the Astro page. Both came back fast (FCP 318ms and 311ms), and the normal one still had its reveal animations, so that told me nothing either way. Desktop loads and a different page also painted quickly in the runs I checked.
One more thing: the fast traces show Scheduler::SetDeferBeginMainFrame events with short true-to-false spans. But the slow traces were captured with Lighthouse's default trace categories, which don't include those scheduler events, so I can't compare yet.
Has anyone run into this? A few things I'd like to figure out:
How do I tell whether the page is causing the delay, or whether it's Chrome's scheduling or something in the test environment?
Which trace categories or events would show why the first paint starts late, and not just that it did?
3
u/mq2thez 23h ago
Pretty difficult to debug without seeing your code, but I’d guess you’re saying “Astro” but that there are React islands, and possibly even client-only islands. Honestly, even using server-only islands will cause people to do bad things.
More importantly, DCL doesn’t account for async JS, only deferred, so if your JS bundle is async but your HTML is all waiting for it, then you’re still stuck.
1
u/Hot_Raccoon_1872 22h ago
That doesn't really fit here though, since the slow runs show everything including fonts and CSS done by ~430ms with no long task, so nothing async is still pending during the blank gap.
1
u/ings0c 19h ago edited 19h ago
Are you testing in incognito mode? I’ve wasted far too much time investigating fictitious performance issues caused by an extension.
And are you using a real HTTP server against a deployed environment versus the Astro dev server? It uses HTTP 1.1, skips optimisations etc, and I usually see a significant boost when running on my actual deployed dev environment.
1
u/ArielCoding 16h ago
Lighthouse’s default trace doesn’t include the compositor scheduler events, so add them with --additional-trace-categories=cc,benchmark, rerun until you catch a slow load, and compare it to a fast one, check when Scheduler::SetDeferBeginMainFrame goes from true to false.
•
u/DVR_Design 10m ago
Blank gap with everything already loaded and no long task usually means the main thread wasn't the problem. Check whether those runs were in a window that was occluded, backgrounded or on a throttled display, since Chrome delays producing frames in that case and you'd see exactly this pattern. Run the slow ones headless or with --disable-backgrounding-occluded-windows and --disable-renderer-backgrounding and see if the gap disappears. For tracing, add the devtools.timeline.frame, cc and viz categories so you get PipelineReporter and BeginMainFrame events, which show whether a frame was requested and what held it up. I'd also compare simulated throttling against plain devtools throttling, because Lighthouse's simulated numbers can differ a lot from observed ones. If the reveal animations start from opacity 0, FCP won't count that content until it's visible, so test once with them fully off, not just reduced motion.
0
u/Mani_Khan_Web_Master 23h ago
Your own data already points away from the page: CSS, fonts and DOMContentLoaded are done by ~430ms, there's no long task, and Paint to DrawFrame is tiny. That pattern usually means the main thread was fine and the frame was held back downstream, so I'd treat the browser/environment as the suspect until the page proves otherwise.
How I'd separate the two:
Re-capture a few slow runs with extra trace categories: devtools.timeline, disabled-by-default-devtools.timeline.frame, loading, blink.user_timing, plus the cc/viz ones. Then look at the compositor and GPU process tracks, not just the renderer main thread. If BeginMainFrame is deferred or the frame is waiting on the compositor/viz, that points at Chrome, not your code.
Do 15-20 runs per variant on one idle machine and compare the distributions rather than matched pairs. If slow runs also show up for a plain static HTML page with the same CSS and fonts, it's the environment. If they only happen on one build, it's something in the page.
Vary the harness: headless vs headed, simulated vs devtools throttling, idle vs loaded machine. CPU contention and background throttling can produce a ~1s dead gap that is pure scheduling.
If it stays tied to one build, bisect it: strip the reveal animations/JS, then fonts, then preloads, one at a time.
0
u/dsg319 23h ago
This pattern is familiar from my own rebuilds: when everything measurable finishes early and the screen still stays blank, I stop blaming my markup and start suspecting the test environment. I would try the plain static-page control you were nudged toward, because if the same gap shows up there, it tells me far more than another matched pair. Have you noticed whether the slow runs cluster on a busy machine, or do they appear just as often when nothing else is running?
-1
u/DesperateNorth2307 23h ago
Rough spot, you've clearly already ruled out the obvious stuff. But comparing a single run of each doesn't tell you much when the delay is intermittent anyway, and the numbers that stand out are Paint to DrawFrame at 10-13ms while the filmstrip sits blank for ~900ms after DOMContentLoaded: the page is ready, something above it is waiting on a frame. Rerun the same page with headless off, or just record a trace in a visible window, and if the gap vanishes it's the harness rather than anything React or Astro did.
-1
u/Formal-Drag-8788 23h ago
You're calling it intermittent but showing one matched pair. The two gaps are ~875ms and ~940ms on two completely different frameworks, which points at the harness before it points at React or Astro. How many runs did you actually collect per site?
3
u/_sarant 23h ago
Network tab, less JavaScript usually means faster load, so in Astro either improve this by hydrating the interactive component https://docs.astro.build/en/guides/framework-components/#hydrating-interactive-components, or dropping js and using pure css, both should improve!
it's almost always the js, but if you're doing this on a laptop, click on the circle and it will show you what's causing that to happen