SoftwareCrafting Logo

Next.js App Router Performance Checklist: Improve Core Web Vitals Without Guesswork

DDeepak RajputWeb Development13 min read16 Aug 2026
Abstract fast web application with performance metrics and connected server and edge nodes

TL;DR: Measure real users first, then reduce the work required to render the first useful view. Keep data-heavy work on the server, make client boundaries small, optimize image dimensions and fonts, remove unnecessary JavaScript, cache intentionally, and validate the production build on representative devices.

Performance Is a User Flow, Not a Lighthouse Number

Next.js can produce a fast page, but the framework cannot decide which data is essential, whether a client component is necessary, or if a third-party script is worth its cost. Performance work starts by looking at the user journey: landing page, search, product detail, checkout, dashboard, or another action that creates business value.

Core Web Vitals provide useful signals, but they are symptoms of the work your application performs. Largest Contentful Paint reflects how quickly the main content becomes visible. Interaction to Next Paint reflects the responsiveness of interactions. Cumulative Layout Shift reflects visual stability. A high score does not guarantee a good product flow, and a low score should lead you to the specific page and device conditions causing it.

For a deeper baseline, SoftwareCrafting’s performance and analytics services can help connect field data to the user journeys that matter instead of optimizing isolated demo pages.

Start With Field and Lab Measurements

Use two types of data:

  • Field data represents real users, networks, devices, geographies, and returning versus first-time visits.
  • Lab data gives repeatable traces for local debugging, pull requests, and release comparisons.

Before changing code, record the URL, template, device class, connection profile, deployment version, and metric distribution. A median score can hide a poor experience for slower phones. Track p75 values for the key metrics and split dashboards by route and release.

Useful questions include:

  1. Which element is the LCP candidate?
  2. Is the delay caused by server response, resource discovery, image decoding, or rendering?
  3. Which interaction has the slowest input-to-update path?
  4. Where does layout move after the page becomes visible?
  5. Did a recent dependency or third-party script change the distribution?

Keep the Server and Client Boundaries Deliberate

The App Router makes Server Components the default. That is valuable because server-rendered components can fetch data and produce HTML without sending their implementation to the browser. The performance benefit disappears when a high-level layout is marked with 'use client' and pulls a large subtree into the client bundle.

Use client components for browser-only APIs, local interactive state, event handlers, and genuinely interactive UI. Keep the boundary close to the interactive leaf.

// Server Component
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const product = await getProduct(id);

  return (
    <main>
      <ProductSummary product={product} />
      <AddToCartButton productId={product.id} />
    </main>
  );
}

ProductSummary can remain a Server Component, while AddToCartButton is a small client boundary. Avoid turning the entire page into a client component only because one button needs an event handler.

Also keep serialized props small. Passing a complete product catalog, internal metadata, or large configuration object into a client component increases the response and hydration work. Select only the fields the browser needs.

Make Data Fetching Parallel and Intentional

Sequential awaits create waterfalls when requests are independent. Start independent work together:

export default async function DashboardPage() {
  const overviewPromise = getOverview();
  const activityPromise = getRecentActivity();
  const alertsPromise = getAlerts();

  const [overview, activity, alerts] = await Promise.all([
    overviewPromise,
    activityPromise,
    alertsPromise,
  ]);

  return <Dashboard overview={overview} activity={activity} alerts={alerts} />;
}

Do not parallelize calls that depend on the output of another call. Instead, model the dependency clearly and cache shared reads. If a page contains independent sections with different latency, use streaming and Suspense so the useful shell can render while slower sections complete.

Avoid fetching the same data in a Server Component, a client component, and a route handler unless each copy serves a clear purpose. Duplicate requests can hide behind fast local development and become expensive in production.

Optimize the Largest Contentful Paint Candidate

The LCP element is often a hero image, headline block, or product image. Find it in a trace instead of assuming it is the image. Then improve the bottleneck that actually delays it.

For an important image:

  • Use next/image with accurate width, height, or responsive sizes.
  • Provide priority only for the image that is truly above the fold and important to the first view.
  • Use the correct source dimensions so the browser does not download a desktop asset for a small phone.
  • Avoid placing a large image behind a client-only component that waits for hydration.
  • Make sure the server response and image origin are geographically sensible.

For a text LCP candidate, inspect font loading and CSS. Preload only fonts that are needed immediately, use a modern format, and avoid loading several weights before the first view. A fallback that renders quickly is often better than blocking the page for a rarely used typeface.

Remove JavaScript That Does Not Create User Value

Every client dependency has download, parse, compile, and runtime costs. Review:

  • Date and chart libraries loaded on pages that do not show them.
  • Global providers that wrap the entire application for one route.
  • UI libraries that ship large modules for a small interaction.
  • Analytics scripts loaded before the page is usable.
  • Repeated icon packages or broad barrel imports.
  • Hydration of hidden menus, dialogs, and dashboards before the user opens them.

Use dynamic imports for heavy, interaction-triggered features. Defer non-essential third-party scripts until after the page is usable and consent requirements are satisfied. Code splitting helps only when the initial route does not import the split module through another path.

Prevent Layout Shift by Reserving Space

Layout shift usually comes from content whose size is unknown when the initial layout is painted. Reserve image dimensions, set stable aspect ratios for media, avoid injecting banners above existing content, and give skeletons dimensions that match the final component.

Ads, consent notices, personalization, and font swaps need special attention. If a banner must appear, render a reserved region or place it where existing content does not move. For conditional UI, decide whether the space should exist in both states.

Animations should use transform and opacity when possible. Animating layout properties such as width, height, top, or left can trigger expensive recalculation and produce jank on lower-end devices.

Cache Data With a Clear Freshness Model

Caching is a correctness decision. For each read, define whether the data is:

Data typeTypical approach
Public contentTime-based revalidation or tag-based invalidation
Product catalogCached read with invalidation after a catalog change
User-specific dashboardRequest-scoped or short-lived cache with authorization checks
Financial or permission stateFresh read or carefully bounded cache
Search resultsCache only when query and authorization dimensions are safe

Do not cache personalized data in a shared layer without including the right identity and authorization boundaries. Do not use a long revalidation period simply to hide slow database queries. Fix query shape, indexing, and upstream latency first, then choose the cache lifetime that matches user expectations.

Audit CSS, Fonts, and Third-Party Code

CSS can block rendering and large global stylesheets can increase work on every route. Remove unused rules, keep route-specific styles local where practical, and avoid importing a large design system into a small landing page.

Third-party scripts deserve a budget. For each script, record its purpose, load timing, transfer size, main-thread work, privacy implications, and fallback behavior. If a marketing script causes the primary interaction to miss its responsiveness budget, move it later or remove it.

Test the Production Build

Development mode is not a performance baseline. Test a production build with realistic content, image sizes, caching, and deployment infrastructure. Include:

  • A slower Android device or an equivalent CPU throttle.
  • Fast and slow network profiles.
  • First visit and repeat visit behavior.
  • Authenticated and unauthenticated routes.
  • Long lists, empty states, errors, and personalized content.
  • JavaScript disabled or blocked third-party scenarios where relevant.

Add budgets to CI for bundle size and key route traces, but do not let a synthetic budget replace field monitoring. A page can pass a lab check while a particular region receives slow server responses or a third-party dependency fails.

Common Failure Modes

The same patterns appear repeatedly:

  • Marking the root layout as client-side to manage one toggle.
  • Calling an API from the browser for data that could be rendered on the server.
  • Setting every image to priority and removing the value of prioritization.
  • Memoizing components while leaving the actual data waterfall untouched.
  • Adding a cache without defining invalidation or user isolation.
  • Measuring only the homepage while the highest-value route is slow.
  • Treating a green Lighthouse score as proof that real users are fast.

Production Checklist

Before shipping a performance-sensitive route, confirm:

  • Field metrics are available by route, device, geography, and release.
  • The LCP candidate and its bottleneck are known.
  • Client boundaries are limited to interactive features.
  • Independent data requests run in parallel or stream independently.
  • Images, fonts, and layout dimensions are intentional.
  • Third-party code has an owner and loading budget.
  • Caching respects freshness and authorization.
  • Production builds are tested on representative devices.
  • Regressions trigger an alert or release review.

Performance is an ongoing operating practice. Start with the user journey, find the largest source of work, make one change at a time, and verify the result in both traces and field data. For a broader architecture review, explore full-stack web development services with performance, caching, and deployment considered together.

About the author

Deepak Rajput

This article was published by SoftwareCrafting engineers for founders, product teams, and developers working on real production delivery. We focus on practical tradeoffs, maintainable architecture, and implementation details that hold up outside demos.

View author profile

Last updated: 2026-08-16