Skip to main content

03 - LCP Optimization: Image Preloading, Critical CSS and SSR

LCP Optimization: Image Preloading, Critical CSS and SSR. Largest Contentful Paint (LCP) measures the time for the main content to render. Learn image preloading strategies, critical CSS inlining, font preloading, fetchpriority attributes, and SSR optimization for LCP under 2.5 seconds.

What do you think about this article?

At a Glance

LCP Optimization: Image Preloading, Critical CSS and SSR. Largest Contentful Paint (LCP) measures the time for the main content to render. Learn image preloading strategies, critical CSS inlining, font preloading, fetchpriority attributes, and SSR optimization for LCP under 2.5 seconds.

LCP Optimization: Image Preloading, Critical CSS and SSR

The Largest Contentful Paint (LCP) measures the time it takes for the main content of the page is visible to the user. For Google, the "Good" threshold is 2.5 seconds: beyond this threshold, the organic ranking is penalized. According to CrUX data from March 2026, the 35% of websites still have LCP in the "Needs Improvement" or "Poor" range, often for reasons that can be avoided with targeted interventions.

The LCP element is typically the hero image, main banner, or multiple text block large above the fold. The optimization strategy depends on the type of item: an image requires different approaches to a text. In both cases, there are four ad techniques high impact covering 90% of real cases: LCP image preloading, inlining the Critical CSS, font preloading with font-display: swap, and Server-Side Rendering.

What You Will Learn

  • How to identify LCP element with Chrome DevTools and Lighthouse
  • Image preloading with <link rel="preload"> and the fetchpriority="high" attribute
  • Critical CSS: what and how to extract it and how to include it inline in HTML
  • Font preloading with font-display: swap and preconnect
  • How SSR (Server-Side Rendering) structurally improves the LCP
  • How to measure LCP improvement before and after optimizations

Identify the LCP Element

The first step is always knowing which element is your LCP. You can identify it in two ways:

  • Chrome DevTools: in the Performance panel, after a load recording page, look for the "LCP" marker in the timeline. Click on it to see which item triggered the LCP and how many dependent resources it requested.
  • web-vitals.js: the property entries of the LCP metric object contains the LargestContentfulPaint entry with the corresponding DOM element.
import { onLCP } from 'web-vitals';

onLCP((metric) => {
  const lcpEntry = metric.entries[metric.entries.length - 1];
  console.log('Elemento LCP:', lcpEntry.element);
  console.log('Valore LCP:', metric.value, 'ms');
  console.log('Rating:', metric.rating); // 'good' | 'needs-improvement' | 'poor'
});

Strategy 1: Image Preloading with fetchpriority

If your LCP element is an image, the most impactful improvement is the preloading explicit with high priority. By default, the browser only discovers images when the HTML parser encounters the tag <img>, which can happen long after the document has started loading. With <link rel="preload">, the browser start downloading the image as soon as possible.

<!-- Nel <head> dell'HTML, prima di altri tag -->
<link
  rel="preload"
  as="image"
  href="/images/hero.webp"
  fetchpriority="high"
  imagesrcset="/images/hero-400.webp 400w,
               /images/hero-800.webp 800w,
               /images/hero-1200.webp 1200w"
  imagesizes="100vw"
/>

The attribute fetchpriority="high" (Baseline 2023, 92% support) communicates to browser that this resource is critical and must be downloaded before prioritized resources normal. On slow networks, this can make a difference of 0.5-1.5 seconds on the LCP.

Responsive Images: imagesrcset and imagesizes

If you use responsive images with srcset, you have to add imagesrcset e imagesizes to the preload link to have the browser preload the correct variant for the current viewport, not the larger version.

WebP and AVIF format

Image format optimization is complementary to preloading. WebP typically reduces the weight of images by 25-35% compared to JPEG. AVIF reduces by 50-55% but has times of higher decoding on older hardware. For LCP image, WebP and optimal format in 2026, with AVIF for progressive improvement on modern browsers.

<picture>
  <source
    srcset="/images/hero.avif"
    type="image/avif"
  />
  <source
    srcset="/images/hero-400.webp 400w, /images/hero-800.webp 800w"
    type="image/webp"
    sizes="100vw"
  />
  <img
    src="/images/hero.jpg"
    alt="Hero image"
    width="1200"
    height="600"
    fetchpriority="high"
    loading="eager"
  />
</picture>

Do not use loading="lazy" on the LCP element

loading="lazy" delays downloading the image until it is close to the viewport. For the LCP element, which is by definition already in the viewport, this causes an unnecessary delay. USA loading="eager" (or no loading attribute) for the LCP image. Apply loading="lazy" only to images below the fold.

Strategy 2: Critical CSS Inline

By default, the browser does not begin rendering the page until it has downloaded and viewed it all the CSS linked in the<head>. If your CSS file is large (100KB+) e served on a slow CDN, this can add 0.5-2 seconds to the LCP. The solution is the Critical CSS: Extract only the CSS needed to render the content above the fold and include them inline in the HTML.

<!-- INVECE DI: -->
<link rel="stylesheet" href="/styles/main.css">

<!-- USA: -->
<style>
  /* Critical CSS inline - solo quello necessario above the fold */
  body { margin: 0; font-family: sans-serif; }
  .hero { width: 100%; height: 60vh; background: #f5f5f5; }
  .hero h1 { font-size: 2.5rem; color: #333; }
  nav { display: flex; justify-content: space-between; padding: 1rem; }
</style>

<!-- Carica il CSS completo in modo non-bloccante -->
<link
  rel="preload"
  as="style"
  href="/styles/main.css"
  onload="this.rel='stylesheet'"
/>
<noscript><link rel="stylesheet" href="/styles/main.css"></noscript>

Tools for Extracting Critical CSS

Manually extracting Critical CSS is not practical on large sites. Use automatic tools:

  • Critters (webpack/Vite plugin): Extracts and inlines critical CSS automatically
  • critical (npm): CLI tool to extract critical CSS from any URL
  • PurgeCSS + Critters: ideal combination for reducing total CSS before inlining
// vite.config.ts - con Critters per Critical CSS automatico
import { defineConfig } from 'vite';
import critters from 'vite-plugin-critters';

export default defineConfig({
  plugins: [
    critters({
      // Inline il critical CSS, carica il resto in modo asincrono
      strategy: 'critical',
      // Non rimuovere i CSS non usati (lascialo a PurgeCSS)
      pruneSource: false,
    }),
  ],
});

Strategy 3: Font Preloading with font-display: swap

Web fonts can block text rendering (FOIT - Flash of Invisible Text) up to 3 seconds on slow connections, delaying the LCP if the text and the LCP element. Two optimizations combine to eliminate this problem:

1. font-display: swap tells the browser to show the text immediately with the fallback font, then replace it with the web font when it is available. Eliminate FOIT.

/* In @font-face o nel parametro di Google Fonts */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap; /* Critico per LCP */
  font-weight: 400 700;
}

/* Con Google Fonts: aggiungi &display=swap all'URL */
<link
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"
  rel="stylesheet"
/>

2. Preconnect + Preload reduces font discovery and download latency:

<!-- Preconnect: stabilisce la connessione con il font server in anticipo -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- Preload: scarica il file font prima che il CSS venga parsato -->
<link
  rel="preload"
  as="font"
  type="font/woff2"
  href="/fonts/inter-400.woff2"
  crossorigin
/>

Strategy 4: Server-Side Rendering

The previous four strategies optimize resources; SSR optimizes the fundamental structure of the page. In a Single Page Application (SPA), the browser receives almost HTML empty and must download, parse and execute JavaScript to render the content. With SSR, the browser receives HTML already rendered: the LCP element is immediately visible without waiting for JavaScript.

The LCP improvement with SSR is typically 0.5-2 seconds:

  • SPA without SSR: HTML shell → JS download (300-800ms) → JS execution (100-500ms) → API calls (200-800ms) → render → LCP
  • With SSR: Full HTML → Instant LCP (download time only of the HTML and the server's TTFB)

Frameworks like Next.js, Angular Universal (SSR), Nuxt.js, and SvelteKit include native SSR. For Angular, configuration is managed by angular.json and from the Express server:

// angular.json - abilita SSR
{
  "projects": {
    "app": {
      "architect": {
        "build": {
          "configurations": {
            "production": {
              "server": "src/main.server.ts",
              "prerender": false,    // SSR on-demand
              "ssr": true
            }
          }
        }
      }
    }
  }
}

Measuring LCP Improvement

After implementing optimizations, measure the improvement with these tools:

  1. Lighthouse CI: create baselines before optimizations and compare after. Run more runs (5+) and use the median to reduce variance.
  2. WebPageTest.org: get filmstrip and detailed loading waterfall on real devices and real connections.
  3. web-vitals.js in production: monitors users' real LCP P75 for 7-14 days after deployment.
import { onLCP } from 'web-vitals';

// Monitora LCP in produzione
onLCP({ reportAllChanges: true }, (metric) => {
  // Invia solo il valore finale (quando la pagina e in background)
  if (metric.entries.length > 0) {
    navigator.sendBeacon('/api/vitals', JSON.stringify({
      metric: 'LCP',
      value: metric.value,
      rating: metric.rating,
      url: window.location.pathname,
      device: navigator.connection?.effectiveType || 'unknown',
    }));
  }
});

Combined Impact of the Four Strategies

Apply all four strategies in combination on a typical SPA with hero image, web fonts and CSS bundled produces real improvements:

  • Image preloading + fetchpriority: -300-600ms LCP
  • Critical CSS inline: -200-500ms LCP (eliminates render-blocking)
  • Font preloading + font-display:swap: -100-300ms LCP
  • SSR: -500-2000ms LCP (depends on the complexity of the SPA)

The total result can take an LCP from 4-6 seconds to 1.5-2.5 seconds, going from "Poor" to "Good" consistently.

Conclusions

LCP optimization is an area where a few targeted interventions produce enormous impacts. 35% of sites with LCPs in the "Needs Improvement" or "Poor" range almost certainly have one or more of these four problems: hero image not preloaded, rendering-blocking CSS, font that delay text, or SPA without SSR.

The correct workflow is: 1) measure the real LCP with web-vitals.js, 2) identify the LCP element, 3) analyze the loading waterfall to find the bottleneck, 4) apply the relevant optimizations, 5) verify the improvement with Lighthouse CI and field data.

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects