AI SCAN ⚡
AI Advisor 10 Points / Message
Hello! I am your AI Advisor. How can I help you improve your SEO today?
Advertisement
Smartphone

Fix Slow Mobile FCP on SSR Sites: Why Fast TTFB Isn’t Enough

Font Size:
Advertisement
Technical Guide By Kaiss-B — Founder & Developer, SEO Software Ai
12 min read  |  Last updated: October 2026

Mobile First Contentful Paint (FCP) can stay slow on a server-rendered page even when the server responds quickly. This guide explains how to tell a server delay from a browser delay, and it summarizes measurements that other engineering teams have published, with a link to each original write-up.

Browser behaviors and thresholds in this guide link to their official documentation inline. The case studies come from the teams that ran them, not from tests by the author, and the technique ranking is labeled as editorial judgement. The full source list is at the end.

Advertisement
Key Takeaways
  • FCP is a diagnostic metric, not a Core Web Vital. The Core Web Vitals are LCP, INP and CLS. A good FCP is 1.8 seconds or less at the 75th percentile, and above 3 seconds is rated poor, per the web.dev FCP guide.
  • A fast TTFB is a starting line. A TTFB of 0.8 seconds or less is rated good according to web.dev, but the browser still has to fetch blocking CSS, resolve fonts and run synchronous scripts before it paints.
  • The usual culprits are dependencies discovered late: fonts referenced only inside CSS, blocking third-party scripts, and snippets that hide the page.
  • Hydration mostly affects INP and Total Blocking Time, not FCP, unless it runs before the content or a mismatch forces a client re-render.
  • Published results vary widely. A platform-wide change moved FCP by tens of milliseconds at the 75th percentile, while one misconfigured page lost whole seconds. The case studies below show both ends.
  • Lab and field data answer different questions. Always confirm a fix in field data, not only in a lab run.

What FCP Actually Measures in an SSR Page

The W3C Paint Timing specification defines FCP as the first moment the browser renders content from the DOM: text, an image, an SVG or a non-white canvas. In frameworks such as Next.js, Nuxt, Astro, Remix or server-rendered Blade views, the HTML already contains that content, yet the browser works through a pipeline before it paints:

  1. HTML parsing and DOM construction. The compressed HTML must arrive, decompress and be parsed into nodes.
  2. CSSOM construction. A stylesheet linked in the head blocks rendering until it has been downloaded and parsed.
  3. Font resolution. Text that uses a custom @font-face without a font-display value can stay invisible during a block period of up to about three seconds, as covered in web.dev’s font guidance.
  4. Synchronous scripts, when present. A classic script element without async, defer or type="module" pauses the HTML parser until it has been fetched and executed.
Related tool and disclosure: The SEO Software Ai SEO Checker scans a URL for Core Web Vitals signals, heading hierarchy and broken links. The author owns and built this tool, and it does not replace a DevTools trace or real-user field data.

The reference document below keeps the above-the-fold styles inline, loads the full stylesheet without blocking the first paint, and defers the framework bundle:




  
  
  

  
  


  
  
  

  
  




  

Server-rendered content

Painted without waiting for the application bundle.



  
  



Figure 1: An SSR document that separates critical styles from non-blocking assets.

Content-Security-Policy caveat: The onload attribute in Figure 1 is an inline event handler. A strict Content-Security-Policy that does not allow inline scripts blocks it, and the full stylesheet then never applies. A CSP-safe alternative is to load the stylesheet with media="print" and switch it to all from an external script, as in Figure 1b. A nonce-based inline script also works if your policy allows nonces.



// /scripts/async-css.js
document.querySelectorAll('link[data-async-css]').forEach((link) => {
  const apply = () => { link.media = 'all'; };
  if (link.sheet) { apply(); } else { link.addEventListener('load', apply); }
});

Figure 1b: Non-blocking stylesheet loading that does not rely on inline event handlers.

Optimizing the Critical Rendering Path at the Edge

A CDN or edge function shortens the distance to the first byte. It cannot shorten a chain of dependencies the browser only discovers after that byte arrives. On mobile networks every extra sequential request adds latency, so these are the changes worth testing first.

Inline critical CSS, load the rest without blocking

If the first screen depends on an external stylesheet, the browser must find the link, request the file and parse it before painting. Extracting the rules for the first viewport into an inline style element removes that dependency. Tools such as critical and penthouse can automate extraction at build time. The cost is a larger HTML document that cannot be cached separately.

Risk: flash of unstyled content and layout shift. If the inlined CSS does not cover everything visible in the first viewport, elements restyle when the full stylesheet arrives, which can cause a visible flash and a Cumulative Layout Shift (CLS) penalty. To test it, record a throttled Performance trace in Chrome DevTools, enable the Layout Shifts track, and compare the filmstrip before and after the full stylesheet applies. Then run Lighthouse and check the CLS value, and repeat on several page templates, because critical CSS extracted for one template is often incomplete for another.

Preload the fonts used above the fold

A font is normally discovered only after the HTML and the CSS have been processed. A preload hint lets the browser request it immediately. Add crossorigin to a font preload even when the font is self-hosted: fonts are fetched in anonymous CORS mode, and a preload without it does not match the request, so the file can be downloaded twice (see web.dev: best practices for fonts).



Figure 2: Font preload with font-display: swap and a separate metric-adjusted fallback face.

The size-adjust descriptor belongs on the fallback face, not on the web font. Support for ascent-override, descent-override and line-gap-override is broadly available across modern browser releases (Chromium 87+, Firefox 89+, Safari 16.4+). The fallback degrades gracefully: where a descriptor is not supported it is ignored, and the text simply renders in the unadjusted fallback font, with a possible small layout shift on swap.

Use Link headers and 103 Early Hints instead of HTTP/2 Push

Chrome removed support for HTTP/2 Server Push and points developers to 103 Early Hints instead. When the origin needs time to build the page, the server or edge sends a preliminary 103 response carrying Link preload hints, so the browser starts fetching CSS and fonts while the HTML is still being generated. Several CDNs can convert Link response headers into Early Hints automatically.

HTTP/2 103
link: ; rel=preload; as=style
link: ; rel=preload; as=font; type="font/woff2"; crossorigin

HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: public, max-age=0, s-maxage=3600, stale-while-revalidate=86400

Figure 3: Simplified response sequence. The preliminary 103 response is followed by the final 200 response.

Keep edge composition cheap

Edge functions and edge-side includes can assemble personalized fragments near the user, but each extra upstream fetch adds directly to TTFB. If a fragment is slow or uncacheable, stream the main shell first and fill in the rest afterwards.

Diagnosing FCP With cURL, DevTools and Field Data

Work through three layers in order: the server and edge, the browser trace, then real-user data.

Step 1: Measure the server with cURL

curl -o /dev/null -s -w 'DNS: %{time_namelookup}s | TCP: %{time_connect}s | TLS: %{time_appconnect}s | TTFB: %{time_starttransfer}s | Total: %{time_total}s\n' https://www.example.com

time_starttransfer approximates TTFB, including connection setup. Run curl -I as well and check Cache-Control, Link and Vary; an unexpected Vary value can quietly cause CDN cache misses.

Step 2: Trace the page in Chrome DevTools

  1. Open the Performance panel, set CPU throttling to 4x slowdown and choose a Fast 4G or Slow 4G network profile.
  2. Record a page reload and locate the FCP marker in the Timings track.
  3. In the Network track, look at the CSS, font and script requests that finish just before that marker. These are the dependencies holding the paint back.
  4. In the Main track, look for long tasks (over 50 ms) that run before FCP, particularly script evaluation from third-party origins.

Step 3: Collect real-user attribution

The attribution build of the web-vitals library exposes timeToFirstByte and firstByteToFCP on the FCP metric, which splits each value into time waiting for the first byte and time after it.

import { onFCP } from 'web-vitals/attribution';

onFCP((metric) => {
  const { timeToFirstByte, firstByteToFCP, loadState } = metric.attribution;

  navigator.sendBeacon('/api/rum/vitals', JSON.stringify({
    metric: 'FCP',
    totalFcpMs: Math.round(metric.value),
    serverTtfbMs: Math.round(timeToFirstByte),
    clientRenderDelayMs: Math.round(firstByteToFCP),
    domLoadState: loadState,
    effectiveConnection: navigator.connection?.effectiveType || 'unknown'
  }));
});

If serverTtfbMs dominates, investigate rendering time, database work and cache misses. If clientRenderDelayMs dominates, look at blocking CSS, fonts and scripts. navigator.connection is only available in Chromium-based browsers, hence the fallback value.

Hydration and FCP: What Is and Is Not Connected

In universal frameworks such as React, Next.js, Nuxt and SvelteKit, the browser normally paints the server HTML first and the JavaScript hydrates it afterwards. Heavy hydration therefore tends to hurt Interaction to Next Paint (INP) and Total Blocking Time (TBT) rather than FCP. It reaches FCP in three situations:

  • Blocking scripts before the content. Classic script tags in the head stop the parser before it reaches any visible element.
  • Hydration mismatches. When the server HTML differs from the first client render (for example a locale-specific date, or markup that branches on window.innerWidth), React reports a hydration error and falls back to client rendering for the affected part of the tree. See the React documentation for hydrateRoot.
  • Visibility locks. CSS that sets visibility: hidden or opacity: 0 on the page wrapper until JavaScript adds a class means no contentful paint can be recorded until the bundle has run.

Load bundles with defer or as modules, split code by route, wrap heavy sections in Suspense so React 18 and later can hydrate them selectively, and keep server and client output deterministic. Comparing the HTML returned by cURL with the DOM in the Elements panel is a quick way to spot mismatches.

What Published Case Studies Report

The author of this guide did not run these tests. Each result below was measured and published by the named team, and is summarized in our own words with a link to the original. Figures are reported as the source states them, and each source describes its own methodology, so treat the numbers as evidence of direction and scale rather than as guaranteed outcomes.

Summary of the four published cases discussed in this section.
Source What was changed Reported result Type of data
Shopify Performance team, Aug 2026 Automatic 103 Early Hints for render-blocking CSS and scripts, plus third-party preconnects FCP 76 ms faster and LCP 100 ms faster at p75 on eligible pageviews Real-user data from production storefronts
Sunday Citizen with Shopify, Dec 2022 Moved render-blocking files to the platform CDN, updated a third-party app, cleaned up preloads and preconnects Requests before FCP fell from 25 to 6 in the test waterfall; domain FCP 0.3 s lower in CrUX Lab waterfall plus CrUX field data
DebugBear, Feb 2025 (updated Nov 2025) Preloaded a late-discovered font; separately, removed 38 excessive font preloads LCP 1.82 s to 1.24 s in the first test; FCP and LCP both improved by over 2 s in the second Lab experiments
Case-Mate with Shopify, Mar 2023 Removed a reveal transition that kept images at zero opacity LCP improved by about 6 s Reported by the consulting team (LCP, not FCP)

Early Hints at platform scale (Shopify)

In a post dated August 28, 2026, Mateusz Krzeszowiak of Shopify’s performance team described scanning the HTML of each storefront to find render-blocking stylesheets and scripts, then telling browsers about them in a 103 response. For eligible pageviews, FCP improved by 76 ms and LCP by 100 ms at the 75th percentile. A first step that only opened connections to third-party hosts improved FCP by 28 ms across buyer traffic, and a narrow rule for Adobe Fonts added another 31 ms where it applied. The post also records two cautions: an earlier Shopify analysis found mixed or worse results when themes sent many hand-written hints, and Safari currently ignores preloads in Early Hints. Gains were larger when the final HTML arrived later, which matches the idea that Early Hints help most when the origin is slow.

Cleaning up the pre-FCP request chain (Sunday Citizen)

In a December 20, 2022 case study, Mateusz Krzeszowiak documented work on the Sunday Citizen store. A WebPageTest waterfall showed 25 requests before first paint; after the team moved render-blocking files to the platform CDN, updated a third-party app and removed unneeded preloads and preconnects, it showed 6. Public field data for the domain showed FCP about 0.3 seconds lower. Shopify also reported a conversion increase of over 6 percent and a bounce-rate decrease of 4 percent for the project; those business figures come from the publisher’s own analysis and should be read as reported, not as a guaranteed effect of faster paint.

Font discovery and too many preloads (DebugBear)

Conor McCarthy of DebugBear published two lab experiments in an article first posted February 7, 2025. On a page whose largest element was heading text hidden until its font loaded, preloading the font moved LCP from 1.82 s to 1.24 s because the font request no longer waited for the stylesheet. In a second example a page preloaded 38 fonts; removing those preloads let the render-blocking CSS finish about two seconds sooner, and both FCP and LCP improved by more than two seconds. The pair of results is the reason this guide recommends preloading only the one or two fonts used above the fold.

Content hidden at zero opacity (Case-Mate)

Michael Gooding of Shopify’s performance team wrote in March 2023 about a Case-Mate product page where the main product image carried a reveal attribute that set opacity: 0 until a theme script ran, roughly 12 seconds into the load in the test. After the attribute was removed, LCP improved by about six seconds. This case measured LCP rather than FCP, but it shows the same mechanism as Pattern 3 below: content that exists in the HTML but starts out fully transparent is not available as a paint candidate until script or CSS reveals it.

Advertisement

What these cases show, and what they do not: Taken together they support three points: late-discovered dependencies are a common cause of slow first paint, more hints are not automatically better, and the size of the gain depends heavily on the baseline. They do not show that any one technique will produce a particular number on your site. Their methods, devices and traffic differ, and two of the four are lab results.

Three Common Failure Patterns (Teaching Examples)

These three patterns are teaching examples built from documented browser behavior. They contain no measurements of their own; where a published case matches a pattern, it is linked. The code is simplified.

Pattern 1: A font discovered too late

Symptom: TTFB is low, but text appears well after the page background.

Cause: The only @font-face rule lives in an external stylesheet, with no preload and no font-display value. The browser downloads the CSS, discovers the font, requests it, and keeps the text invisible during the block period.

Fix: Preload the one or two fonts used above the fold, set font-display: swap (or optional), subset the font to the glyphs you need, and place the @font-face rule in the inline critical CSS.

How to verify: In a throttled DevTools recording, the font request should start near the top of the waterfall, and text should be visible in the fallback font before the swap.

Documented parallel: the DebugBear font-preload experiment above, and Shopify’s note that a stylesheet from Adobe Fonts can import a second stylesheet, adding another serial step.

Pattern 2: A synchronous third-party script in the head

Symptom: FCP is acceptable on fast connections but varies widely on mobile networks.

Cause: A tag manager, heatmap or testing script is added as a plain script element at the top of the head. The parser stops until it is fetched and executed, so the stylesheet and content behind it wait on a server you do not control.

Fix: Load non-essential scripts with async or defer, or after the first paint. If an A/B testing vendor requires synchronous loading, move variant assignment to the server or edge so the final HTML is already correct.

How to verify: Under throttling, the third-party request should no longer sit in front of the stylesheet request, and no long task from that origin should precede the FCP marker.

Documented parallel: the Sunday Citizen case, where updating a third-party app and cleaning up the request chain reduced requests before first paint.

Pattern 3: An anti-flicker snippet that hides the page

Symptom: The document and assets are downloaded, yet the screen stays blank until a fixed delay passes.

Cause: Some A/B testing tools have historically shipped snippets that set opacity: 0 on the page until the tool loads, with a safety timeout of several seconds. Fully transparent content is not counted as contentful, so FCP waits for the tool or the timeout.

Fix: Remove the snippet or cut its timeout sharply, limit any hiding to the specific element under test, and prefer server-side or edge-side variant selection.

How to verify: Search the rendered source for rules that hide html or body, then confirm in a throttled recording that the first paint no longer coincides with a vendor script finishing.

Documented parallel: the Case-Mate case, where removing a zero-opacity reveal transition improved LCP by about six seconds.

Ranking the Main Optimizations

The ranking below is an editorial judgement, informed by the documented browser behavior and the published cases above, of how often each technique removes the largest delay for the least effort. It is not a benchmark, and the order can differ on your pages. Measure before and after with field data.

Ranking of SSR and edge techniques for improving mobile First Contentful Paint, by editorial judgement.
Rank Technique Why it ranks here Trade-off or risk Effort
1 Defer or remove blocking head scripts One blocking script can stall parsing of everything behind it, and the fix is a one-attribute change. Tags and analytics start slightly later Low
2 Font preload plus font-display Late font discovery hides text on otherwise fast pages, and the change is small. Too many preloads compete with other critical requests Low
3 Inline critical CSS Removes a render-blocking request, but needs build tooling and testing. Larger HTML; risk of flash of unstyled content and CLS Medium
4 103 Early Hints and Link headers Helps only when the origin spends noticeable time building the HTML. Needs CDN support and careful cache configuration Low to medium
5 Streaming SSR with Suspense Large benefit on pages with slow data fetches, but the most invasive change. More complex data loading and error handling High
Pre-Release Checklist
  • HTML is cacheable at the edge and TTFB is stable across regions.
  • No synchronous third-party script sits in the head ahead of the content.
  • Only the fonts needed above the fold are preloaded, each with crossorigin and a font-display value.
  • Critical CSS covers the whole first viewport, and CLS was checked after the full stylesheet applies.
  • No rule hides html or body while waiting for a script.
  • Improvement is confirmed in field data, not only in a lab run.

Frequently Asked Questions

Does a fast TTFB guarantee a good FCP on an SSR page?

No. TTFB only tells you when the first byte of the HTML arrived. The browser still needs to download render-blocking CSS, resolve fonts and run any synchronous script before it can paint, so a page with a low TTFB can still have a slow FCP.

Is First Contentful Paint a Google ranking factor?

FCP itself is not one of the Core Web Vitals; those are LCP, INP and CLS. However, LCP can never occur before FCP, so a late first paint puts a lower bound on how early LCP can be. Improving FCP is worthwhile mainly because visitors notice a blank screen.

Why does Lighthouse show a good FCP while real mobile users see a slow one?

Lighthouse runs one simulated load on a controlled profile, usually with a clean cache. Field data, such as the Chrome User Experience Report shown in PageSpeed Insights, covers many real devices and networks and is evaluated at the 75th percentile. Third-party scripts, slower devices and variable connections are the most common reasons the two disagree.

How can I tell whether an FCP problem is on the server or in the browser?

Compare the time to first byte with the time between that byte and the first paint. A large TTFB share points to origin rendering, database work or cache misses. A small TTFB with a large remainder points to blocking CSS, fonts or scripts. The attribution build of the web-vitals library reports both values for real visitors.

Sources and Further Reading

How This Article Was Produced

AI tools assisted with drafting and editing this article. The author compiled it from the official documentation and published case studies listed under Sources. The measurements in the case-study section come from those publications; the author did not run the tests, and the code samples follow patterns from the linked documentation rather than benchmarks of the author’s own. Spotted an error or an outdated statement? Please use the contact page, and see the Editorial Policy for how content on this site is produced and reviewed.

 KB 
Compiled by Kaiss Bouterfif

Founder and developer of SEO Software Ai. Compiled and edited this guide from the sources listed above.

We may use cookies or any other tracking technologies when you visit our website, including any other media form, mobile website, or mobile application related or connected to help customize the Site and improve your experience. Read our Cookie Policy