Suggest an editImprove this articleRefine the answer for “Key features of Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Next.js** is a React framework with per-page rendering control: SSG generates HTML at build time, SSR on each request, ISR on a schedule, and CSR in the browser. ```tsx export const revalidate = 3600; // ISR const fresh = await fetch('/api/user', { cache: 'no-store' }); // SSR const cached = await fetch('/api/posts', { cache: 'force-cache' }); // SSG ``` **Key point:** App Router (v13+) controls rendering through fetch options. All components are server-side by default, reducing bundle size and allowing direct database access without API routes.Shown above the full answer for quick recall.Answer (EN)Image**Next.js** is a React framework that lets you mix server-side rendering (SSR), static site generation (SSG), incremental static regeneration (ISR), and client-side rendering (CSR) per page, so each route gets exactly the rendering strategy its data needs. ## Theory ### TL;DR - SSG generates HTML at build time and serves it from CDN. Fastest option, but data freezes until the next build. - SSR renders HTML on the server per request. Always fresh, but adds server latency. - ISR is the middle ground: static HTML rebuilt in the background on a schedule. - App Router (v13+) replaces `getStaticProps`/`getServerSideProps` with fetch options on async components. - All components are Server Components by default. Add `'use client'` only where you need state or browser APIs. ### Hybrid rendering The core idea: one app, multiple rendering strategies. You pick per route. ```tsx // SSG - HTML generated once at build, served from CDN async function BlogPage() { const res = await fetch('https://api.itlead.org/posts', { cache: 'force-cache' // default behavior in App Router }); const posts = await res.json(); return <PostList posts={posts} />; } // ISR - static at build, rebuilds in background every 5 minutes export const revalidate = 300; async function ProblemsPage() { const res = await fetch('https://api.itlead.org/problems'); const problems = await res.json(); return <ProblemList problems={problems} />; } // SSR - fresh HTML per request async function DashboardPage() { const res = await fetch('https://api.itlead.org/user/stats', { cache: 'no-store' }); const stats = await res.json(); return <StatsPanel stats={stats} />; } ``` The decision rule is simpler than it sounds: SSG for anything that looks the same to every visitor, SSR for anything personalized or real-time, ISR for content that changes but not every second. In practice, the biggest shift coming from Create React App is unlearning the idea of one rendering mode for the whole project. ### App Router and file-based routing Next.js 13 introduced the App Router. The folder structure inside `app/` directly maps to routes. Special files handle specific concerns without any configuration: | File | Purpose | |------|----------| | `page.tsx` | Route UI, makes the segment publicly accessible | | `layout.tsx` | Persistent wrapper across child navigations | | `loading.tsx` | Suspense-based loading UI | | `error.tsx` | Error boundary for the segment | | `not-found.tsx` | 404 UI | ```tsx // app/docs/[slug]/page.tsx // Handles: /docs/javascript, /docs/react, /docs/nextjs export default async function DocPage({ params }: { params: { slug: string } }) { const doc = await getDocument(params.slug); return <article>{doc.content}</article>; } ``` The key shift from Pages Router: `getStaticProps` and `getServerSideProps` are gone. The `fetch` options inside async Server Components replace them. Less boilerplate, same control. ### Server Components Every component in the App Router is a Server Component by default. That means its code runs on the server, never ships to the browser, and can access databases, environment variables, and the file system directly. ```tsx // app/stats/page.tsx - no client bundle impact at all import { db } from '@/lib/db'; export default async function StatsPage() { const totalUsers = await db.user.count(); const totalProblems = await db.problem.count(); return ( <div> <p>Users: {totalUsers}</p> <p>Problems: {totalProblems}</p> </div> ); } ``` When you need interactivity - state, event handlers, browser APIs - add `'use client'` at the top. The pattern that works well in production: keep data fetching in Server Components, push interactive logic down into small Client Components beneath them. ```tsx 'use client' import { useState } from 'react' export default function ThemeToggle() { const [dark, setDark] = useState(false) return ( <button onClick={() => setDark(!dark)}> {dark ? 'Light mode' : 'Dark mode'} </button> ) } ``` ### Server Actions Server Actions let you call server-side functions from Client Components without writing a separate API route. Mark the function with `'use server'` and call it like any other function. ```tsx // actions/subscribe.ts 'use server' import { db } from '@/lib/db' import { revalidatePath } from 'next/cache' export async function subscribe(email: string) { await db.subscriber.create({ data: { email } }) revalidatePath('/newsletter') // update the cached page } ``` ```tsx // Client component - no /api/subscribe route needed 'use client' import { subscribe } from '@/actions/subscribe' export default function SubscribeForm() { return ( <form action={async (formData) => { const email = formData.get('email') as string await subscribe(email) }}> <input name="email" type="email" placeholder="Email" /> <button type="submit">Subscribe</button> </form> ) } ``` Under the hood, Next.js turns the Server Action into a secure POST request. You get type safety across the client-server boundary without any manual API wiring. ### Nested Layouts Layouts in the App Router persist across navigations. The layout component mounts once and stays mounted while users navigate between child routes. ```tsx // app/docs/layout.tsx - wraps all /docs/* routes export default function DocsLayout({ children }: { children: React.ReactNode }) { return ( <div className="flex"> <Sidebar /> {/* stays mounted, keeps scroll position */} <main className="flex-1">{children}</main> </div> ) } ``` Going from `/docs/javascript` to `/docs/react`? The sidebar does not remount. Scroll position, open sections, local state in the layout all survive. Only `children` re-renders. That is the whole point of nested layouts. ### Built-in optimizations Three components handle most performance concerns out of the box. **`next/image`** resizes images, converts to WebP or AVIF, and handles lazy loading automatically. You provide dimensions, it does the rest. ```tsx import Image from 'next/image' export default function Avatar() { return ( <Image src="/avatar.png" width={64} height={64} alt="User avatar" /> ) } ``` **`next/link`** prefetches pages in the background when a link enters the viewport. By the time a user clicks, the page data is already loaded. ```tsx import Link from 'next/link' export default function Nav() { return ( <nav> <Link href="/problems">Problems</Link> <Link href="/docs">Documentation</Link> </nav> ) } ``` **`next/font`** downloads Google Fonts at build time and self-hosts them. No client-side requests to Google servers, no layout shift from late-loading fonts. ```tsx import { Inter } from 'next/font/google' const inter = Inter({ subsets: ['latin', 'cyrillic'] }) export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html className={inter.className}> <body>{children}</body> </html> ) } ``` ### Middleware Middleware runs at the Edge before a request hits your application. You can redirect, rewrite URLs, or modify headers. The Edge Runtime is not Node.js - `fs`, `path`, and other Node-only APIs are not available here. ```tsx // middleware.ts (project root) import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export function middleware(request: NextRequest) { const locale = request.cookies.get('locale')?.value || 'en' if (!request.nextUrl.pathname.startsWith(`/${locale}`)) { return NextResponse.redirect( new URL(`/${locale}${request.nextUrl.pathname}`, request.url) ) } } export const config = { matcher: ['/((?!api|_next|favicon.*))'] } ``` Common uses: authentication guards, locale detection, A/B testing redirects, security header injection. ### Metadata API Next.js has a built-in API for SEO metadata. Export a `metadata` object from any `page.tsx` or `layout.tsx`: ```tsx import type { Metadata } from 'next' export const metadata: Metadata = { title: 'JavaScript Problems — IT Lead', description: 'Solve problems from real frontend interviews', openGraph: { title: 'JavaScript Problems', description: 'Solve problems from real frontend interviews', type: 'website' } } export default function ProblemsPage() { return <ProblemsList /> } ``` For per-item metadata (blog posts, product pages), export `generateMetadata` as an async function instead. It receives the route params and can fetch data to build the title and description dynamically. ### Common mistakes **Forgetting `cache: 'no-store'` on SSR pages.** The App Router caches `fetch` calls by default. A user dashboard that calls `fetch('/api/user')` without options caches the response at build time and serves the same stale data to everyone. ```tsx // Wrong - caches like SSG, serves stale user data to every visitor const data = await fetch('/api/user'); // Correct - fresh data per request const data = await fetch('/api/user', { cache: 'no-store' }); ``` **Putting `'use client'` at the top of a page that only needs one interactive element.** This forces full CSR on the entire page and removes server-rendered HTML. Move state and event handlers into small child Client Components instead. ```tsx // Wrong - whole page becomes CSR, SEO gone 'use client' export default async function Page() { ... } // Correct - page is server-rendered, interactivity is isolated export default async function Page() { const data = await fetchData(); return <InteractiveChild data={data} />; // 'use client' lives inside here } ``` **Thinking `revalidate: 0` enables SSR.** Setting `export const revalidate = 0` does not make a route server-rendered. The page is still static at build. For actual per-request rendering use `export const dynamic = 'force-dynamic'` or add `cache: 'no-store'` to all fetch calls. **Skipping `<Suspense>` around streaming components.** Without a Suspense boundary, an async Server Component blocks the entire page from sending any HTML until it resolves. Wrap slow components in `<Suspense fallback={<Loader />}>`. ### Real-world usage - Vercel.com: SSG for documentation (global CDN delivery), SSR for the authenticated dashboard. - Hashnode: SSG for blog posts, SSR for comment sections and user-specific feeds. - E-commerce stores: ISR for product pages with `revalidate: 60`, so prices update without a full rebuild. - Admin panels: CSR with `'use client'` throughout, since SEO is irrelevant and data changes on every action. ### Follow-up questions **Q:** What is the difference between `cache: 'no-store'` and `export const dynamic = 'force-dynamic'`? **A:** `cache: 'no-store'` applies to a single fetch call and opts that one request out of caching. `dynamic = 'force-dynamic'` marks the entire route as dynamic and cascades to all child components, overriding any route-level caching. **Q:** How does Turbopack affect build times? **A:** Turbopack replaces Webpack as the bundler in development. Next.js 14 reports around 700x faster hot module replacement in dev mode. Production builds still use Webpack as of Next.js 14. **Q:** What is an RSC payload and how does it relate to streaming? **A:** React Server Components serialize their output as an RSC payload, not raw HTML. The client receives a static HTML shell first, then the RSC payload streams in to hydrate interactive parts. Wrapping slow components in `<Suspense>` sends the shell immediately and streams the slow parts after they resolve. **Q:** When does a hydration mismatch happen and how do you fix it? **A:** When the server-rendered HTML does not match what React expects to render on the client. Common causes: timestamps, random IDs, or browser-only APIs called during render. Fix with `useEffect` to defer client-only code, or `suppressHydrationWarning` on the element for harmless differences. **Q:** In a high-traffic app, how would you invalidate ISR cache across multiple regions? **A:** Use `revalidateTag` or `revalidatePath` triggered by a webhook when content changes. Pair with Upstash Redis as a shared cache store across Edge regions. For paths where any stale data is unacceptable, switch to SSR with `cache: 'no-store'`. ## Examples ### ISR product page A product page that rebuilds in the background every 60 seconds. Users always get a valid cached response. The update happens without any deploy. ```tsx // app/products/[id]/page.tsx interface Product { id: string; name: string; price: number; } export const revalidate = 60; export default async function ProductPage({ params }: { params: { id: string } }) { const product: Product = await fetch( `https://api.example.com/products/${params.id}`, { next: { revalidate: 60 } } ).then(res => res.json()); return ( <div> <h1>{product.name}</h1> <p>${product.price}</p> </div> ); } // Static HTML from CDN, product data updated in background - no rebuild needed ``` ### Streaming dashboard with Suspense The header renders instantly. The analytics panel streams in after its data resolves. Without `<Suspense>`, the entire page waits for the slowest component to finish. ```tsx // app/dashboard/page.tsx import { Suspense } from 'react' import Analytics from './analytics' export default function Dashboard() { return ( <> <Header /> {/* visible at ~50ms */} <Suspense fallback={<div>Loading analytics...</div>}> <Analytics /> {/* streams in at ~2s */} </Suspense> </> ) } // app/dashboard/analytics.tsx export default async function Analytics() { const data = await fetch('https://api.itlead.org/analytics', { cache: 'no-store' }).then(r => r.json()); return <Chart data={data} />; } // No full-page spinner. Users see the shell while analytics load. ``` ### Server Action with cache invalidation Form submission writes to the database and invalidates the relevant cached route. No separate API file needed. ```tsx // actions/solution.ts 'use server' import { db } from '@/lib/db' import { revalidatePath } from 'next/cache' export async function submitSolution(formData: FormData) { const code = formData.get('code') as string const problemId = formData.get('problemId') as string await db.solution.create({ data: { code, problemId } }) revalidatePath(`/problems/${problemId}`) // invalidate cached problem page } ``` ```tsx // app/problems/[id]/submit.tsx 'use client' import { submitSolution } from '@/actions/solution' export default function SubmitForm({ problemId }: { problemId: string }) { return ( <form action={submitSolution}> <input type="hidden" name="problemId" value={problemId} /> <textarea name="code" placeholder="Your solution..." rows={10} /> <button type="submit">Submit solution</button> </form> ) } // Form posts to server action, writes to DB, refreshes the cached problem page ```For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.