{"id":34150,"date":"2026-09-24T10:30:28","date_gmt":"2026-09-24T05:00:28","guid":{"rendered":"https:\/\/www.vocso.com\/blog\/?p=34150"},"modified":"2026-09-24T10:40:45","modified_gmt":"2026-09-24T05:10:45","slug":"nextjs-core-web-vitals-performance","status":"publish","type":"post","link":"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/","title":{"rendered":"Next.js Performance: How to Pass Core Web Vitals (LCP, INP, CLS)"},"content":{"rendered":"<div style=\"margin-top: 0px; margin-bottom: 0px;\" class=\"sharethis-inline-share-buttons\" ><\/div>\n<p>To pass Core Web Vitals on a Next.js site, render above-the-fold content on the server, load the hero image and fonts early with <code>next\/image<\/code> and <code>next\/font<\/code>, keep client JavaScript to a minimum with Server Components, and reserve space for everything that loads late. Google&#8217;s &#8220;good&#8221; thresholds are LCP \u2264 2.5 s, INP \u2264 200 ms and CLS \u2264 0.1, measured at the 75th percentile of real visits. Most failing sites miss them for a few repeatable reasons, each with a framework-level fix.<\/p>\n\n\n\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_81 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title ez-toc-toggle\" style=\"cursor:pointer\">Table of Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#measure-first-field-data-vs-lab-data\" >Measure first: field data vs lab data<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#collect-your-own-field-data-with-usereportwebvitals\" >Collect your own field data with useReportWebVitals<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#fixing-lcp-largest-contentful-paint\" >Fixing LCP (Largest Contentful Paint)<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#load-the-hero-image-early-with-nextimage\" >Load the hero image early with next\/image<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#self-host-fonts-with-nextfont\" >Self-host fonts with next\/font<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#render-above-the-fold-content-on-the-server\" >Render above-the-fold content on the server<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#stream-the-slow-parts\" >Stream the slow parts<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#cache-aggressively-and-serve-from-a-cdn\" >Cache aggressively and serve from a CDN<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#fixing-inp-interaction-to-next-paint\" >Fixing INP (Interaction to Next Paint)<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#ship-less-javascript-with-server-components\" >Ship less JavaScript with Server Components<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#defer-heavy-client-code-with-dynamic-imports\" >Defer heavy client code with dynamic imports<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#control-third-party-scripts-with-nextscript\" >Control third-party scripts with next\/script<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#break-up-long-tasks\" >Break up long tasks<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#fixing-cls-cumulative-layout-shift\" >Fixing CLS (Cumulative Layout Shift)<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#give-every-image-dimensions\" >Give every image dimensions<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#match-font-fallbacks\" >Match font fallbacks<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#reserve-space-for-embeds-ads-and-late-content\" >Reserve space for embeds, ads and late content<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-18\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#bundle-analysis-find-what-you-are-shipping\" >Bundle analysis: find what you are shipping<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-19\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#middleware-cost\" >Middleware cost<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-20\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#diagnosis-table-symptom-likely-cause-nextjs-fix\" >Diagnosis table: symptom, likely cause, Next.js fix<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-21\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#a-practical-order-of-work\" >A practical order of work<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-22\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#how-vocso-can-help\" >How VOCSO can help<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-23\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#faq\" >FAQ<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-24\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#what-are-the-%e2%80%9cgood%e2%80%9d-core-web-vitals-thresholds\" >What are the &#8220;good&#8221; Core Web Vitals thresholds?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-25\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#my-lighthouse-score-is-high-so-why-does-search-console-say-my-pages-fail\" >My Lighthouse score is high, so why does Search Console say my pages fail?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-26\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#how-long-does-it-take-for-fixes-to-show-in-search-console\" >How long does it take for fixes to show in Search Console?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-27\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#does-using-the-app-router-automatically-make-my-site-fast\" >Does using the App Router automatically make my site fast?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-28\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#should-i-use-nextimage-for-every-image\" >Should I use next\/image for every image?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-29\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#is-inp-affected-by-the-server\" >Is INP affected by the server?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-30\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/#related-reading\" >Related reading<\/a><\/li><\/ul><\/li><\/ul><\/nav><\/div>\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"measure-first-field-data-vs-lab-data\"><\/span>Measure first: field data vs lab data<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Core Web Vitals are judged on <strong>field data<\/strong>: measurements from real Chrome users, collected in the Chrome User Experience Report (CrUX). Search Console and the top of PageSpeed Insights both use CrUX, which covers a rolling 28-day window.<\/p>\n\n\n\n<p><strong>Lab data<\/strong>, from Lighthouse or the Performance panel in Chrome DevTools, runs a single simulated load on a fixed device and network profile. It is ideal for debugging, but it is not what Google uses to assess your pages. It also cannot measure INP during a page load, because INP needs real interactions; Total Blocking Time is the closest lab proxy.<\/p>\n\n\n\n<figure class=\"wp-block-table table table-bordered\"><table><tbody><tr><td><strong>Source<\/strong><\/td><td><strong>Type<\/strong><\/td><td><strong>Best used for<\/strong><\/td><\/tr><tr><td>Search Console (Core Web Vitals report)<\/td><td>Field (CrUX)<\/td><td>Which URL groups fail, and on mobile or desktop<\/td><\/tr><tr><td>PageSpeed Insights<\/td><td>Field + lab<\/td><td>Per-URL field data plus a Lighthouse run with diagnostics<\/td><\/tr><tr><td>Lighthouse \/ DevTools Performance panel<\/td><td>Lab<\/td><td>Finding the LCP element, long tasks and layout shifts locally<\/td><\/tr><tr><td><code>useReportWebVitals<\/code> + your analytics<\/td><td>Field (your own RUM)<\/td><td>Per-route, per-release data without waiting 28 days<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"collect-your-own-field-data-with-usereportwebvitals\"><\/span>Collect your own field data with useReportWebVitals<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Next.js exposes a <code>useReportWebVitals<\/code> hook from <code>next\/web-vitals<\/code>. Put it in a small Client Component and render that once in the root layout.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app\/_components\/web-vitals.tsx\n'use client';\n\nimport { useReportWebVitals } from 'next\/web-vitals';\n\nexport function WebVitals() {\n  useReportWebVitals((metric) =&gt; {\n    const body = JSON.stringify({\n      name: metric.name,     \/\/ 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB'\n      value: metric.value,\n      id: metric.id,\n      path: window.location.pathname,\n    });\n    navigator.sendBeacon?.('\/api\/vitals', body);\n  });\n  return null;\n}\n\n\/\/ app\/layout.tsx: render &lt;WebVitals \/&gt; inside &lt;body&gt;<\/code><\/pre>\n\n\n\n<p>Segment the values by route and device, and read the 75th percentile, not the average.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"fixing-lcp-largest-contentful-paint\"><\/span>Fixing LCP (Largest Contentful Paint)<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>LCP measures when the largest image or text block in the viewport renders, usually a hero image or headline. Split it into server response time (TTFB), resource load delay, download time and render delay; each has a different fix.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"load-the-hero-image-early-with-nextimage\"><\/span>Load the hero image early with next\/image<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p><code>next\/image<\/code> lazy-loads images by default, which is right for images further down the page and wrong for the LCP image. Mark the hero with <code>priority<\/code> so Next.js preloads it and skips lazy loading. Give it an accurate <code>sizes<\/code> value so the browser picks a sensibly sized file from the generated <code>srcset<\/code> instead of the largest one.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import Image from 'next\/image';\nimport hero from '@\/public\/hero.jpg';\n\nexport function Hero() {\n  return (\n    &lt;Image\n      src={hero}\n      alt=\"Students comparing colleges\"\n      priority\n      sizes=\"(max-width: 768px) 100vw, 50vw\"\n    \/&gt;\n  );\n}<\/code><\/pre>\n\n\n\n<p>Use <code>priority<\/code> on one or two images per page, not all of them. The prop naming has changed in recent releases, so check the docs for your version. Avoid CSS <code>background-image<\/code> heroes: the browser discovers them late.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"self-host-fonts-with-nextfont\"><\/span>Self-host fonts with next\/font<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>When the LCP element is text, font loading decides when it paints. <code>next\/font<\/code> downloads Google Fonts (or your local files) at build time and serves them from your own domain, so there is no extra connection to a third-party font host. It also preloads the font files and uses <code>font-display: swap<\/code> by default.<\/p>\n\n\n\n<p>Import the font once in the root layout, apply its <code>className<\/code> to <code>&lt;html&gt;<\/code>, and load only the subsets and weights you use. A variable font is usually lighter than several static weights.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"render-above-the-fold-content-on-the-server\"><\/span>Render above-the-fold content on the server<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>A very common LCP failure is a page that fetches its main content in a <code>useEffect<\/code>. The headline waits for HTML, JavaScript, hydration and an API call. With the App Router, fetch the data in a Server Component instead so the content arrives in the initial HTML:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app\/courses\/[slug]\/page.tsx\nexport default async function CoursePage({\n  params,\n}: {\n  params: Promise&lt;{ slug: string }&gt;;\n}) {\n  const { slug } = await params;\n  const course = await getCourse(slug); \/\/ runs on the server\n  return (\n    &lt;article&gt;\n      &lt;h1&gt;{course.title}&lt;\/h1&gt;\n      &lt;p&gt;{course.summary}&lt;\/p&gt;\n    &lt;\/article&gt;\n  );\n}<\/code><\/pre>\n\n\n\n<p>(In older versions <code>params<\/code> is a plain object rather than a Promise; follow the signature your version expects.) Moving from a client-rendered React app? Our guide on <a href=\"https:\/\/www.vocso.com\/blog\/migrating-to-next-js-from-reactjs\/\">migrating from React to Next.js<\/a> covers the rest.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"stream-the-slow-parts\"><\/span>Stream the slow parts<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>If one section depends on a slow API, such as reviews or recommendations, do not make the whole response wait. Wrap it in <code>&lt;Suspense&gt;<\/code> (or add a <code>loading.tsx<\/code> for the route segment) so Next.js streams the fast shell, including the LCP element, first and fills in the slow section when it is ready. Make the fallback the same size as the final content, or you swap an LCP problem for a CLS problem.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"cache-aggressively-and-serve-from-a-cdn\"><\/span>Cache aggressively and serve from a CDN<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>A slow TTFB puts a floor under LCP. Pages that do not change per user should be static or use ISR: set <code>export const revalidate = 3600<\/code> on the route, or pass <code>{ next: { revalidate: 3600 } }<\/code> to individual <code>fetch<\/code> calls, and pre-build known paths with <code>generateStaticParams<\/code>. Caching defaults for <code>fetch<\/code> have changed between major versions, so check whether your data is actually being cached rather than assuming it is. Put a CDN in front as well. We compare the rendering options in <a href=\"https:\/\/www.vocso.com\/blog\/static-site-generation-ssg-vs-server-side-rendering-ssr-in-nextjs-which-is-best-for-seo\/\">SSG vs SSR in Next.js<\/a>, and cover CDN setup in <a href=\"https:\/\/www.vocso.com\/blog\/deploying-a-next-js-application-on-aws-step-by-step-guide\/\">deploying a Next.js application on AWS<\/a>.<\/p>\n\n\n\n<p>Watch for anything that silently makes a route dynamic, such as reading <code>cookies()<\/code> or <code>headers()<\/code> in a shared layout. The build output shows which routes are static.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"fixing-inp-interaction-to-next-paint\"><\/span>Fixing INP (Interaction to Next Paint)<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>INP measures how quickly the page responds visually to clicks, taps and key presses. Poor INP usually means a busy main thread: too much JavaScript to hydrate, heavy event handlers or third-party scripts.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"ship-less-javascript-with-server-components\"><\/span>Ship less JavaScript with Server Components<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>App Router components are Server Components by default and send no component JavaScript to the browser. <code>'use client'<\/code> marks a boundary: that file and everything it imports join the client bundle, so a directive at the top of a layout or large page pulls in the whole tree.<\/p>\n\n\n\n<p>Push the boundary down to the leaves: keep pages, layouts and static content on the server, and make only the interactive widget (an add-to-cart button, a filter panel) a Client Component. Server Components can also be passed as <code>children<\/code> into a Client Component, such as a tab switcher wrapping server-rendered panels, without becoming client code themselves.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"defer-heavy-client-code-with-dynamic-imports\"><\/span>Defer heavy client code with dynamic imports<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Load charts, editors, maps and modals on demand with <code>next\/dynamic<\/code> (<code>ssr: false<\/code> only works inside a Client Component):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>'use client';\nimport dynamic from 'next\/dynamic';\n\nconst PriceChart = dynamic(() =&gt; import('.\/price-chart'), {\n  ssr: false,\n  loading: () =&gt; &lt;div className=\"h-80\" \/&gt;, \/\/ reserve the chart's height\n});<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"control-third-party-scripts-with-nextscript\"><\/span>Control third-party scripts with next\/script<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Tag managers, chat widgets and ad scripts share your main thread. Load them with <code>next\/script<\/code> and choose the latest strategy you can live with:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><code>beforeInteractive<\/code>: loads before hydration; only for scripts that must run first, such as a consent manager.<\/li><li><code>afterInteractive<\/code> (default): loads after hydration; fine for analytics.<\/li><li><code>lazyOnload<\/code>: loads in idle time; use it for chat widgets and social embeds.<\/li><\/ul>\n\n\n\n<p>Then audit what your tag manager injects; it often loads scripts nobody on the team reviewed.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"break-up-long-tasks\"><\/span>Break up long tasks<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Any task over 50 ms is a long task and delays input handling. Record the interaction in DevTools, find the long tasks after the click, then:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Wrap expensive state updates in <code>useTransition<\/code> so input stays responsive.<\/li><li>Use <code>useDeferredValue<\/code> for search-as-you-type results.<\/li><li>Show visual feedback first, then do the heavy work, yielding to the main thread in between.<\/li><li>Filter or sort large datasets on the server, or paginate.<\/li><li>Virtualise long lists instead of rendering thousands of DOM nodes.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"fixing-cls-cumulative-layout-shift\"><\/span>Fixing CLS (Cumulative Layout Shift)<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>CLS measures unexpected movement of visible content. The fixes are mostly about reserving space.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"give-every-image-dimensions\"><\/span>Give every image dimensions<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p><code>next\/image<\/code> requires <code>width<\/code> and <code>height<\/code> (inferred automatically for static imports) or the <code>fill<\/code> prop, so the browser can reserve the right box before the file loads. With <code>fill<\/code>, the parent must be positioned and have a size, for example with a CSS <code>aspect-ratio<\/code>. Watch for plain <code>&lt;img&gt;<\/code> tags in CMS rich text.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"match-font-fallbacks\"><\/span>Match font fallbacks<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>With <code>font-display: swap<\/code>, text reflows when the web font replaces the fallback. <code>next\/font<\/code> generates a fallback font face with adjusted metrics (<code>size-adjust<\/code> and related descriptors) so both take up nearly the same space. Keep that on, and remove leftover <code>&lt;link&gt;<\/code> or <code>@import<\/code> font loads.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"reserve-space-for-embeds-ads-and-late-content\"><\/span>Reserve space for embeds, ads and late content<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li>Give ad slots, video embeds and iframes a <code>min-height<\/code> or <code>aspect-ratio<\/code> matching their content.<\/li><li>Make <code>Suspense<\/code> and <code>loading.tsx<\/code> skeletons the same size as the final content.<\/li><li>Show cookie banners and promo bars as overlays.<\/li><li>Use CSS media queries, not <code>window.innerWidth<\/code>, to switch layouts.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"bundle-analysis-find-what-you-are-shipping\"><\/span>Bundle analysis: find what you are shipping<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p><code>@next\/bundle-analyzer<\/code> produces a treemap of each client bundle (it targets webpack builds; check the docs for the Turbopack equivalent):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ next.config.ts\nimport bundleAnalyzer from '@next\/bundle-analyzer';\nconst withBundleAnalyzer = bundleAnalyzer({ enabled: process.env.ANALYZE === 'true' });\nexport default withBundleAnalyzer({ \/* your config *\/ });\n\/\/ ANALYZE=true npm run build<\/code><\/pre>\n\n\n\n<p>Typical findings: a whole utility or icon library imported for a few functions, or a server SDK pulled in by a <code>'use client'<\/code> import. Import <code>server-only<\/code> in modules that must never reach the browser so the build fails if they do.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"middleware-cost\"><\/span>Middleware cost<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Middleware runs before every request it matches, even for pages otherwise served from cache. A database lookup or external API call inside it adds directly to TTFB, and so to LCP. (Recent releases have renamed the middleware file convention to <code>proxy<\/code>; the cost model is the same.)<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Use the <code>matcher<\/code> config to limit it to the paths that need it, excluding <code>_next\/static<\/code>, <code>_next\/image<\/code> and public assets.<\/li><li>Keep it to cheap checks such as reading a cookie or redirecting; do data access in the page or layout.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"diagnosis-table-symptom-likely-cause-nextjs-fix\"><\/span>Diagnosis table: symptom, likely cause, Next.js fix<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<figure class=\"wp-block-table table table-bordered\"><table><tbody><tr><td><strong>Symptom<\/strong><\/td><td><strong>Likely cause<\/strong><\/td><td><strong>Next.js fix<\/strong><\/td><\/tr><tr><td>High LCP, high TTFB<\/td><td>Dynamic rendering per request; slow middleware; no CDN<\/td><td>Static rendering or <code>revalidate<\/code>; narrow middleware <code>matcher<\/code>; CDN<\/td><\/tr><tr><td>High LCP, hero image is the LCP element<\/td><td>Hero lazy-loaded, oversized or a CSS background<\/td><td><code>next\/image<\/code> with <code>priority<\/code> and accurate <code>sizes<\/code><\/td><\/tr><tr><td>High LCP, content appears after a spinner<\/td><td>Above-the-fold data fetched in <code>useEffect<\/code><\/td><td>Fetch in a Server Component; stream slower sections with <code>Suspense<\/code><\/td><\/tr><tr><td>High LCP, text is the LCP element<\/td><td>Fonts from a third-party host or too many weights<\/td><td><code>next\/font<\/code> with only the subsets and weights you use<\/td><\/tr><tr><td>Poor INP across the site<\/td><td>Large client bundle; <code>'use client'<\/code> high in the tree<\/td><td>Move <code>'use client'<\/code> to leaf components; bundle analysis; <code>next\/dynamic<\/code><\/td><\/tr><tr><td>Poor INP on one interaction<\/td><td>Long task in an event handler<\/td><td><code>useTransition<\/code>, <code>useDeferredValue<\/code>, server-side work, virtualised lists<\/td><\/tr><tr><td>Poor INP in field, fine in lab<\/td><td>Third-party scripts and tags<\/td><td><code>next\/script<\/code> with <code>lazyOnload<\/code>; prune the tag manager<\/td><\/tr><tr><td>High CLS on load<\/td><td>Images without dimensions; font swap<\/td><td><code>next\/image<\/code> sizing or <code>fill<\/code> with aspect ratio; <code>next\/font<\/code> fallback metrics<\/td><\/tr><tr><td>High CLS after load<\/td><td>Ads, embeds or streamed content inserted without space<\/td><td>Sized placeholders; same-size <code>Suspense<\/code> fallbacks<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"a-practical-order-of-work\"><\/span>A practical order of work<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<ul class=\"wp-block-list\"><li>Find the failing URL groups and device type in Search Console.<\/li><li>Reproduce each issue in DevTools.<\/li><li>Fix templates, not individual pages; one layout change can fix thousands of URLs.<\/li><li>Add a Lighthouse check to CI for regressions, and treat field data as the final verdict.<\/li><\/ul>\n\n\n\n<p>That template-first approach is how we build data-heavy portals. <a href=\"https:\/\/www.vocso.com\/case-study\/schools18-schools-search-website-application-development\">Schools18<\/a>, a search portal covering 19,000+ schools on Strapi and Next.js, was built for programmatic SEO and fast performance with large data.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"how-vocso-can-help\"><\/span>How VOCSO can help<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>VOCSO has been building software since 2009, with 600+ projects delivered, ISO 27001 certification and a 5.0\u2605 rating on Clutch. Our <a href=\"https:\/\/www.vocso.com\/nextjs-development-services\">Next.js development services<\/a> cover Core Web Vitals audits, App Router migrations and performance fixes to existing sites, and our <a href=\"https:\/\/www.vocso.com\/frontend-development-services\">frontend development team<\/a> handles the image, font and JavaScript work that moves these metrics. If you need extra hands on your own team, you can also <a href=\"https:\/\/www.vocso.com\/hire-nextjs-developers\">hire Next.js developers<\/a> from us, under NDA from day one. <a href=\"https:\/\/www.vocso.com\/contact-us\">Contact us<\/a> with your Search Console report and we will tell you where to start.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"faq\"><\/span>FAQ<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"what-are-the-%e2%80%9cgood%e2%80%9d-core-web-vitals-thresholds\"><\/span>What are the &#8220;good&#8221; Core Web Vitals thresholds?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>LCP of 2.5 seconds or less, INP of 200 milliseconds or less and CLS of 0.1 or less, each at the 75th percentile of visits, assessed separately for mobile and desktop.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"my-lighthouse-score-is-high-so-why-does-search-console-say-my-pages-fail\"><\/span>My Lighthouse score is high, so why does Search Console say my pages fail?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Lighthouse is a single lab run on a simulated device. Search Console uses field data from real Chrome users on their own devices and networks, with real interactions. Field data is what counts.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"how-long-does-it-take-for-fixes-to-show-in-search-console\"><\/span>How long does it take for fixes to show in Search Console?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>CrUX uses a rolling 28-day window, so improvements appear gradually over about four weeks. Your own <code>useReportWebVitals<\/code> data shows the change sooner.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"does-using-the-app-router-automatically-make-my-site-fast\"><\/span>Does using the App Router automatically make my site fast?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>No. Server Components make it easier to ship less JavaScript, but a misplaced <code>'use client'<\/code>, client-side data fetching or heavy third-party scripts can still fail every metric.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"should-i-use-nextimage-for-every-image\"><\/span>Should I use next\/image for every image?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>For content images, yes: you get responsive sizes, modern formats and reserved space. Add <code>priority<\/code> to the LCP image and configure <code>remotePatterns<\/code> for CMS-hosted images.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"is-inp-affected-by-the-server\"><\/span>Is INP affected by the server?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Mostly not. INP is dominated by main-thread work on the device. A slow Server Action does not directly hurt INP if you show immediate feedback, such as a pending state, while it runs.<\/p>\n\n\n<!-- vocso-related -->\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"related-reading\"><\/span>Related reading<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/www.vocso.com\/blog\/nextjs-seo-checklist\/\">Next.js SEO: A Technical Checklist for the App Router<\/a><\/li><li><a href=\"https:\/\/www.vocso.com\/blog\/static-site-generation-ssg-vs-server-side-rendering-ssr-in-nextjs-which-is-best-for-seo\/\">Static Site Generation (SSG) vs Server-Side Rendering (SSR) in NextJS: Which is Best for SEO?<\/a><\/li><li><a href=\"https:\/\/www.vocso.com\/blog\/nextjs-vs-reactjs-when-to-use-each-and-why\/\">NextJS vs ReactJS: When to Use Each and Why<\/a><\/li><\/ul>\n\n<!-- \/vocso-related -->","protected":false},"excerpt":{"rendered":"<p>A practical guide to passing Core Web Vitals on a Next.js App Router site: how to measure LCP, INP and CLS properly, and the framework-level fixes for each.<\/p>\n","protected":false},"author":23,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1415],"tags":[],"class_list":["post-34150","post","type-post","status-publish","format-standard","hentry","category-nextjs"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/posts\/34150","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/users\/23"}],"replies":[{"embeddable":true,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/comments?post=34150"}],"version-history":[{"count":2,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/posts\/34150\/revisions"}],"predecessor-version":[{"id":34310,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/posts\/34150\/revisions\/34310"}],"wp:attachment":[{"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/media?parent=34150"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/categories?post=34150"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/tags?post=34150"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}