Skip to content

nextjs-optimization

<!-- Loaded by performance-optimization-engineer when task involves Next.js, App Router, SSR, ISR, next/image, next/font, streaming, or server components -->

From plugin
vexjoy-agent
413198 skills198 agents10 commands86 hooks
Install
$ npx -y skills add notque/vexjoy-agent --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

<!-- Loaded by performance-optimization-engineer when task involves Next.js, App Router, SSR, ISR, next/image, next/font, streaming, or server components -->

Agent definition

nextjs-optimization.md

Next.js Performance Optimization Reference

<!-- Loaded by performance-optimization-engineer when task involves Next.js, App Router, SSR, ISR, next/image, next/font, streaming, or server components -->

> **Scope**: Next.js-specific performance patterns for App Router (13.4+). Pages Router patterns are noted where they differ. > **Version range**: Next.js 13.4+ (App Router stable); Next.js 14+ (Server Actions, partial prerendering) > **Generated**: 2026-04-09

---

Overview

Next.js App Router introduces React Server Components, streaming, and partial prerendering — all of which change the performance optimization playbook. The most common failure mode is applying Pages Router patterns to App Router: using `getServerSideProps` mental models with Server Components, or blocking streaming with synchronous data fetching in layout files.

---

Pattern Table

| Pattern | API | Version | Pages Router Equivalent | |---------|-----|---------|-------------------------| | Route-level caching | `export const revalidate = 60` | Next.js 13.4+ | `getStaticProps` revalidate | | Opt out of caching | `export const dynamic = 'force-dynamic'` | Next.js 13.4+ | `getServerSideProps` | | Parallel data fetching | Multiple `await` in Server Component | Next.js 13.4+ | `getServerSideProps` with `Promise.all` | | Streaming UI | `<Suspense>` around Server Components | Next.js 13.4+ | No equivalent (client-only) | | Preconnect/DNS prefetch | `<link rel="preconnect">` in `<head>` | All versions | `next/head` | | Optimized images | `next/image` with `sizes` prop | Next.js 13+ | Same | | Font optimization | `next/font` | Next.js 13+ | `@next/font` (deprecated) |

---

Correct Patterns

Parallel Data Fetching in Server Components

Fetch data in parallel, not sequentially. Each `await` in series adds to LCP.

// app/dashboard/page.tsx
async function DashboardPage() {
  // BAD: Sequential — each fetch waits for the previous
  // const user = await getUser()
  // const posts = await getPosts(user.id)
  // const analytics = await getAnalytics()

  // GOOD: Parallel — all fetches start simultaneously
  const [user, posts, analytics] = await Promise.all([
    getUser(),
    getPosts(),
    getAnalytics(),
  ])

  return <Dashboard user={user} posts={posts} analytics={analytics} />
}

**Why**: Sequential fetches in a single Server Component are serialized on the server — a 3-request chain of 200ms each = 600ms total. Parallel fetches = 200ms total. This directly impacts TTFB and LCP.

---

Streaming with Suspense for Slow Data

Use Suspense to stream fast content immediately while slow data loads.

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { AnalyticsSkeleton } from './skeletons'

export default function DashboardPage() {
  return (
    <main>
      {/* Fast: renders immediately from cache */}
      <UserProfile />

      {/* Slow: streams in when ready, shows skeleton until then */}
      <Suspense fallback={<AnalyticsSkeleton />}>
        <AnalyticsPanel /> {/* Server Component with slow DB query */}
      </Suspense>
    </main>
  )
}

**Why**: Without Suspense, the entire page waits for the slowest data source. With Suspense, React streams HTML progressively — users see content faster and LCP improves because the above-fold content doesn't block on slow queries.

---

next/image with Proper `sizes` Attribute

Always provide `sizes` to prevent downloading oversized images.

import Image from 'next/image'

// BAD: Missing sizes — Next.js downloads the largest srcset variant
<Image src="/hero.jpg" width={800} height={600} alt="Hero" />

// GOOD: sizes tells browser which breakpoint variant to download
<Image
  src="/hero.jpg"
  width={800}
  height={600}
  alt="Hero"
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px"
  priority // Add for LCP image — disables lazy loading
/>

**Why**: Without `sizes`, the browser downloads the image at full display width regardless of viewport. A 1600px image downloaded on a 375px mobile viewport is a 4-5x image size regression. The `priority` prop skips lazy loading for LCP candidates — critical for images above the fold.

---

next/font for Zero Layout Shift

Use `next/font` instead of `@import` in CSS to eliminate font-related CLS.

// app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  // Preload reduces FOUT; variable improves load efficiency
  variable: '--font-inter',
})

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

**Why**: `@import url('...')` in CSS blocks rendering. `next/font` downloads fonts at build time, serves them from the same domain (no DNS lookup), and automatically inlines the font-face declaration — eliminating the layout shift from font swapping that contributes to CLS.

---

Route Segment Config for Caching Control

Use route segment config to set cache behavior per page — not global middleware.

// app/dashboard/page.tsx

// Static: cached at build time, revalidated every 60s (ISR)
export const revalidate = 60

// Dynamic: no caching, runs on every request (like getServerSideProps)
export const dynamic = 'force-dynamic'

// Static: cached permanently until manually revalidated
export const revalidate = false // or: export const dynamic = 'force-static'

**Why**: Overusing `force-dynamic` bypasses the Next.js data cache and kills performance. Profile which routes actually need fresh data (user-specific, real-time) vs. can be cached (product pages, blog posts). Misconfigured `dynamic` is one of the most common causes of unexpectedly slow Next.js apps.

---

Pattern Catalog

Stream Slow Data with Suspense Boundaries

**Detection**:

grep -rn "async function.*Layout\|async.*layout" --include="*.tsx" --include="*.ts" app/
rg "a
Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.

Get the whole plugin, auto-invoked