Mastering TBT Reduction in Next.js: Innovative Strategies for Smooth Hydration

You've spent days optimizing your Next.js application, carefully refining components and implementing best practices. But when you run a Lighthouse audit, you're dismayed by a glaring red score for Total Blocking Time (TBT). Despite your efforts, your application still feels sluggish, with frustrating delays between user interactions.

This performance bottleneck isn't just affecting your metrics—it's impacting real users who expect lightning-fast experiences from modern web applications.

Understanding Total Blocking Time (TBT)

Total Blocking Time measures the amount of time between First Contentful Paint (FCP) and Time to Interactive (TTI) during which the main thread is blocked for long enough to prevent input responsiveness. In simpler terms, it quantifies how long users must wait before they can interact with your page after it begins displaying content.

TBT is a critical metric that makes up 30% of your Lighthouse performance score. It's especially important for Next.js applications due to how React's hydration process works.

How TBT is Calculated

The browser considers any task that blocks the main thread for more than 50 milliseconds to be a "long task." TBT is the sum of the "blocking portions" of all long tasks between FCP and TTI. The blocking portion is the duration exceeding the 50ms threshold.

For example:

  • Task 1: 250ms (Blocking Time = 200ms)

  • Task 2: 90ms (Blocking Time = 40ms)

  • Task 3: 35ms (Blocking Time = 0ms, as it's under 50ms)

  • Task 4: 30ms (Blocking Time = 0ms, as it's under 50ms)

  • Task 5: 155ms (Blocking Time = 105ms)

Total Blocking Time = 200ms + 40ms + 0ms + 0ms + 105ms = 345ms

For optimal user experience, aim for a TBT of less than 200ms on mobile devices.

The Hydration Problem in Next.js

The primary source of high TBT in Next.js applications is React's default hydration process. By default, React hydrates the entire page at once, including components that are not immediately visible to the user. This results in unnecessary JavaScript execution and slower interactivity.

As one developer noted in a Reddit discussion: "By default, React hydrates the entire page at once, including components that are not immediately visible, which results in unnecessary JavaScript execution and slower interactivity."

This inefficient hydration process can lead to TBT scores north of 4,000-5,000ms on complex pages, severely impacting perceived performance and user satisfaction.

Innovative Strategies to Reduce TBT in Next.js

Let's explore practical strategies to tackle high TBT and optimize your Next.js application's performance:

1. Implement Lazy Hydration with next-lazy-hydration-on-scroll

One of the most effective ways to reduce TBT is by deferring the hydration of components until they're actually needed. The next-lazy-hydration-on-scroll library provides a simple solution by hydrating components only when they enter the viewport.

Here's how to implement it:

import { LazyHydrate } from 'next-lazy-hydration-on-scroll';

function MyComponent() {
  return (
    <div>
      {/* Components above the fold hydrate normally */}
      <Header />
      <HeroSection />
      
      {/* Components below the fold only hydrate when scrolled into view */}
      <LazyHydrate>
        <HeavyComponent />
      </LazyHydrate>
      
      <LazyHydrate>
        <Footer />
      </LazyHydrate>
    </div>
  );
}

This approach can dramatically reduce initial TBT by postponing the JavaScript execution of below-the-fold components until they're actually visible to the user.

2. Utilize next/dynamic for Component-Level Code Splitting

Next.js provides the next/dynamic function, which enables component-level code splitting and lazy loading. This is particularly useful for heavy components that aren't immediately needed:

import dynamic from 'next/dynamic';

// The component will only be loaded when it's rendered
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
  loading: () => <p>Loading...</p>,
  ssr: false // Disable server-side rendering for client-only components
});

function HomePage() {
  return (
    <div>
      <Header />
      <MainContent />
      <HeavyComponent /> {/* This will be lazy-loaded */}
    </div>
  );
}

By leveraging next/dynamic, you ensure that JavaScript for non-critical components isn't loaded during the initial page load, reducing TBT significantly.

3. Optimize Images with next/image

Large, unoptimized images can contribute to poor performance. Next.js provides the next/image component to automatically optimize images, reducing their impact on loading performance:

import Image from 'next/image';

function ProductCard({ product }) {
  return (
    <div className="product-card">
      <Image
        src={product.imageUrl}
        alt={product.name}
        width={300}
        height={200}
        priority={product.featured} // Load priority images immediately
        loading="lazy" // Lazy load non-priority images
      />
      <h3>{product.name}</h3>
      <p>{product.description}</p>
    </div>
  );
}

The priority attribute should be used for above-the-fold images that are immediately visible to users, while loading="lazy" is appropriate for images that appear below the fold.

4. Optimize Third-Party Scripts with next/script

Third-party scripts often contribute significantly to TBT. Next.js provides the next/script component to control how and when these scripts load:

import Script from 'next/script';

function MyApp({ Component, pageProps }) {
  return (
    <>
      {/* Critical script that blocks rendering */}
      <Script src="https://critical-script.com/script.js" strategy="beforeInteractive" />
      
      {/* Load after page becomes interactive */}
      <Script src="https://analytics.com/script.js" strategy="afterInteractive" />
      
      {/* Load during browser idle time */}
      <Script src="https://non-essential.com/script.js" strategy="lazyOnload" />
      
      <Component {...pageProps} />
    </>
  );
}

Using the appropriate loading strategy for each script can dramatically reduce their impact on TBT.

5. Leverage Server Components in the App Router

Next.js 13+ introduced server components in the App Router, which can significantly reduce the amount of JavaScript sent to the client:

// app/products/page.js
// This is a Server Component by default in the App directory
export default async function ProductsPage() {
  // This code runs on the server and doesn't affect client-side TBT
  const products = await fetchProducts();
  
  return (
    <div>
      <h1>Our Products</h1>
      <ProductList products={products} />
    </div>
  );
}

// ProductList.js
// Explicitly mark as a client component when needed
'use client';

export default function ProductList({ products }) {
  // This component will be hydrated on the client
  return (
    <ul>
      {products.map(product => (
        <li key={product.id} onClick={() => handleClick(product)}>
          {product.name}
        </li>
      ))}
    </ul>
  );
}

By keeping as much logic as possible in server components, you can minimize the JavaScript that needs to be executed on the client, directly reducing TBT.

6. Implement Streaming with Suspense

Next.js supports streaming with Suspense, allowing you to progressively render UI as data becomes available:

// app/dashboard/page.js
import { Suspense } from 'react';
import LoadingSpinner from '../components/LoadingSpinner';

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      
      {/* Critical UI renders immediately */}
      <UserProfile />
      
      {/* Less critical sections can be streamed in */}
      <Suspense fallback={<LoadingSpinner />}>
        <RevenueChart />
      </Suspense>
      
      <Suspense fallback={<LoadingSpinner />}>
        <RecentOrders />
      </Suspense>
    </div>
  );
}

Streaming allows the browser to hydrate parts of the UI incrementally, distributing the main thread work and reducing TBT.

7. Optimize Images Further with Async Decoding

Beyond using next/image, you can further optimize image loading with the decoding="async" attribute:

<img src="example.jpg" decoding="async" loading="lazy" alt="Example" />

As noted by a developer in a Reddit thread: "put decoding='async' on EVERY image tag on your site" to improve performance.

8. Analyze and Reduce Bundle Size

Use the @next/bundle-analyzer to identify large dependencies that could be contributing to TBT:

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // your Next.js config
});

Run with ANALYZE=true npm run build to visualize your bundle size and identify problematic dependencies that need optimization.

Measuring the Impact

After implementing these strategies, it's crucial to measure their impact on TBT. Use Lighthouse in Chrome DevTools to compare before and after metrics. A successful optimization should reduce TBT below 200ms for optimal performance.

Conclusion

Total Blocking Time is a critical performance metric that directly affects how users perceive the responsiveness of your Next.js application. By implementing lazy hydration, leveraging server components, optimizing images and scripts, and carefully managing your JavaScript bundle, you can significantly reduce TBT and create a smoother, more responsive user experience.

Remember that performance optimization is an ongoing process. Regularly audit your application, monitor TBT metrics, and adjust your strategies as your application evolves.

By mastering these TBT reduction techniques, you'll not only improve your Lighthouse scores but, more importantly, deliver the lightning-fast experience that modern web users expect.

Raymond Yeh

Raymond Yeh

Published on 07 April 2025

Get engineers' time back from marketing!

Don't let managing a blog on your site get in the way of your core product.

Wisp empowers your marketing team to create and manage content on your website without consuming more engineering hours.

Get started in few lines of codes.

Choosing a CMS
Related Posts
Mastering Mobile Performance: A Complete Guide to Improving Next.js Lighthouse Scores

Mastering Mobile Performance: A Complete Guide to Improving Next.js Lighthouse Scores

Comprehensive guide to improving Next.js mobile Lighthouse scores. Learn to optimize Core Web Vitals, reduce static chunks, and implement effective third-party script management.

Read Full Story
Optimizing Your tRPC Implementation in Next.js

Optimizing Your tRPC Implementation in Next.js

Discover how to balance server and client responsibilities in your tRPC setup. Stop wrestling with performance issues and start leveraging Next.js features for blazing-fast applications.

Read Full Story
How Vercel's Prefetching Works: A Deep Dive into Benefits and Gotchas

How Vercel's Prefetching Works: A Deep Dive into Benefits and Gotchas

Puzzled by mysterious GET requests in your Vercel logs? Discover how prefetching works, why it's causing those requests, and learn to balance performance gains with cost implications.

Read Full Story
Loading...