perf hacks

Code Splitting

Reduce initial bundle size by splitting code into smaller chunks

Code splitting divides your JavaScript bundle into smaller chunks that can be loaded on demand. This reduces initial load time and improves performance metrics like LCP and TTI.

CODE SPLITTINGbundle.js500KBAll code upfrontsplitmain.js120KBvendors.js80KBchart.js200KBmodal.js100KBInitial loadLazy loadedInitial bundle200KB60% smallerLazy chunks load on demand → faster initial page load

Automatic Code Splitting in Next.js

Next.js automatically code splits at the page level:

page.tsx

Each route segment generates its own JavaScript chunk.

index.tsx
about.tsx

Each page file generates its own JavaScript chunk.

Each route only loads the JavaScript it needs.

Dynamic Imports

Dynamic imports with next/dynamic are the primary way to manually split code in Next.js. Use them for components that aren't needed immediately.

Basic Dynamic Import

import dynamic from 'next/dynamic';

// Component loads only when rendered
const HeavyChart = dynamic(() => import('@/components/Chart'));

export default function Dashboard() {
  return (
    <div>
      <Header />
      <HeavyChart /> {/* Loads separately */}
    </div>
  );
}

With Loading State

const HeavyChart = dynamic(() => import('@/components/Chart'), {
  loading: () => (
    <div className="h-64 animate-pulse bg-gray-100 rounded-lg" />
  ),
});

Disable SSR for Client-Only Components

Components that use browser-only APIs like window, document, or Web APIs must have ssr: false to prevent server-side rendering errors.

// Components that use browser APIs
const MapComponent = dynamic(() => import('@/components/Map'), {
  ssr: false,
  loading: () => <MapSkeleton />,
});

// Window-dependent components
const EditorComponent = dynamic(() => import('@/components/Editor'), {
  ssr: false,
});

Strategic Code Splitting

Split Heavy Dependencies

// ❌ Bad: Loads moment.js on every page
import moment from 'moment';

// ✅ Good: Load only when needed
const DatePicker = dynamic(() => import('./DatePicker'), {
  loading: () => <input type="text" placeholder="Select date..." />,
});

Split Below the Fold

export default function ProductPage() {
  return (
    <div>
      {/* Critical - loads immediately */}
      <ProductHero />
      <ProductInfo />
      <AddToCart />

      {/* Below fold - can load later */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews />
      </Suspense>

      <Suspense fallback={<RelatedSkeleton />}>
        <RelatedProducts />
      </Suspense>
    </div>
  );
}

Split by User Interaction

'use client';

import { useState } from 'react';
import dynamic from 'next/dynamic';

// Only load modal when user clicks
const SettingsModal = dynamic(() => import('./SettingsModal'));

export function Settings() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Settings</button>
      {isOpen && <SettingsModal onClose={() => setIsOpen(false)} />}
    </>
  );
}

Split by Route Segment

app/dashboard/layout.tsx
import dynamic from 'next/dynamic';

// Dashboard-specific components load only in dashboard
const DashboardSidebar = dynamic(() => import('./Sidebar'));
const DashboardHeader = dynamic(() => import('./Header'));

export default function DashboardLayout({ children }) {
  return (
    <div className="flex">
      <DashboardSidebar />
      <div className="flex-1">
        <DashboardHeader />
        {children}
      </div>
    </div>
  );
}

Named Exports

// For components with named exports
const Chart = dynamic(() =>
  import('@/components/Charts').then((mod) => mod.LineChart)
);

// Multiple named exports
const BarChart = dynamic(() =>
  import('@/components/Charts').then((mod) => mod.BarChart)
);

Preloading

import dynamic from 'next/dynamic';

const HeavyComponent = dynamic(() => import('./HeavyComponent'));

// Preload on hover
function Button() {
  return (
    <button
      onMouseEnter={() => {
        // Start loading before click
        import('./HeavyComponent');
      }}
      onClick={() => setShowHeavy(true)}
    >
      Open
    </button>
  );
}

Route-Based Code Splitting

// Prefetch likely next routes
import { useRouter } from 'next/navigation';

function ProductCard({ product }) {
  const router = useRouter();

  return (
    <div
      onMouseEnter={() => router.prefetch(`/products/${product.id}`)}
    >
      <Link href={`/products/${product.id}`}>
        {product.name}
      </Link>
    </div>
  );
}

Analyzing Chunks

# Add to package.json
"analyze": "ANALYZE=true next build"

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

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

Common Patterns

Conditional Loading

'use client';

import dynamic from 'next/dynamic';

// Load based on feature flags
const BetaFeature = dynamic(() => import('./BetaFeature'));

function App({ user }) {
  const showBeta = user.flags.includes('beta');

  return (
    <div>
      <MainContent />
      {showBeta && <BetaFeature />}
    </div>
  );
}

Role-Based Loading

// Admin components only load for admins
const AdminPanel = dynamic(() => import('./AdminPanel'), {
  ssr: false,
});

function Dashboard({ user }) {
  return (
    <div>
      <UserDashboard />
      {user.role === 'admin' && <AdminPanel />}
    </div>
  );
}

Performance Impact

StrategyBundle ReductionBest For
Route splittingAutomaticAll apps
Dynamic imports20-60% per componentHeavy components
Interaction-based30-50% initialModals, dropdowns
Below-fold20-40% initialLong pages

Checklist

What to Split

Heavy chart/visualization libraries Chart.js, D3, Recharts - load only when needed.

Rich text editors TipTap, Slate, Draft.js are heavy - defer them.

Map components Mapbox, Google Maps - use dynamic import with ssr: false.

Modal dialogs Users don't see them on initial load.

Admin-only features Regular users shouldn't download admin code.

Below-fold content Defer anything not visible on initial viewport.

What NOT to Split

Critical above-fold content Users need this immediately for good LCP.

Small utility components The overhead of splitting isn't worth it.

Frequently used components Better to keep in main bundle if used everywhere.

Components needed for LCP Dynamic import adds delay to your largest paint.

On this page