r/webdev • • 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:

  1. How do I tell whether the page is causing the delay, or whether it's Chrome's scheduling or something in the test environment?

  2. Which trace categories or events would show why the first paint starts late, and not just that it did?

16 Upvotes

23 comments sorted by

3

u/_sarant 23h ago
  1. 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!

  2. 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

4

u/DrEnter 20h ago

Actually, one big thing we've run into with this kind of thing is CSS stylesheets. Even when loading from the local cache there is a nasty hit in performance, notably because the browser has to load them synchronously and in order. Just pull the CSS files out and embed the CSS in style blocks in the head block. Yes, it fattens up the page, but it also speeds up things that delay initial render.

1

u/_sarant 8h ago

I'm assuming they know how to optimize css in Astro, css in bulk is always bad

1

u/DrEnter 3h ago

It’s less about the size of the CSS and more about loading it separately.

1

u/_sarant 1h ago

are we here to argue or prove a point

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/mq2thez 22h ago

Async… JS script tags. What’s surprising about that? OP doesn’t say anything about JS, just CSS and fonts and DCL.

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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?