StarterApp Docs
Performance

Performance Overview

Built-in optimizations for fast-loading applications

The framework delivers fast page loads through Next.js 15's automatic optimizations. Code splitting, edge caching, and asset optimization work by default without manual configuration.

Pro Tip

Performance optimizations are built-in: automatic code splitting, aggressive tree-shaking via optimizePackageImports, and CDN-friendly caching for public content.

Core Optimizations

Automatic Code Splitting

Next.js 15 splits code at route boundaries automatically. Users visiting /dashboard don't download /billing JavaScript. Each route loads independently.

Tree-Shaking Configuration

The framework enables aggressive tree-shaking for large dependencies:

import { INTERNAL_PACKAGES } from "@workspace/ui/lib/internal-packages";

export default {
  experimental: {
    optimizePackageImports: ["lucide-react", ...INTERNAL_PACKAGES],
  },
};

This transforms icon imports from bundling entire libraries to importing only used icons:

// Before optimization: ~500KB bundle
import { CheckIcon, XIcon, AlertIcon } from "lucide-react";

// After optimization: ~5KB bundle
// Next.js imports only these 3 icons

Pro Tip

Add heavy dependencies to optimizePackageImports to enable automatic tree-shaking. Works for lucide-react, @radix-ui/*, and most component libraries.

Streaming and Suspense

Server components stream HTML progressively. Users see page structure immediately while data loads:

import { Suspense } from "react";

export default function DashboardPage() {
  return (
    <>
      <DashboardHeader /> {/* Renders immediately */}

      <Suspense fallback={<TicketsSkeleton />}>
        <SupportTickets /> {/* Streams when data is ready */}
      </Suspense>

      <Suspense fallback={<ActivitySkeleton />}>
        <RecentActivity /> {/* Streams independently */}
      </Suspense>
    </>
  );
}

Each Suspense boundary streams independently. Fast queries render before slow ones. No waterfall blocking.

Heads up

Suspense requires async server components. Client components with 'use client' cannot use Suspense for data fetching - use loading states instead.

Static vs Dynamic Rendering

Marketing App (Static)

Public pages use static generation with periodic revalidation:

export const revalidate = 600; // Regenerate every 10 minutes

export default function PricingPage() {
  return <PricingTable />;
}

Benefits:

  • Instant response from edge CDN
  • No server requests for cached pages
  • Automatic global distribution

Dashboard App (Dynamic)

User-specific pages require fresh data:

export const revalidate = 0;
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";

export default async function DashboardPage() {
  const session = await getCurrentSession();
  // Return user-specific content
}

Why dynamic:

  • User data must never be cached between users
  • Billing state changes require fresh queries
  • Security: stale sessions are rejected immediately

Measuring Performance

pnpm lighthouse

Runs Lighthouse CI on key pages with mobile throttling. Generates reports in apps/marketing/reports/lighthouse/.

ANALYZE=true pnpm build

Opens interactive treemap showing JavaScript distribution. Identify heavy dependencies and optimization opportunities.

pnpm dev --turbopack

Turbopack provides near-instant HMR during development. Fast iteration improves developer experience.

Core Web Vitals

Target metrics for production:

MetricDescriptionTarget
LCPLargest Contentful Paint - Loading performanceUnder 2.5s
INPInteraction to Next Paint - InteractivityUnder 200ms
CLSCumulative Layout Shift - Visual stabilityUnder 0.1

Pro Tip

Use next/image and next/font components to automatically optimize LCP and CLS. These components handle format selection, lazy loading, and layout reservation.

Performance Patterns

Lazy Load Heavy Components

import dynamic from "next/dynamic";

// Load chart library only when needed
const Chart = dynamic(() => import("~/components/chart"), {
  loading: () => <ChartSkeleton />,
  ssr: false, // Skip server-side rendering for client-only libs
});

export default function AnalyticsPage() {
  return (
    <div>
      <h1>Analytics</h1>
      <Chart data={data} />
    </div>
  );
}

Optimize Third-Party Scripts

import Script from "next/script";

export default function Layout({ children }) {
  return (
    <html>
      <body>
        {children}

        {/* Load analytics after page interactive */}
        <Script
          src="https://analytics.example.com/script.js"
          strategy="lazyOnload"
        />
      </body>
    </html>
  );
}

Script strategies:

  • beforeInteractive - Critical scripts (rare)
  • afterInteractive - Analytics, tracking (default)
  • lazyOnload - Non-critical widgets

Common Performance Issues

Next Steps