Next.js 15 Speed Hacks: 7 Tweaks for a Perfect Lighthouse Score

Ralph Sanchez

Next.js 15 Speed Hacks: 7 Tweaks for a Perfect Lighthouse Score

In the competitive digital landscape, website performance is not just a technical metric but a crucial factor for user engagement and SEO ranking. With the release of Next.js 15, developers have more powerful tools than ever to build blazing-fast web applications. This article explores seven practical speed hacks that will help you optimize your Next.js app for a perfect Lighthouse score. We'll cover everything from leveraging the new compiler to advanced image optimization.
For those looking to accelerate their development even further, understanding how to achieve faster builds by slashing build times with Turbopack is a game-changer. If you want to implement these advanced techniques, you can always hire Next.js developers to ensure your project is optimized for performance and success.

Understanding Core Web Vitals in 2025

Before diving into the hacks, it's essential to understand what we're optimizing for. Core Web Vitals have become the gold standard for measuring user experience on the web. These metrics directly impact your Google search rankings and, more importantly, how users perceive your site's performance.
Think of Core Web Vitals as your website's report card. Just like you wouldn't want a failing grade in school, you don't want poor scores here. Google uses these metrics to determine if your site provides a good user experience, and they've made it clear that performance matters for SEO.

Largest Contentful Paint (LCP)

LCP measures how quickly the main content of your page loads. It tracks when the largest element in the viewport becomes visible. This could be a hero image, a video, or a large block of text. Users judge your site's speed based on when they see meaningful content, not when the page technically finishes loading.
Common culprits for poor LCP in Next.js apps include unoptimized images, render-blocking resources, and slow server response times. If your hero image takes five seconds to appear, users might bounce before seeing what you have to offer. The goal is to achieve an LCP of 2.5 seconds or less.

Interaction to Next Paint (INP)

INP replaced First Input Delay (FID) as a Core Web Vital in March 2024, and it's a much more comprehensive metric. While FID only measured the first interaction, INP looks at all user interactions throughout the page's lifetime. It measures the time from when a user interacts with your page until the browser paints the next frame.
This metric is crucial because it reflects real-world responsiveness. When someone clicks a button, they expect immediate feedback. INP captures those frustrating moments when buttons feel sluggish or unresponsive. A good INP score is 200 milliseconds or less.

Cumulative Layout Shift (CLS)

CLS quantifies visual stability by measuring unexpected layout shifts during the page load. Ever tried to click a button only to have it move at the last second? That's what CLS measures. These shifts are incredibly frustrating for users and can lead to accidental clicks on ads or wrong buttons.
Common causes include images without dimensions, dynamically injected content, and web fonts causing text to reflow. A good CLS score is 0.1 or less, meaning your page elements should stay put once they appear.

Hack 1: Embrace the Power of Turbopack

Next.js 15 brings Turbopack to the forefront, offering significant performance gains during development. This Rust-based bundler is designed from the ground up to be incredibly fast, making your development experience smoother and more enjoyable.
Turbopack isn't just marginally faster than webpack—it's dramatically faster. We're talking about startup times measured in milliseconds instead of seconds. For large projects, this difference becomes even more pronounced, potentially saving hours of development time each week.

Enabling Turbopack for Development

Getting started with Turbopack is surprisingly simple. You can enable it by adding a single flag to your development command. In your package.json, update your dev script:
"scripts": {
"dev": "next dev --turbo"
}

Alternatively, you can enable it in your next.config.js:
module.exports = {
experimental: {
turbo: {}
}
}

Once enabled, you'll immediately notice faster cold starts and near-instant hot module replacement. The difference is especially noticeable on larger projects with hundreds or thousands of modules.

How Turbopack Accelerates Development

Turbopack achieves its impressive speed through incremental computation and lazy compilation. Unlike traditional bundlers that process your entire application upfront, Turbopack only compiles the code you're actually using. This means if you're working on a single page, it won't waste time processing unrelated components.
The Rust foundation provides memory safety and parallelization capabilities that JavaScript-based bundlers can't match. Turbopack can leverage all your CPU cores effectively, processing multiple files simultaneously. This architectural advantage translates directly into faster build times and a more responsive development experience.

Hack 2: Master the Next.js Image Component

Images are often the heaviest assets on a page, and they're usually the main culprit behind poor LCP scores. The Next.js Image component is your secret weapon for automatically optimizing images without sacrificing quality.
This component does so much heavy lifting behind the scenes. It automatically serves the right image format based on browser support, resizes images on-demand, and implements lazy loading by default. You get all these optimizations with minimal configuration.

Automatic Optimization and Modern Formats

The Image component automatically serves modern formats like WebP and AVIF when browsers support them. These formats can reduce file sizes by 30-50% compared to JPEG, with no visible quality loss. Here's how to configure it properly:
import Image from 'next/image'

<Image
src="/hero-image.jpg"
alt="Hero image"
width={1200}
height={600}
quality={85}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

The sizes prop is crucial for responsive images. It tells the browser which image size to download based on the viewport width, preventing mobile devices from downloading desktop-sized images.

Using the 'priority' Prop for LCP Elements

For images that appear above the fold, especially your LCP element, use the priority prop. This tells Next.js to preload the image, ensuring it loads as quickly as possible:
<Image
src="/hero-banner.jpg"
alt="Welcome banner"
width={1920}
height={1080}
priority
/>

This simple prop can shave seconds off your LCP time. Next.js will add a preload link tag in the document head, giving this image priority over other resources. Use it sparingly though—only for truly critical images.

Preventing CLS with Static Dimensions

Always specify width and height for your images. This reserves the correct amount of space before the image loads, preventing layout shifts:
// Good - prevents CLS
<Image
src="/product.jpg"
alt="Product"
width={400}
height={300}
/>

// Bad - causes layout shift
<img src="/product.jpg" alt="Product" />

For responsive images where you don't know the exact dimensions, use the fill prop with a container that has relative positioning. This maintains aspect ratio while preventing shifts.

Hack 3: Optimize Font Loading

Web fonts can significantly impact loading performance and cause layout shifts. Custom fonts are beautiful, but they come with a performance cost if not handled correctly. The next/font module solves these problems elegantly.
Traditional font loading involves multiple network requests, DNS lookups, and potential flash of unstyled text (FOUT) or invisible text (FOIT). Next.js eliminates these issues by optimizing fonts at build time.

Self-Hosting Fonts with next/font

Self-hosting fonts with next/font is straightforward. Here's how to implement Google Fonts:
import { Inter } from 'next/font/google'

const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter'
})

export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.variable}>
<body>{children}</body>
</html>
)
}

For local fonts, the process is similar:
import localFont from 'next/font/local'

const myFont = localFont({
src: './my-font.woff2',
display: 'swap',
variable: '--font-custom'
})

This approach downloads fonts during build time and serves them from your domain, eliminating external requests and improving privacy.

Leveraging font-display: swap

The font-display: swap CSS property is automatically applied by next/font, ensuring text remains visible during font load. This prevents the dreaded invisible text problem that hurts user experience.
With swap, the browser immediately displays text using a fallback font, then swaps to your custom font once it loads. Users can start reading content immediately, even if the final font hasn't loaded yet. This significantly improves perceived performance and prevents layout shifts from font loading.

Hack 4: Implement Intelligent Code Splitting

Next.js offers automatic code splitting, but strategic use of dynamic imports can take your performance to the next level. Not all code needs to load immediately—some components are only needed after user interaction or on specific conditions.
Code splitting reduces your initial bundle size, leading to faster page loads and improved INP scores. The key is identifying which parts of your application can be loaded later without impacting user experience.

Dynamic Imports for Components

Here's how to lazy-load components that aren't immediately visible:
import dynamic from 'next/dynamic'

const DynamicChart = dynamic(() => import('../components/Chart'), {
loading: () => <p>Loading chart...</p>,
})

const DynamicModal = dynamic(() => import('../components/Modal'), {
ssr: false
})

export default function Dashboard() {
const [showChart, setShowChart] = useState(false)

return (
<div>
<button onClick={() => setShowChart(true)}>
Show Analytics
</button>
{showChart && <DynamicChart />}
</div>
)
}

This approach ensures the Chart component only loads when needed, reducing the initial JavaScript bundle by potentially hundreds of kilobytes.

Disabling SSR for Client-Side Only Components

Some components rely heavily on browser APIs and don't benefit from server-side rendering. Disable SSR for these components to reduce server processing time:
const ClientOnlyComponent = dynamic(
() => import('../components/ClientOnly'),
{ ssr: false }
)

This is particularly useful for components using window, document, or other browser-specific APIs. It prevents hydration mismatches and reduces the server-side bundle size.

Hack 5: Refined Caching Strategies

Next.js 15 introduces a more explicit and refined caching architecture. The days of "magic" caching are over—now you have granular control over exactly what gets cached and when. This change might seem daunting at first, but it actually gives you more power to optimize performance.
The new caching system is more predictable and easier to debug. You explicitly declare your caching intentions, making it clearer for other developers (including future you) to understand how data flows through your application.

Understanding the New Caching Semantics

In Next.js 15, fetch requests and GET Route Handlers are no longer cached by default. This breaking change prevents unexpected stale data issues but requires you to be more intentional about caching:
// Next.js 14 - automatically cached
const data = await fetch('https://api.example.com/data')

// Next.js 15 - not cached by default
const data = await fetch('https://api.example.com/data')

// Next.js 15 - explicitly cached
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // Cache for 1 hour
})

This explicit approach helps prevent those frustrating "why isn't my data updating?" moments that plagued earlier versions.

On-Demand and Time-Based Revalidation

Next.js 15 provides powerful tools for cache management. Use revalidateTag for on-demand updates:
// In your Server Action or Route Handler
import { revalidateTag } from 'next/cache'

export async function updateProduct(productId) {
// Update your database
await db.products.update(productId, newData)

// Invalidate the cache
revalidateTag(`product-${productId}`)
}

// In your component
const product = await fetch(`/api/products/${id}`, {
next: { tags: [`product-${id}`] }
})

For time-based revalidation, specify the cache duration:
const data = await fetch('https://api.example.com/trending', {
next: { revalidate: 300 } // Revalidate every 5 minutes
})

This gives you the perfect balance between fresh data and performance.

Hack 6: Leverage Script Optimization with next/script

Third-party scripts can be a major performance bottleneck. Analytics, chat widgets, and advertising scripts often load synchronously, blocking your main thread and hurting performance metrics. The next/script component gives you fine-grained control over when and how these scripts load.
Many developers add scripts without considering their performance impact. That innocent Google Analytics script might be adding 500ms to your page load time. With proper script optimization, you can have your cake and eat it too—keeping necessary third-party functionality while maintaining blazing-fast performance.

Choosing the Right Loading Strategy

Next.js provides three main loading strategies, each suited for different use cases:
import Script from 'next/script'

// For critical scripts needed before page becomes interactive
<Script
src="https://critical-script.com/script.js"
strategy="beforeInteractive"
/>

// For scripts needed after page becomes interactive (default)
<Script
src="https://analytics.example.com/script.js"
strategy="afterInteractive"
/>

// For scripts that can wait until idle time
<Script
src="https://chat-widget.com/script.js"
strategy="lazyOnload"
/>

Use beforeInteractive sparingly—only for truly critical scripts. Most analytics and tracking scripts work perfectly with afterInteractive. Chat widgets and other non-essential features should use lazyOnload.

Moving Scripts to the Worker Thread

The experimental worker strategy is a game-changer for script performance. It runs scripts in a web worker, completely freeing up the main thread:
<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
strategy="worker"
/>

This approach is perfect for analytics scripts that don't need direct DOM access. Your Google Analytics or Facebook Pixel can collect data without impacting user interactions. Just remember this is still experimental, so test thoroughly before using in production.

Hack 7: Monitor and Analyze with Vercel Analytics

You can't improve what you don't measure. Continuous monitoring is crucial for maintaining performance over time. As your application grows and changes, new performance issues can creep in. Vercel Analytics provides real-world data from actual users, not just lab tests.
Lab data from Lighthouse is useful, but it doesn't tell the whole story. Real users have different devices, network conditions, and usage patterns. Vercel Analytics captures this diversity, showing you how your site performs in the wild.

Setting Up Vercel Analytics

Getting started with Vercel Analytics is refreshingly simple. First, install the analytics package:
npm install @vercel/analytics

Then add it to your root layout:
import { Analytics } from '@vercel/analytics/react'

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Analytics />
</body>
</html>
)
}

That's it! Analytics will automatically start collecting Core Web Vitals data from your real users. No configuration needed—it just works.

Interpreting Your Performance Score

The Vercel Analytics dashboard presents data in an actionable format. You'll see your Core Web Vitals scores broken down by page, device type, and geography. This granular view helps identify specific problem areas.
Look for patterns in your data. Maybe your mobile LCP is great but desktop is lagging. Or perhaps users in certain regions experience poor INP scores. These insights guide your optimization efforts. Focus on pages with the most traffic and worst scores first—that's where you'll see the biggest impact.
The dashboard also shows performance trends over time. Did that new feature deployment hurt your CLS score? You'll see it immediately. This rapid feedback loop helps you catch and fix performance regressions before they impact too many users.

Conclusion

Achieving a perfect Lighthouse score isn't just about bragging rights—it's about providing the best possible experience for your users. These seven hacks give you the tools to build Next.js applications that are not just fast, but blazingly fast.
Start with the low-hanging fruit. Implement Turbopack for faster development, optimize your images with the Next.js Image component, and set up proper font loading. These changes alone can dramatically improve your scores. Then move on to more advanced optimizations like intelligent code splitting and refined caching strategies.
Remember, performance optimization is an ongoing process. Use Vercel Analytics to monitor your progress and identify new opportunities for improvement. What works today might need adjustment tomorrow as your application grows and evolves.
The web is getting faster, and user expectations are rising. With Next.js 15 and these optimization techniques, you're well-equipped to meet and exceed those expectations. Your users will thank you with longer session times, lower bounce rates, and better engagement. And Google will reward you with improved search rankings.
Now it's time to put these hacks into practice. Pick one area where your site needs improvement and start there. Small, incremental changes add up to significant performance gains. Your journey to a perfect Lighthouse score starts with a single optimization.

References

Like this project

Posted Jun 19, 2025

Unlock top performance in Next.js 15. Discover 7 essential speed hacks to optimize your app, satisfy Core Web Vitals, and achieve a green Lighthouse score overnight.

Server Actions Goldmine: The API-Free Pattern for Modern Next.js Apps
Server Actions Goldmine: The API-Free Pattern for Modern Next.js Apps
Beyond Fulfillment: What Shopify-to-Flexport Means for Freelance Logistics Integrations
Beyond Fulfillment: What Shopify-to-Flexport Means for Freelance Logistics Integrations
Subscription Stampede: Shopify’s Native App Is Spawning Monthly-Retainer Dev Gigs
Subscription Stampede: Shopify’s Native App Is Spawning Monthly-Retainer Dev Gigs
Planet-Positive Stores: Carbon-Neutral Badges That Boost Conversions—and Freelance Fees
Planet-Positive Stores: Carbon-Neutral Badges That Boost Conversions—and Freelance Fees

Join 50k+ companies and 1M+ independents

Contra Logo

© 2025 Contra.Work Inc