paulserban.eu

Writing Edition

Paul Serban

Writing, snippets & book notes

Mastering the Critical Rendering Path: How Browsers Turn Bytes Into Pixels

Subtitle: A deep technical guide to understanding, optimizing, and debugging the browser's rendering pipeline - from network bytes to painted frames.

Introduction

Every time a user navigates to a URL, an elaborate sequence of events unfolds in the browser before a single pixel appears on screen. This sequence - the Critical Rendering Path (CRP) - is one of the most consequential performance domains in frontend engineering, yet it remains poorly understood outside of specialist circles. Engineers who grasp it deeply can build pages that feel instantaneous; those who don't will keep shipping experiences that feel slow regardless of how fast their servers are.

The term "Critical Rendering Path" refers to the sequence of steps a browser must complete to convert HTML, CSS, and JavaScript into pixels on screen. Every step in this pipeline has a cost in time. Understanding those costs - and knowing when they are unavoidable versus incidental - is the difference between performance work that moves metrics and performance work that wastes sprints.

This article is a complete treatment of the CRP: what it is, how each stage works at a mechanical level, where engineers routinely get it wrong, and how to build systems that respect the browser's constraints from the start. Whether you are optimizing a production application or designing a new architecture, the mental model developed here will pay dividends across every project you touch.

What the Critical Rendering Path Actually Is

Before diving into optimization, it's important to have a precise definition. The Critical Rendering Path is not a single operation; it is a pipeline composed of distinct phases, each with its own dependencies and failure modes. The W3C Navigation Timing specification formally delineates many of these stages, and Chrome's Blink rendering engine documentation provides the canonical implementation reference.

The pipeline, at its highest level, looks like this: the browser fetches the HTML document, parses it into a Document Object Model (DOM), fetches and parses CSS into a CSS Object Model (CSSOM), combines the DOM and CSSOM into a Render Tree, performs Layout (also called Reflow), and finally Paints the pixels to the screen. JavaScript complicates this picture significantly because it can both read and mutate the DOM and CSSOM mid-pipeline, forcing the browser to pause, recalculate, and resume.

What makes the CRP "critical" is not that it is the only rendering work the browser does, but that it defines the minimum necessary work to produce the first meaningful frame. Every resource or computation that sits on this critical path delays what the user sees. Everything off this path can be deferred, loaded lazily, or preloaded without blocking the initial render. The engineering discipline of CRP optimization is fundamentally about distinguishing these two categories and ruthlessly deferring everything that can be.

The Pipeline in Depth

Stage 1: DOM Construction

DOM construction begins the moment the browser receives the first bytes of the HTML document. The browser feeds these bytes through a tokenizer that converts raw text into tokens (start tags, end tags, attributes, text nodes) and then into nodes, which are assembled into the tree structure of the DOM. This process is incremental: the browser doesn't wait for the entire HTML document to arrive before beginning to parse. It parses opportunistically as bytes stream in, which is why techniques like HTTP/2 server push and streaming responses can have measurable impact.

The DOM construction process is, by itself, relatively fast - modern browsers can parse several megabytes of HTML per second. The dominant threat to DOM construction time is not the parser itself, but parser-blocking resources: <script> tags without async or defer attributes encountered in the document will cause the parser to stop completely, download the script, execute it, and only then resume parsing. This is the parser-blocking behavior that makes render-blocking JavaScript so damaging to perceived performance. A single synchronous script on a slow connection can add hundreds of milliseconds to the time before any content is visible.

Stage 2: CSSOM Construction

While the DOM represents the content and structure of the document, the CSSOM represents the computed style information that will be applied to that content. The browser collects all CSS - from <link> stylesheets, <style> blocks, and inline style attributes - and constructs the CSSOM tree, resolving the cascade, inheritance, and specificity rules that determine the final computed style for every node.

Unlike DOM construction, CSSOM construction is not incremental in the same way. The browser must have a complete CSSOM before it can build the Render Tree, because CSS rules can cascade and override each other in unpredictable ways based on specificity and source order. This means that any CSS resource encountered in the <head> is, by definition, render-blocking: the browser will not begin painting until it has downloaded and parsed every stylesheet linked in the document head. Large, unoptimized CSS files - thousands of lines of unused rules, or stylesheets served without compression - directly extend the time to first render.

Stage 3: Render Tree Construction

With the DOM and CSSOM available, the browser combines them into the Render Tree. This is not a simple union of the two structures. Nodes that are not visible - those with display: none, <head> elements, <script> elements, and HTML comments - are excluded from the Render Tree entirely. Nodes with visibility: hidden are included because they still occupy space in the layout even though they are not painted. This distinction matters for optimization: using display: none to hide content genuinely removes it from the rendering pipeline, while visibility: hidden only skips painting.

The Render Tree maps to the visual representation of the document. Each node in the Render Tree corresponds to a box that will be laid out on screen. At this stage, the tree encodes which styles apply to which content, but it does not yet encode positions or sizes - that is the job of the next stage.

Stage 4: Layout (Reflow)

Layout, historically called Reflow in Firefox's rendering engine, is the stage where the browser calculates the exact position and size of every element in the Render Tree. The browser starts from the root of the tree and works downward, computing the geometry of every box, resolving percentages to pixel values, handling floats, flexbox, grid, and every other layout algorithm that CSS specifies. The output of Layout is a box model for every rendered element, with absolute pixel coordinates.

Layout is expensive because it is inherently hierarchical and dependent: changing the width of a parent element can cascade layout recalculations to all of its descendants. This is the reason that "forced synchronous layouts" (reading a geometric property like offsetHeight immediately after modifying the DOM in JavaScript) are such a common and painful performance anti-pattern. The browser, having been asked to read a geometry value, has no choice but to complete layout before returning the value, which can turn an O(1) read into O(n) work across the entire subtree.

Stage 5: Paint

Paint is the process of filling in the pixels. Given the box model from Layout, the browser traverses the Render Tree and emits draw calls - fill this rectangle with this color, draw this text at these coordinates, render this border with these properties. On modern browsers this is done into layers, which are then composited together by the GPU in a separate stage called Compositing.

The introduction of layers and GPU compositing fundamentally changed the performance profile of animations and transitions. When a property change is confined to the composite layer stage - transform and opacity being the canonical examples - the browser can bypass Layout and Paint entirely and hand the work directly to the GPU. This is why CSS transform-based animations are dramatically cheaper than animations that modify width, height, top, or left, which each trigger a full Layout -> Paint -> Composite cycle..

JavaScript's Role as Both Asset and Threat

JavaScript occupies a uniquely powerful and dangerous position in the CRP. It can read and modify the DOM and CSSOM at any point during page load, which makes it both indispensable and a significant source of rendering bottlenecks. Understanding the mechanics of script execution timing is one of the most practically valuable pieces of CRP knowledge an engineer can have.

When the HTML parser encounters a <script> tag without any loading attribute, it halts, downloads the script, hands it to the JavaScript engine for execution, and only then resumes parsing. This is parser-blocking. The async attribute tells the browser to download the script in parallel with HTML parsing and execute it as soon as it is downloaded (without blocking parsing during the download, but potentially blocking it during execution). The defer attribute tells the browser to download the script in parallel and execute it only after the HTML has been fully parsed, in document order. For most application logic that doesn't need to run during parsing, defer is the correct default.

<!-- ❌ Parser-blocking: halts HTML parsing until downloaded and executed -->
<script src="app.js"></script>

<!-- ✅ Downloads in parallel, executes immediately when ready -->
<script src="analytics.js" async></script>

<!-- ✅ Downloads in parallel, executes after HTML parsing is complete -->
<script src="main.js" defer></script>

A subtler issue is CSSOM-blocking. Even an async script will not execute until the CSSOM is complete if a stylesheet appears before it in the document. This is because scripts can query computed styles via getComputedStyle(), and the browser must ensure that the CSSOM is up to date before running any script that follows a stylesheet. The implication is that stylesheet ordering matters: a stylesheet placed immediately before a large async script effectively serializes both of them on the critical path.

<!-- ❌ The async script won't execute until large-styles.css is parsed,
     because the browser can't know whether the script queries computed styles -->
<link rel="stylesheet" href="large-styles.css" />
<script src="important.js" async></script>

<!-- ✅ Move the stylesheet after the script, or eliminate the dependency -->
<script src="important.js" async></script>
<link rel="stylesheet" href="large-styles.css" />

Measuring the Critical Rendering Path

You cannot optimize what you cannot measure. The browser exposes several APIs and tools that give direct visibility into CRP performance.

Core Web Vitals and CRP Metrics

Google's Core Web Vitals are the most widely used performance metrics in production web applications, and several of them map directly to CRP stages. Largest Contentful Paint (LCP) measures how long it takes for the largest content element in the viewport to be rendered - a direct function of CRP efficiency for above-the-fold content. Cumulative Layout Shift (CLS) captures unexpected layout shifts caused by late-loading resources that alter the geometry of rendered elements. First Contentful Paint (FCP) marks the moment the browser first paints any text or image, which corresponds closely to the completion of the first CRP iteration.

The PerformanceObserver API makes it straightforward to measure these in real user monitoring (RUM) environments:

// Observe LCP in a real-user monitoring context
const observer = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const lastEntry = entries[entries.length - 1];

  console.log("LCP candidate:", {
    element: lastEntry.element,
    startTime: lastEntry.startTime,
    renderTime: lastEntry.renderTime,
    loadTime: lastEntry.loadTime,
    size: lastEntry.size,
  });

  // Send to your RUM backend
  sendToAnalytics({
    metric: "LCP",
    value: lastEntry.startTime,
    id: lastEntry.id,
  });
});

observer.observe({ type: "largest-contentful-paint", buffered: true });

Chrome DevTools: The Performance Panel

For local debugging, Chrome DevTools' Performance panel provides a frame-by-frame breakdown of the entire rendering pipeline. The waterfall view shows resource fetch timings; the flame chart shows the JavaScript call stack during execution; the "Rendering" track shows when Layout, Paint, and Composite operations occur. A critical diagnostic workflow is to record a page load, look for long yellow blocks (JavaScript execution), long purple blocks (Layout), and long green blocks (Paint), and trace each back to its source.

The performance.getEntriesByType('navigation') API provides programmatic access to Navigation Timing data, which is invaluable for automated performance regression detection in CI/CD pipelines:

function getCriticalPathMetrics(): Record<string, number> {
  const [nav] = performance.getEntriesByType(
    "navigation",
  ) as PerformanceNavigationTiming[];

  if (!nav) return {};

  return {
    // Time to first byte: measures server + network
    ttfb: nav.responseStart - nav.requestStart,

    // DOM parsing time: measures HTML parser performance
    domParsingTime: nav.domInteractive - nav.responseEnd,

    // CSS/resource blocking time
    resourceBlockingTime: nav.domContentLoadedEventStart - nav.domInteractive,

    // Full DOM ready time
    domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,

    // Full page load (all resources including images)
    loadEvent: nav.loadEventEnd - nav.startTime,
  };
}

Practical Patterns for CRP Optimization

Understanding the theory is necessary but not sufficient. The following patterns represent the highest-leverage interventions available to a working frontend engineer.

Inline Critical CSS

The most direct way to eliminate render-blocking stylesheets is to inline the styles required to render above-the-fold content directly in the <head> of the document, and load the rest of the stylesheet asynchronously. This ensures that the browser can complete the CSSOM and begin painting the first frame without any network round trips for CSS.

The challenge is identifying what constitutes "critical CSS". This is not a decision to make by hand in any production system. Tools like critical (an npm package by Addy Osmani) automate the extraction of above-the-fold CSS by running a headless browser, capturing the rendered viewport, and collecting only the CSS rules that affect visible elements.

<head>
  <!-- Critical CSS inlined: zero network round trips for first paint -->
  <style>
    /* Inlined by build toolchain - styles for above-the-fold content only */
    body {
      margin: 0;
      font-family: "Georgia", serif;
      background: #fff;
    }
    .hero {
      display: flex;
      align-items: center;
      min-height: 60vh;
      padding: 2rem;
    }
    .hero__headline {
      font-size: clamp(2rem, 5vw, 4rem);
      line-height: 1.15;
    }
    .nav {
      position: sticky;
      top: 0;
      background: #fff;
      z-index: 100;
    }
  </style>

  <!-- Full stylesheet loaded non-blocking -->
  <link
    rel="preload"
    href="/styles/main.css"
    as="style"
    onload="this.onload=null;this.rel='stylesheet'"
  />
  <noscript><link rel="stylesheet" href="/styles/main.css" /></noscript>
</head>

Resource Hints: Preload, Prefetch, Preconnect

The browser's speculative loading engine is powerful, but it cannot always predict what resources will be needed. Resource hints provide a declarative way to guide the browser's fetching behavior.

preload is the highest-priority hint: it tells the browser to fetch a resource immediately because it will be needed in the current navigation. Use it for render-critical resources that the HTML parser wouldn't discover early - web fonts referenced in CSS, hero images defined in JavaScript, or scripts needed for above-the-fold interactions. preconnect tells the browser to establish a TCP connection and TLS handshake to an origin before a resource from that origin is actually requested, reducing latency for cross-origin resources. prefetch is a low-priority hint for resources likely to be needed in a subsequent navigation.

<head>
  <!-- Preconnect to third-party origins used above the fold -->
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

  <!-- Preload the LCP image so the browser fetches it at the highest priority -->
  <link
    rel="preload"
    as="image"
    href="/images/hero-1920.webp"
    imagesrcset="/images/hero-480.webp 480w, /images/hero-1920.webp 1920w"
    imagesizes="100vw"
  />

  <!-- Preload a critical web font to avoid FOIT -->
  <link
    rel="preload"
    as="font"
    href="/fonts/body-regular.woff2"
    crossorigin
    type="font/woff2"
  />

  <!-- Prefetch the next page for likely navigation -->
  <link rel="prefetch" href="/dashboard" as="document" />
</head>

Avoiding Forced Synchronous Layouts

One of the most damaging CRP anti-patterns in JavaScript-heavy applications is the forced synchronous layout (FSL), also called layout thrashing. It occurs when JavaScript reads a geometric property (like offsetWidth, getBoundingClientRect(), or scrollTop) after having written to the DOM or styles in the same JavaScript frame. The browser, which would normally batch layout work until the end of the frame, is forced to perform layout immediately to return an accurate value.

The fix is to batch all reads before all writes within a given frame. The requestAnimationFrame API provides the correct scheduling hook:

// ❌ Forces layout on every iteration - O(n) layout work per frame
function badResizeElements(elements: HTMLElement[]): void {
  elements.forEach((el) => {
    const width = el.offsetWidth; // READ: forces layout
    el.style.height = `${width * 0.75}px`; // WRITE: invalidates layout
  });
}

// ✅ Batch reads, then batch writes - one layout calculation total
function goodResizeElements(elements: HTMLElement[]): void {
  // Phase 1: Read all geometry (one layout pass)
  const widths = elements.map((el) => el.offsetWidth);

  // Phase 2: Write all styles (no layout triggered until next frame)
  requestAnimationFrame(() => {
    elements.forEach((el, i) => {
      el.style.height = `${widths[i] * 0.75}px`;
    });
  });
}

For complex scenarios involving many DOM measurements and mutations across a component tree, libraries like fastdom or React's batched state update model serve the same purpose at a higher level of abstraction.

Font Loading Optimization

Web fonts are a particularly insidious CRP hazard because they block text rendering - not page rendering. A page can paint its layout, backgrounds, and images while fonts are still loading, but text will either be invisible (Flash of Invisible Text, or FOIT) or rendered in a fallback font (Flash of Unstyled Text, or FOUT). Both are visible to users and both correlate with negative user experience signals.

The font-display CSS descriptor gives developers direct control over this trade-off:

@font-face {
  font-family: "EditorialSerif";
  src: url("/fonts/editorial-serif-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;

  /*
   * swap: show fallback immediately, swap when font loads (FOUT, acceptable)
   * optional: give the font 100ms; if not loaded, use fallback for this page view
   * block: invisible text for up to 3s (FOIT, avoid for body text)
   * fallback: 100ms invisible, then fallback, swap only if fast
   */
  font-display: swap;
}

For LCP text elements, font-display: optional combined with <link rel="preload"> for the font file is often the optimal strategy: the preload ensures the font is available by the time the LCP element is painted, and the optional setting prevents any visible flash if the preload somehow fails.

Trade-offs, Pitfalls, and Edge Cases

The Preload Trap

rel="preload" is powerful, but misuse creates its own performance problems. Preloading resources that are not actually used within a few seconds generates browser console warnings and wastes bandwidth on resources that compete with genuinely critical fetches. Preloading every image on a page, or preloading entire JavaScript bundles speculatively, will degrade performance on constrained connections. The correct scope for preload is narrow: resources that the browser's preload scanner would not discover on its own, and that are used in the current navigation.

A related pitfall is preloading images without specifying imagesrcset and imagesizes for responsive images. A preload that fetches the 1920px hero image on a 375px mobile device wastes significant bandwidth, and the browser will then download the correct responsive image anyway because the <img> srcset will override the preloaded resource if the hints don't match precisely.

Third-Party Scripts and the CRP

Third-party scripts - analytics, tag managers, chat widgets, A/B testing frameworks - are one of the leading causes of poor CRP performance on production sites, precisely because they are difficult to control. A tag manager loading synchronously in the <head> is functionally equivalent to a parser-blocking first-party script, except that it may also dynamically inject additional synchronous scripts. Tools like Lighthouse's "Reduce the impact of third-party code" audit quantify this cost, and the standard remediation is to load all third-party scripts with async or defer and audit the tag manager configuration regularly.

The deeper issue is that many third-party scripts are designed to run as early as possible - often for legitimate reasons, such as ensuring that A/B test assignments are applied before any content is rendered to prevent flicker. This creates a genuine architectural tension between performance and functionality that requires explicit product decisions about which scripts justify their CRP cost.

CSS Specificity and CSSOM Complexity

A poorly architected CSS codebase imposes costs on CSSOM construction that are easy to overlook. Deep specificity chains, excessive use of @import within stylesheets (each @import creates a new HTTP request that is sequential, not parallel), and very large stylesheets with high specificity rules all contribute to CSSOM construction time. CSS @import is particularly dangerous because it is not parallelized by the browser in the same way that <link> tags are: a stylesheet that @imports three other stylesheets creates a sequential chain of three blocking requests.

The practical rules are straightforward: never use @import in production stylesheets; use <link> tags or a build-time bundler instead. Regularly audit CSS bundle size with tools like PurgeCSS or similar dead-code elimination tools. And be aware that CSS custom properties (var()) and modern layout modes like grid and subgrid, while generally performant, can create Layout invalidation cascades under certain dynamic conditions that naive benchmarks will not capture.

The will-change Anti-pattern

The will-change CSS property is a browser hint that causes an element to be promoted to its own compositor layer in advance of an animation. When used correctly - on elements that will be animated with transform or opacity imminently - it allows the browser to prepare the layer and avoid a jank frame when the animation begins. When overused, it consumes GPU memory for every element that has it applied and can actually degrade performance on memory-constrained devices.

The common anti-pattern is adding will-change: transform globally to large numbers of elements "just in case." The correct pattern is to apply it programmatically, immediately before an animation is scheduled to begin, and remove it when the animation completes:

function animateElement(el: HTMLElement): void {
  // Hint to browser: promote this element just before animation
  el.style.willChange = "transform, opacity";

  el.addEventListener(
    "animationend",
    () => {
      // Remove hint after animation: releases compositor layer memory
      el.style.willChange = "auto";
    },
    { once: true },
  );

  el.classList.add("animate-in");
}

Best Practices: A Framework for CRP-Aware Development

Adopting a CRP-aware mindset during design and development - rather than as a remediation exercise after shipping - produces substantially better outcomes. The following practices form the foundation of a defensible approach.

Audit before you optimize. Run Lighthouse or WebPageTest against every significant page in your application and establish baselines for FCP, LCP, and Time to Interactive (TTI). Without a baseline, optimization efforts lack direction and cannot be validated. Integrate automated Lighthouse runs into your CI/CD pipeline to catch regressions before they reach production.

Treat the <head> as critical infrastructure. The document <head> directly controls what is on the critical path. Establish a strict ordering convention: charset and viewport meta first, then DNS prefetch and preconnect hints, then critical CSS (inlined or preloaded), then deferred scripts. Review any PR that modifies the document head with the same scrutiny you would apply to a database migration.

Make render-blocking a deliberate choice. Every render-blocking resource should be a conscious engineering decision, not an accident of code organization. CSS in the <head> is render-blocking by design; scripts without defer or async are render-blocking by accident. Code review processes should flag any new parser-blocking script added to the document.

Separate critical and non-critical CSS at the build level. Configure your build toolchain to automatically extract and inline critical CSS for each route. Tools like Vite, Next.js, and Gatsby have varying levels of built-in support for this; for custom setups, the critical npm package integrates well with most build pipelines. The goal is that no stylesheet fetch sits on the critical path for above-the-fold rendering.

Use the loading attribute and Intersection Observer for below-the-fold resources. Native lazy loading (loading="lazy" on <img> and <iframe>) removes below-the-fold images from the critical path with a single attribute. For JavaScript-driven content, Intersection Observer provides a low-overhead mechanism for deferring work until elements approach the viewport, avoiding the layout recalculations associated with scroll event listeners.

Profile under realistic conditions. A fast development machine on a wired connection produces a completely unrepresentative performance profile. Use Chrome DevTools' CPU throttling (4x or 6x slowdown) and network throttling (Slow 4G or Fast 3G) to approximate median mobile conditions. Field data from your actual users, collected via RUM tooling, will always differ from lab data; use both, and weight field data more heavily for business decisions.

The 80/20 of CRP Optimization

If you apply only a small number of CRP optimizations, these are the ones that produce the majority of the improvement in the majority of applications.

The single highest-leverage intervention is eliminating render-blocking scripts and stylesheets. Add defer to every script tag that doesn't need to run before DOM construction; reduce your critical CSS footprint to only above-the-fold styles; load everything else asynchronously. This one change frequently produces 30-60% improvements in FCP on poorly optimized pages.

The second is ensuring your LCP resource is discovered early and prioritized appropriately. If your LCP element is an image, give it a <link rel="preload"> in the document head with the correct imagesrcset. If it is text, ensure the font is either system-stack or preloaded. The browser's resource scheduler does not inherently know which resource will become the LCP element; you need to tell it.

The third is eliminating or deferring third-party scripts that load in the <head>. Tag managers and analytics scripts are common offenders. Moving them to defer or loading them after the load event is fired often has no measurable impact on their functionality and a significant positive impact on CRP metrics.

Analogies and Mental Models

The pipeline model of the CRP maps well to a factory assembly line. Each stage (DOM, CSSOM, Render Tree, Layout, Paint) is a workstation on the line. Work can only move forward, not backward, without restarting from an upstream station. Blocking a workstation with a long-running task (a large synchronous script, a massive unoptimized stylesheet) stalls the entire line. Introducing a defect late in the line (a forced synchronous layout in a scroll handler) forces a partial restart from Layout. The goal of CRP optimization is to minimize stalls and partial restarts.

A complementary mental model for understanding render-blocking resources is the concept of a gate that must be opened before traffic can flow. Each render-blocking stylesheet and parser-blocking script is a gate. Until every gate on the path to first paint is open - every stylesheet downloaded and parsed, every synchronous script executed - the browser cannot render anything. The first frame is only as fast as the slowest gate.

Conclusion

The Critical Rendering Path is not an academic concept reserved for browser engine developers. It is a practical engineering constraint that affects every web application, every page, every frame. The browser's rendering pipeline is deterministic and well-documented; its performance characteristics are predictable given an understanding of the underlying mechanics. That predictability is a gift: unlike distributed systems failures or cloud provider incidents, CRP performance is almost entirely within the control of the application engineer.

The engineers who consistently build fast web experiences are not necessarily the ones with the fastest servers or the largest performance teams. They are the ones who have internalized the model deeply enough to make CRP-aware decisions by default - who automatically reach for defer instead of a bare <script> tag, who think about whether a new stylesheet belongs in the head or can be loaded asynchronously, who benchmark under realistic conditions rather than on their development machines.

Invest in building this mental model. Audit your current applications with the framework described here. Make CRP performance a first-class concern in your architecture review process and your pull request workflow. The return on that investment, measured in user experience and business outcomes, is consistently positive.

Key Takeaways

Five things you can apply today:

  1. Run Lighthouse on your highest-traffic pages and establish CRP metric baselines (FCP, LCP, TTI). Integrate Lighthouse CI into your deployment pipeline to prevent regressions.
  2. Audit every <script> tag in your HTML - any tag without async or defer that appears before the closing </body> tag is parser-blocking by default. Add defer unless there is an explicit reason not to.
  3. Preload your LCP image by adding <link rel="preload" as="image" href="..." imagesrcset="..." imagesizes="..."> to the document head. Measure before and after with WebPageTest to quantify the improvement.
  4. Review third-party scripts in your tag manager for anything loading synchronously in the <head>. Load analytics and marketing tags with async or defer and validate that their functionality is unaffected.
  5. Add font-display: swap (or optional) to all @font-face declarations and preload the most critical font files. This eliminates FOIT for body text and removes a common source of LCP degradation.

References