3>The Foundational Metrics: Deconstructing Core Web Vitals
To effectively optimize, one must first deeply understand the target. Core Web Vitals are not arbitrary benchmarks; they are specific, user-centric metrics designed to quantify key aspects of the user experience. They measure loading performance, interactivity, and visual stability. While Google’s set of Core Web Vitals evolves, the current trio consists of Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS). Understanding the nuances of what each metric measures, what constitutes a “good” score, and the common culprits behind poor performance is the first and most critical step in any optimization journey.
Largest Contentful Paint (LCP): The Perception of Speed
Largest Contentful Paint measures loading performance. Specifically, it marks the point in the page load timeline when the main content of the page has likely loaded. A fast LCP helps reassure the user that the page is actually working and delivering value. It’s a proxy for perceived load speed because it focuses on what the user sees first and cares about most, rather than technical milestones like DOMContentLoaded
or load
.
What is the LCP Element?
The LCP element is the largest image or text block visible within the viewport. The browser reports a PerformanceEntry
for LCP as the page loads, and it can change as more content renders. The final LCP element is the largest one reported before the user interacts with the page (by clicking, tapping, or scrolling).
Elements that can be considered for LCP include:
elements.elements inside an
element.
- The poster image of a
element.
- An element with a
background-image
loaded via theurl()
function (as opposed to a CSS gradient). - Block-level elements containing text nodes or other inline-level text element children.
LCP Thresholds:
Google defines the following performance buckets for LCP, based on data from the Chrome User Experience Report (CrUX):
- Good: 2.5 seconds or less.
- Needs Improvement: Between 2.5 seconds and 4.0 seconds.
- Poor: More than 4.0 seconds.
The goal is to have at least 75% of your page loads fall into the “Good” category.
Common Causes of a Poor LCP:
A slow LCP is almost always caused by a delay in one of four sub-parts of the loading process:
- Slow server response time: The browser has to wait a long time to receive the initial HTML document from the server. This is also known as Time to First Byte (TTFB).
- Render-blocking JavaScript and CSS: Before the browser can render anything, it must parse the HTML and build the DOM tree. If it encounters external CSS or synchronous JavaScript files, it must pause and wait for them to be downloaded, parsed, and executed.
- Slow resource load times: The LCP element itself (e.g., a large hero image or a web font for a headline) takes a long time to download.
- Client-side rendering: Many modern frameworks render the page on the client-side using JavaScript. This can involve large JavaScript bundles that must be downloaded and executed before any meaningful content can be displayed, delaying the LCP.
Interaction to Next Paint (INP): The Essence of Responsiveness
Interaction to Next Paint is the Core Web Vital that assesses a page’s overall responsiveness to user interactions. It replaced First Input Delay (FID) in March 2024 because it provides a more comprehensive measure. While FID only measured the delay of the first interaction, INP considers all interactions throughout the user’s visit. It reports one of the longest interactions, giving a better picture of the page’s overall interactivity.
INP measures the time from when a user initiates an interaction (like a click, tap, or key press) until the next frame is painted on the screen, showing visual feedback that the interaction has been processed.
What Constitutes an Interaction?
INP measures the latency of these user actions:
- Clicking with a mouse.
- Tapping on a touchscreen device.
- Pressing a key on a physical or onscreen keyboard.
The total interaction latency is composed of three phases:
- Input Delay: The time the browser has to wait before it can even begin processing the event handlers. This happens if the main thread is busy with other tasks.
- Processing Time: The time it takes to execute the code in the associated event handlers (e.g., the JavaScript that runs when a button is clicked).
- Presentation Delay: The time it takes the browser to recalculate styles, perform layout, and paint the new frame to the screen.
INP Thresholds:
- Good: 200 milliseconds or less.
- Needs Improvement: Between 200 milliseconds and 500 milliseconds.
- Poor: More than 500 milliseconds.
Like LCP, the target is for at least 75% of page visits to have an INP in the “Good” range.
Common Causes of a High INP:
A high INP is almost always caused by an over-burdened main thread. The main thread is where the browser does most of its work, including parsing HTML, executing JavaScript, and handling user input.
- Long-running JavaScript tasks: A single, long-running script can block the main thread, preventing it from responding to user input. This is the most common cause.
- Large JavaScript payloads: The more JavaScript you have, the longer it takes to download, parse, and execute, increasing the likelihood of main thread contention.
- Inefficient event handlers: Poorly written code within event listeners can take a long time to execute.
- Excessive DOM size: A very large and complex DOM tree can make rendering updates (re-calculating styles and layout) very slow, increasing the presentation delay.
- Third-party scripts: Often, analytics, ads, or social media widgets run heavy JavaScript that competes for main thread time, delaying your own interaction handlers.
Cumulative Layout Shift (CLS): The Guarantee of Visual Stability
Cumulative Layout Shift measures visual stability. It quantifies how much unexpected layout shift occurs during the entire lifespan of a page. A layout shift happens when a visible element changes its position from one rendered frame to the next. These shifts are frustrating for users, as they can cause them to lose their place while reading or accidentally click on the wrong element.
How CLS is Calculated:
The score for a single layout shift is calculated using two factors:
- Impact Fraction: This measures how much of the viewport is affected by the unstable elements between two frames. It’s the area of the union of the element’s visible area in the previous frame and its visible area in the current frame, as a fraction of the total viewport area.
- Distance Fraction: This measures the distance that the unstable elements have moved relative to the viewport. It’s the greatest distance any unstable element has moved (either horizontally or vertically) divided by the viewport’s largest dimension (width or height).
layout shift score = impact fraction * distance fraction
The page’s overall CLS score is not just the sum of all individual shift scores. The browser groups shifts that occur close together in time into “session windows.” The final CLS score for the page is the maximum session window score. This prevents a long-lived page with many small, user-initiated shifts from accumulating an unfairly high score.
CLS Thresholds:
- Good: 0.1 or less.
- Needs Improvement: Between 0.1 and 0.25.
- Poor: More than 0.25.
Again, the goal is for 75% of users to experience a “Good” CLS score.
Common Causes of a High CLS:
- Images without dimensions: If
width
andheight
attributes are not specified on an
tag, the browser doesn’t know how much space to reserve. When the image finally loads, it pops into place, pushing other content down. - Ads, embeds, and iframes without dimensions: Similar to images, these elements often load asynchronously and can cause significant layout shifts if a container with a fixed size is not reserved for them.
- Dynamically injected content: Content like cookie consent banners, newsletter sign-up forms, or “related articles” sections that are injected into the page above existing content will push everything below them down.
- Web fonts causing FOIT or FOUT: When a custom web font loads, it can replace a system fallback font. If the two fonts have different dimensions, this swap can cause text to reflow, shifting surrounding elements. This is known as Flash of Invisible Text (FOIT) or Flash of Unstyled Text (FOUT).
- Actions waiting for a network response before updating the DOM: For example, clicking an “Add to Cart” button that doesn’t provide immediate feedback and then suddenly injects a “Success!” message above the button can cause a layout shift.
The On-Page SEO Toolkit: Measuring and Diagnosing Vitals
Optimizing for Core Web Vitals is an evidence-based practice. You cannot improve what you cannot measure. A robust toolkit is essential for identifying issues, testing solutions, and monitoring progress. These tools fall into two primary categories: Field Data and Lab Data.
Field Data: Understanding Real-User Experiences
Field data, also known as Real User Monitoring (RUM), is collected from actual users visiting your site. This is the most important data because it reflects the diverse devices, network conditions, and user behaviors of your real audience. Core Web Vitals are officially assessed using field data.
- Google Search Console: The Core Web Vitals report in GSC is your primary source of truth. It groups your site’s URLs by status (Poor, Needs Improvement, Good) for each metric on both mobile and desktop. It’s based on data from the Chrome User Experience Report (CrUX). When you fix an issue, you can use this report to validate the fix and request Google to re-evaluate your pages. Its main limitation is that the data is aggregated over the previous 28 days, so it’s not immediate.
- PageSpeed Insights (PSI): The top section of a PSI report, labeled “Discover what your real users are experiencing,” shows CrUX data for the specific URL you’ve entered (if it has sufficient traffic) and its origin. This is a quick way to check the field data for a single, important page.
- CrUX Dashboard: For a more comprehensive view, you can build a custom CrUX Dashboard in Google Data Studio. This provides a free, powerful way to visualize your site’s historical performance trends across all Core Web Vitals and other supplementary metrics like TTFB and FCP.
Lab Data: Debugging in a Controlled Environment
Lab data is collected in a controlled environment with predefined device and network settings. It’s perfect for debugging because it provides reproducible results and detailed diagnostic information. While it doesn’t represent all your users, it’s invaluable for identifying the root cause of a problem found in your field data.
- Lighthouse: This is the quintessential lab tool, available directly in Chrome DevTools (under the “Lighthouse” tab). It runs a series of audits on your page and provides a performance score. While Lighthouse doesn’t measure INP directly, it has a Total Blocking Time (TBT) metric that is an excellent lab proxy. TBT measures the total time that the main thread was blocked during page load, preventing it from responding to input. It also provides a CLS score and LCP time, along with specific optimization opportunities.
- PageSpeed Insights (PSI): The bottom section of the PSI report, labeled “Diagnose performance issues,” is a lab test powered by Lighthouse. It provides the same rich diagnostic information, including a waterfall chart of network requests and specific audits with recommendations.
- Chrome DevTools Performance Panel: This is the most powerful tool for deep-dive debugging, especially for INP and TBT. You can record a performance profile while interacting with your page to see exactly what’s happening on the main thread. You can identify long tasks, see their breakdown (scripting, rendering, painting), and pinpoint the exact functions causing the bottleneck.
- WebPageTest: A highly advanced tool that allows you to test your site from various locations, on different devices, and with custom network conditions. It provides extremely detailed waterfall charts, filmstrip views of the page loading process, and Core Web Vitals measurements. It’s excellent for diagnosing complex loading issues and the impact of third-party scripts.
Debugging Specific Vitals in Chrome DevTools
- For LCP: Use the Performance panel. Load the page with profiling on, and in the “Timings” lane, you’ll see an LCP marker. Hovering over it will highlight the LCP element in the main window and show you a breakdown of its loading time (TTFB, resource load delay, resource load time).
- For INP: Use the Performance panel. Start recording, then perform an interaction on your page (e.g., click a menu button). Stop recording. Look for long tasks (those with a red triangle) in the main thread overview. The “Interactions” lane will group all events related to your click, and you can see the full latency from input to the next paint.
- For CLS: Use the Rendering tab in DevTools. Check the “Layout Shift Regions” box. Now, as you interact with your page, any elements that shift will be briefly highlighted in blue. For a more detailed analysis, use the Performance panel. Record a profile of the page load. In the “Experience” lane, you’ll see red blocks representing layout shifts. Clicking on one will give you a “Summary” tab below with details on what element moved from where to where.
Deep Dive: On-Page Strategies for LCP Optimization
Optimizing LCP involves a multi-faceted approach that addresses every stage of the rendering path, from the initial server request to the final rendering of the largest element.
1. Eliminate Slow Server Response Times (TTFB)
The first bottleneck for LCP is often the server itself. If the browser has to wait a long time just to get the first byte of the HTML document, every subsequent step is delayed.
- Upgrade Your Hosting: Shared hosting is cheap but often slow due to resource contention. If your TTFB is consistently high (above 600ms), consider upgrading to a Virtual Private Server (VPS), a dedicated server, or a managed host specializing in your platform (e.g., Kinsta or WP Engine for WordPress).
- Implement a Content Delivery Network (CDN): A CDN is a network of servers distributed globally. It caches your static assets (HTML, CSS, JS, images) closer to your users. When a user requests your page, they are served by the nearest CDN server (edge server), dramatically reducing network latency. This is one of the single most effective ways to improve TTFB for a global audience. Services like Cloudflare, Fastly, and AWS CloudFront are popular choices.
- Leverage Caching:
- Page Caching: For dynamic sites (like those built on WordPress or Magento), generating a page from scratch for every visitor is slow. Page caching saves a fully-rendered static HTML version of the page. When a user requests it, the server can send the pre-built file instantly, bypassing slow database queries and PHP processing. Most managed hosts and caching plugins (e.g., WP Rocket) handle this.
- Browser Caching: Use the
Cache-Control
HTTP header to instruct the browser to store static assets locally. For returning visitors, the browser can load these assets from its local cache instead of re-downloading them, making subsequent page loads nearly instant. A typical setting for static assets like CSS and JS might beCache-Control: public, max-age=31536000
(one year).
- Optimize Your Database: Slow database queries can cripple TTFB on dynamic websites. Regularly clean and optimize your database, use an object cache like Redis or Memcached to cache frequent query results, and ensure your database tables are properly indexed.
2. Eradicate Render-Blocking Resources
By default, CSS and JavaScript are render-blocking. The browser pauses rendering while it downloads and processes them. Deferring the non-essential ones is crucial.
Minify CSS and JavaScript: Minification removes all unnecessary characters (whitespace, comments, newlines) from code without changing its functionality. This reduces file size, leading to faster downloads. Most build tools (like Webpack, Vite) and optimization plugins do this automatically.
Inline Critical CSS: Identify the minimum CSS required to render the above-the-fold content of your page. This “critical CSS” should be placed directly inside a
tag in the
of your HTML. This allows the browser to start rendering the visible part of the page immediately, without waiting for an external CSS file to download. Tools like the “Penthouse” or “Critical” libraries can automate this process.
Load Non-Critical CSS Asynchronously: The rest of your CSS, which styles content below the fold or on other pages, can be loaded in a non-blocking way. A common technique is:
This initially tells the browser the stylesheet is for
print
(low priority), and once it loads (onload
), themedia
attribute is switched toall
, applying it to the screen. Thetag provides a fallback.
Defer or Async Non-Critical JavaScript: For JavaScript, you have two primary attributes for the
tag:
defer
: Downloads the script in parallel with HTML parsing but guarantees execution after the HTML has been fully parsed, and in the order they appear in the document. This is the best choice for scripts that need the full DOM and whose execution order matters.async
: Downloads the script in parallel but executes it as soon as it’s finished downloading, which can be at any point, potentially interrupting HTML parsing. This is best for independent, third-party scripts where order doesn’t matter, like analytics.
Example:
3. Accelerate Resource Load Times
If the LCP element itself is a large image or web font, its own download time can be the primary bottleneck.
- Master Image Optimization:
- Choose the Right Format:
- WebP: Offers excellent compression for both photos and graphics, with support for transparency and animation. It’s the go-to format for the web, supported by all modern browsers.
- AVIF: A newer format that offers even better compression than WebP, especially at lower quality settings. Support is good and growing.
- JPEG: Still the best for photographic images if you need to support older browsers.
- PNG: Use for graphics requiring a lossless transparent background.
- SVG: Use for logos and icons. As a vector format, it scales infinitely without quality loss and is usually very small in file size.
- Compress Aggressively: Use tools like Squoosh, ImageOptim, or automated services (e.g., Imagify, ShortPixel) to compress your images. Finding the right balance between file size and visual quality is key. A quality setting of 75-85 for JPEGs/WebPs is often a good starting point.
- Implement Responsive Images: Don’t serve a massive 2000px desktop image to a user on a 360px wide mobile screen. Use the
element or the
srcset
andsizes
attributes on the
tag to provide the browser with multiple image sizes. The browser will then choose the most appropriate one based on the device’s screen size and resolution. - Lazy Load Offscreen Images: Lazy loading defers the loading of below-the-fold images until the user scrolls near them. This prioritizes the loading of critical, above-the-fold content, improving LCP. Native lazy loading is now widely supported and incredibly easy to implement:
Important: Never apply
loading="lazy"
to your LCP image. It will delay its discovery and make your LCP worse. The LCP element should always be loaded eagerly.
- Choose the Right Format:
- Optimize Web Fonts:
- Preload Key Fonts: If a web font is used for the LCP element (e.g., the main headline), its loading can delay LCP. You can instruct the browser to fetch it with high priority by preloading it in the
:
The
crossorigin
attribute is essential, even for self-hosted fonts. - Use
font-display: swap;
: This CSS property tells the browser to initially display text using a fallback system font, and then “swap” to the web font once it has loaded. This ensures text is visible immediately (improving perceived performance) but can cause a layout shift (CLS), which must be managed. - Self-Host Fonts: While convenient, using services like Google Fonts adds an extra DNS lookup and connection to a third-party domain. Self-hosting your fonts on your own server or CDN can be faster.
- Subset Fonts: If you only use a few characters from a font file (e.g., for a logo or specific headlines), you can create a “subset” of the font that only includes the glyphs you need. This can dramatically reduce the font file size.
- Preload Key Fonts: If a web font is used for the LCP element (e.g., the main headline), its loading can delay LCP. You can instruct the browser to fetch it with high priority by preloading it in the
4. Address Client-Side Rendering (CSR) Issues
Websites built with frameworks like React, Angular, or Vue often render content on the client’s device using JavaScript. This can lead to a blank page until a large JavaScript bundle is downloaded and executed, severely delaying LCP.
- Embrace Pre-rendering: Instead of sending an empty HTML shell, send a fully or partially rendered HTML page from the server.
- Server-Side Rendering (SSR): The server renders the initial HTML for each request and sends it to the client. This provides a fast FCP and LCP. Frameworks like Next.js (for React) and Nuxt.js (for Vue) make SSR easier to implement.
- Static Site Generation (SSG): For content that doesn’t change often, you can pre-build every page into a static HTML file at build time. This offers the fastest possible performance as there’s no server-side rendering to do on-demand.
- Implement Code Splitting: Break up your large JavaScript bundle into smaller chunks. Load the essential JavaScript needed for the initial page view first, and then lazy-load the rest as the user navigates to different routes or interacts with features that require them. This reduces the initial JS payload, freeing up the main thread and speeding up LCP.
Deep Dive: On-Page Strategies for INP Optimization
Improving INP is all about reducing the time it takes to process an interaction and provide visual feedback. This means optimizing JavaScript execution and minimizing main thread work.
1. Identify and Break Up Long Tasks
The primary cause of high INP is a blocked main thread, typically by a long-running JavaScript task. The goal is to break these “long tasks” (any task taking more than 50 milliseconds) into smaller pieces, giving the browser opportunities to handle user input in between.
Yield to the Main Thread: Manually insert points in your long-running code where you “yield” control back to the browser. The simplest way is with
setTimeout
with a 0-millisecond delay. This queues the remainder of the function to run in a subsequent task, after the browser has had a chance to process other things, like user input.Before (Long Task):
function processLargeArray(items) { for (let i = 0; i < items.length; i++) { // Expensive operation on each item processItem(items[i]); } }
After (Yielding with
setTimeout
):function processLargeArray(items) { let i = 0; function processChunk() { const CHUNK_SIZE = 100; const end = Math.min(i + CHUNK_SIZE, items.length); for (; i < end; i++) { processItem(items[i]); } if (i < items.length) { setTimeout(processChunk, 0); // Yield! } } processChunk(); }
Use
isInputPending()
: A more advanced technique is to use theisInputPending()
API. This allows you to check if there is a pending user input event without yielding unnecessarily. This can be more efficient than blindly yielding after a set number of operations.
2. Optimize JavaScript Payloads and Execution
Less JavaScript means less to download, parse, and execute, leading to a more responsive main thread.
- Code Splitting and Tree Shaking: As mentioned for LCP, these are also critical for INP. Code splitting divides your code into necessary vs. lazy-loaded chunks. Tree shaking is a process used by modern bundlers (like Webpack or Rollup) to automatically eliminate unused code (dead code) from your final bundles, making them smaller.
- Optimize Event Listeners:
- Debounce and Throttle: For events that can fire rapidly (like
scroll
,resize
, orkeyup
), attaching an expensive event handler directly can overwhelm the main thread.- Throttling: Ensures the function is called at most once per a specified time interval (e.g., every 200ms). Good for things like tracking scroll position.
- Debouncing: Ensures the function is only called after a period of inactivity. Good for search input fields, where you only want to fire the search request after the user has stopped typing.
- Use Passive Event Listeners: For scroll and touch event listeners that don’t need to call
preventDefault()
, add thepassive: true
option. This tells the browser that the listener won’t block scrolling, allowing it to proceed smoothly on a separate thread even if your listener code is slow.document.addEventListener('touchstart', myTouchHandler, { passive: true });
- Debounce and Throttle: For events that can fire rapidly (like
3. Minimize Main Thread Work and Rendering Complexity
Beyond just JavaScript, the work the browser has to do to render updates can also contribute to interaction latency.
Offload to Web Workers: For computationally intensive tasks that don’t need direct DOM access (e.g., complex data processing, cryptography, parsing large files), use Web Workers. A Web Worker runs a script in a background thread, completely separate from the main thread. This allows your page to remain fully responsive while the heavy lifting happens in the background.
Avoid Layout Thrashing: Layout thrashing occurs when you repeatedly write and then read from the DOM in a loop. For example, changing a style on an element (a write) and then immediately reading its
offsetHeight
(a read). This forces the browser to perform a synchronous layout calculation in every iteration. Instead, batch your reads first, and then batch your writes.Bad (Layout Thrashing):
function resizeElements(elements) { elements.forEach(el => { const width = container.offsetWidth; // Read el.style.width = (width / 2) + 'px'; // Write }); }
Good (Batched Read/Write):
function resizeElements(elements) { const width = container.offsetWidth; // Read once elements.forEach(el => { el.style.width = (width / 2) + 'px'; // Write multiple times }); }
Simplify CSS Selectors and Reduce Style Complexity: Overly complex CSS selectors (e.g.,
nav > ul > li > a.active
) can take longer for the browser to match during style recalculation. Similarly, complex CSS properties likefilter
orbox-shadow
can be more expensive to render. Keep selectors simple and use expensive properties judiciously.
4. Audit and Control Third-Party Scripts
Third-party scripts for ads, analytics, A/B testing, and social media widgets are a notorious source of high INP. They often execute long tasks on the main thread that you have no direct control over.
- Audit and Remove: Regularly audit the third-party scripts on your site. Use tools like WebPageTest or the DevTools Network panel to identify them and their performance impact. Ask the critical question: is the value this script provides worth the performance cost? Remove any that are non-essential.
- Load Asynchronously: Always load third-party scripts with
async
ordefer
so they don’t block the initial page render. - Use Facades: For heavy embeds like YouTube videos or live chat widgets, use an “interaction facade.” Instead of loading the full embed immediately, display a lightweight placeholder that looks like the real thing (e.g., a static image of the video player with a play button). Only load the actual heavy third-party code when the user clicks on the facade. This drastically improves initial load performance and only incurs the cost for users who actually engage with the widget.
Deep Dive: On-Page Strategies for CLS Optimization
Ensuring visual stability is about communicating layout intentions to the browser before all resources have loaded. The goal is to reserve space for content so it doesn’t pop in and push other elements around.
1. Provide Dimensions for All Media
This is the most common and easiest CLS issue to fix.
- Images: Always provide
width
andheight
attributes on your
andelements. The browser uses these attributes to calculate the aspect ratio and reserve the correct amount of space in the layout before the image has even started downloading.
Even with responsive images using
srcset
, thewidth
andheight
attributes are still crucial for the browser to reserve the initial space. Modern CSS can then handle the resizing:img { max-width: 100%; height: auto; }
- Use the
aspect-ratio
CSS Property: For cases where specifyingwidth
andheight
is difficult (e.g., a container whose width is determined by the grid), theaspect-ratio
CSS property is a modern and powerful alternative. It explicitly tells the browser to maintain a specific aspect ratio for an element..responsive-container { width: 100%; aspect-ratio: 16 / 9; }
2. Reserve Space for Ads, Embeds, and Iframes
These dynamically loaded elements are a major source of CLS. The key is to style their container to reserve the necessary space before the ad or embed script executes.
- Static Sizing: If you know the ad slot or embed will always be a specific size, simply style the container div with a fixed
min-height
andmin-width
. - Dynamic Sizing: Often, the size of an ad isn’t known beforehand. In this case, your best strategy is to reserve space for the largest possible ad size that could be served in that slot. If no ad is served, you can then collapse the container. While not perfect, this prevents the jarring shift of content being pushed down.
- Historical Data: Analyze your ad provider’s reports to see the most common ad dimensions served to a particular slot. You can then use this data to set a more accurate
min-height
for the container.
3. Manage Dynamically Injected Content
Content that appears after the initial page load, like cookie banners, subscription pop-ups, or “related posts” carousels, must be handled carefully.
- Avoid Inserting Content Above Existing Content: The golden rule is to never inject new content above existing, visible content, unless it is in response to a direct user interaction.
- Use Overlays or Modals: For things like cookie consent banners or promotional pop-ups, display them as an overlay on top of the page content rather than pushing the content down. This does not affect the layout of the underlying page and therefore does not contribute to CLS.
- Reserve Space: If you must inject content into the flow of the document (e.g., a “special offer” banner at the top), reserve the space for it from the very beginning with a placeholder container, similar to the ad-slot technique. The content can then load into this pre-allocated space.
4. Mitigate Layout Shifts from Web Fonts (FOIT/FOUT)
While font-display: swap
is great for making text visible quickly, the switch from the fallback font to the web font can cause a layout shift if their sizes differ.
- Preload Your Key Fonts: As mentioned for LCP, preloading the font used for the most important above-the-fold text significantly reduces the time window for a shift to occur. The font loads faster, so the “swap” happens earlier and is less noticeable.
- Use the
font-display
Descriptor API: Modern CSS offers new descriptors to pair with@font-face
rules to minimize the size difference between the fallback and web font. Properties likesize-adjust
,ascent-override
,descent-override
, andline-gap-override
allow you to fine-tune the metrics of the fallback font to more closely match the web font. This is an advanced technique but can nearly eliminate font-related CLS./* In your @font-face rule for the fallback font */ @font-face { font-family: 'Inter-fallback'; src: local('Arial'); /* Use a common local font */ size-adjust: 98%; /* Tweak metrics to match the web font */ ascent-override: 90%; }
This requires careful calibration using font analysis tools but offers the most robust solution.
5. Animate with Compositor-Friendly Properties
Animations can cause layout shifts if you animate properties that affect layout, such as width
, height
, top
, left
, or margin
. Each frame of the animation forces the browser to recalculate the layout of the page.
Use
transform
andopacity
: Whenever possible, use CSStransform
for animations and transitions. Properties liketransform: translateX()
,transform: scale()
, andopacity
are handled by the browser’s compositor thread. This means they don’t trigger a layout recalculation. The browser can simply move the element’s layer as a bitmap on the GPU, which is incredibly fast and efficient, and guaranteed not to cause layout shifts on other elements.Bad (Triggers Layout):
.slide-in { animation: slide 1s ease-out forwards; } @keyframes slide { from { left: -100%; } to { left: 0; } }
Good (Compositor-Only):
.slide-in { animation: slide 1s ease-out forwards; } @keyframes slide { from { transform: translateX(-100%); } to { transform: translateX(0); } }
This shift in mindset from manipulating layout properties to using transforms is fundamental for creating performant, stable animations on the web.