How We Hit Lighthouse 95 on a 230KB Next.js Homepage
230KB total page weight. Mobile Lighthouse 95. We started at 58 and fixed it in six weeks. Here is the exact build config and every tradeoff we made.
230KB total page weight. Mobile Lighthouse 95. We started at 58 and fixed it in six weeks. Here is the exact build config and every tradeoff we made.
Our Striveloom homepage went from a mobile Lighthouse performance score of 58 to 95 in six weeks. Total page weight dropped from 890KB to 230KB. LCP went from 4.3 seconds to 1.7 seconds on mobile. These are our actual PageSpeed Insights field data numbers, not lab simulations.
The changes were not exotic. No CDN rewrite. No edge computing. No server-side rendering changes. The gains came from using Next.js features we were already paying for but not using correctly, auditing and removing JavaScript that was adding weight without adding value, and fixing two image problems that were the primary cause of the LCP failure.
Here is exactly what we did, in the order we did it.
Before any changes, our mobile metrics were:
These are genuinely bad numbers for a homepage that is supposed to be a selling tool. A 4.3-second LCP on mobile means most users on an average mobile connection are waiting over four seconds before they see the main content of the page. Core Web Vitals research from web.dev suggests that every second of load time increase correlates with measurable bounce rate increases.
The 58 Lighthouse score put us in the "Needs Improvement" category across every Core Web Vitals metric. Google's field data assessment uses real user data from Chrome users, which means these numbers were affecting our actual search performance, not just a synthetic test.
We made changes in three categories: images, JavaScript, and fonts. Here is the before and after for each:
| Category | Before | After | Change Made |
|---|---|---|---|
| Hero image format | 1.8MB PNG | 42KB WebP | next/image with quality={80} |
| Hero image loading | No lazy loading | Eager + priority prop | next/image priority flag |
| Hero image dimensions | No width/height | Explicit 1200x630 | Fixed CLS to 0.02 |
| JavaScript bundle | 540KB | 198KB | Removed 3 analytics scripts |
| Font loading | @import in CSS | next/font with preload | Eliminated render block |
| Third-party scripts | 4 (inline loaded) | 1 (strategy="lazyOnload") | Next.js Script component |
| Total page weight | 890KB | 230KB | All of the above |
The hero image was a 1.8MB PNG loaded with a standard HTML img tag. No compression. No format conversion. No size attributes. It was causing the LCP failure almost entirely on its own.
We replaced it with the Next.js Image component (next/image). This component automatically serves WebP to browsers that support it, generates multiple sizes for responsive delivery, and prevents layout shift through the required width and height props. We set quality={80}, which produced a 42KB WebP that is visually indistinguishable from the original on any device we tested.
We also added the priority prop to the hero image. This tells Next.js to preload the image during the initial document fetch rather than waiting for the JavaScript runtime to process the component tree. LCP for the hero image dropped from 4.3 seconds to 1.7 seconds with just these two changes.
Below-the-fold images got the standard lazy loading behavior, which is the Next.js Image default. No additional configuration needed.
Our 540KB JavaScript bundle included:
We were running Google Analytics 4, a heatmap tool, and a live chat widget simultaneously. All three were loading synchronously in the document head, blocking rendering. None of them were being actively used by the team for any decision-making. The heatmap tool had not been checked in four months. The live chat widget was generating two conversations per month at a cost of $79/month and adding 140KB to every page load.
We removed all three. We added back Google Analytics 4 using Next.js Script component with strategy="lazyOnload", which delays loading until after the page is interactive. The heatmap tool was not re-added. The live chat widget was not re-added.
JavaScript bundle went from 540KB to 198KB. INP dropped from 310ms to 87ms.
This is the hard conversation most performance optimization projects avoid: the fastest JavaScript is the JavaScript you do not ship. That requires removing tools, which requires stakeholder agreement that those tools were not delivering value. In our case, the data was clear. We ran three months without the heatmap and saw no loss of insight we could measure.
We were loading our brand font using @import in a global CSS file. This is a render-blocking operation: the browser cannot render text until the font file downloads. We were serving fonts from a Google Fonts CDN URL, which meant an external DNS lookup was also required before the font bytes started transferring.
We switched to next/font/google, which is Next.js's built-in font optimization system. It downloads the font at build time, serves it from the same domain as the application (eliminating the external DNS lookup), and generates a size-adjust fallback font so text renders immediately with the system font while the brand font loads. No visible flash. No layout shift.
This change eliminated the render-blocking CSS import and reduced font-related page weight by 18KB. The improvement on Lighthouse's "Eliminate render-blocking resources" audit was immediate.
Being specific about what we kept and what we removed is important here, because performance optimization without product tradeoffs is not realistic advice.
We kept:
We removed:
We also moved three large components to dynamic imports so they only load when the route that uses them loads. Our team page was importing a mapping library on every page because it was in the global component tree. Moving it to a dynamic import with next/dynamic reduced the homepage bundle by 28KB.
Our CLS of 0.22 came from two sources: the hero image with no width/height attributes (content shifted down when the image loaded), and a promotional banner that loaded after the initial paint (pushing the navigation down by 48px).
The image fix: adding width and height to the Next.js Image component, which reserves space before the image loads.
The banner fix: rendering the banner on the server with a fixed height container so the space is reserved in the HTML rather than created by JavaScript after load.
CLS dropped to 0.02.
Six weeks of focused work:
Our Core Web Vitals assessment in Search Console moved from "Failing" to "Passing" within 28 days of the field data updating. Organic impressions on the homepage improved by 23% in the following 60 days. We cannot attribute all of that to Core Web Vitals directly — we made other SEO changes in the same period — but the correlation is consistent with what the research suggests about performance as a ranking signal.
Most Next.js sites have the same four problems we had:
Fixing all four is achievable in a single sprint using only built-in Next.js features. No new infrastructure. No new dependencies. The main cost is the conversation about removing tools that add weight.
If you want help running this audit on your site, our web development team does performance audits and Core Web Vitals remediation for Next.js and other modern stacks. The 230KB homepage is also the one you're looking at right now — the browser dev tools are open to anyone who wants to verify the numbers.
Google's Core Web Vitals thresholds define 'Good' as LCP under 2.5s, CLS under 0.1, and INP under 200ms. A Lighthouse Performance score above 90 on mobile is generally considered strong. Scores above 95 are achievable but require significant optimization effort. More important than the Lighthouse lab score is the field data in Google Search Console's Core Web Vitals report, which reflects real user experiences across all Chrome users visiting your site and is the signal Google actually uses for search ranking.
The Next.js Image component (next/image) improves Lighthouse scores through automatic WebP conversion (serving smaller file sizes to supporting browsers), responsive image sizing (serving appropriately sized images for each viewport), lazy loading by default for below-the-fold images, and CLS prevention through required width and height attributes that reserve space before the image loads. The priority prop adds preload hints for above-the-fold images that are the Largest Contentful Paint element, which directly improves LCP timing.
The fastest approach is auditing what is in the bundle using Next.js Bundle Analyzer (@next/bundle-analyzer). Run the build with ANALYZE=true and examine the treemap to identify the largest modules. Common findings include third-party analytics scripts that could be loaded lazily, libraries imported in full when only one function is used, and components that are in the global component tree but only needed on specific routes. Moving route-specific components to dynamic imports with next/dynamic is often the single largest win.
Lighthouse score is a diagnostic tool — Google does not use it directly as a ranking signal. What Google uses is Core Web Vitals field data from real Chrome users, which it collects through the Chrome User Experience Report (CrUX). A Lighthouse lab score improvement often correlates with a field data improvement, but not always, because field data reflects actual user conditions including network speed, device capability, and geographic distribution. Focus on getting Core Web Vitals to 'Good' in Search Console's field data report, not on hitting a specific Lighthouse number.
Use the Next.js Script component with strategy='lazyOnload'. This delays loading the analytics script until after the page is fully interactive, preventing it from blocking the initial render. In Next.js App Router, place the Script component in the root layout. For GA4 specifically, the gtag.js script adds approximately 45KB. Loading it lazily keeps it out of the critical path. If you are concerned about missing early user events, GA4's event model handles this gracefully — most meaningful interactions happen after the page is interactive.
In order of typical impact: the Image component for any site with above-the-fold images, next/font for sites using custom fonts, the Script component with lazyOnload for third-party scripts, dynamic imports (next/dynamic) for large components only used on specific routes, and App Router server components for reducing client-side JavaScript by moving data fetching to the server. Image optimization and font optimization together typically account for the largest page weight reductions. Third-party script management typically accounts for the largest INP improvements.
Founder & CEO of Striveloom. Software engineer and Harvard graduate student researching software engineering, e-commerce platforms, and customer experience. Builds the agency that ships like software — one team, one pipeline, one platform. Writes on AI agencies, web development, paid advertising, and conversion optimization.
Everything a startup founder needs to know about building a website in 2026 — tech stack choices, realistic budgets, timelines, and the mistakes that kill product launches.
Every tool Striveloom uses to run a 7-person digital agency in 2026, with actual monthly costs. $1,847/month total. Here is the full bill and why we chose each one.
We made all 14 SEO mistakes on our own website before fixing them. Here is the before/after data, what each mistake costs, and how to fix each one.
Book a free 30-minute call to scope your project. Fixed pricing, transparent timelines.