Next.js 16 — npx create-next-app Installed 3,400 Dependencies: Why?
What does create-next-app actually install? 3,400+ node_modules dependencies explained.
20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.
- ✓Solid grasp of React fundamentals (components, props, state)
- ✓Node.js 18+ installed on your machine
- ✓Basic understanding of npm or yarn package managers
- ✓Familiarity with the command line
- create-next-app installs 3,400+ packages because Next.js bundles Webpack, SWC, PostCSS, Babel, ESLint, and 40+ transitive dependencies per package
- The default starter ships with 5 files but generates 12,000+ files in node_modules
- You can trim to ~800 deps with
--turboand a minimal ESLint config - Every dependency exists because Next.js chose bundling over CDN — you own the full toolchain
Think of create-next-app like ordering a fully stocked kitchen instead of just buying a stove. You get every pot, pan, utensil, and ingredient you might ever need — but your pantry now has 3,400 items. Most of them sit unused, but they're there so you can cook anything without a second trip to the store. The difference is, in software, those unused items still take up space and need security updates.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
You ran npx create-next-app@latest my-app, waited 47 seconds, and now you have 3,400 packages in node_modules. For a "Hello World" app. If that feels wrong, you're not crazy — it is wrong. But it's also intentional.
Next.js 16 ships with a complete build toolchain baked in: Webpack (or Turbopack), SWC for transpilation, PostCSS for CSS processing, ESLint for linting, and a dozen Babel plugins for compatibility. Each of those tools pulls in its own dependencies, and those pull in more. The result is a dependency tree that looks like a fractal.
This article walks through every major dependency group in a fresh Next.js 16 install. You'll learn what each package does, which ones you actually need, and how to strip your project down to the essentials for production. By the end, you'll understand why 3,400 packages isn't necessarily bad — but you'll also know how to audit every single one.
We'll cover the project structure, the build pipeline, the dev server, and the production optimizations. You'll never run npm install blind again.
What Actually Happens When You Run create-next-app
When you type npx create-next-app@latest my-app, you're not just scaffolding files. You're invoking a CLI that downloads the latest Next.js starter template, then runs npm install (or yarn/pnpm) to resolve the dependency tree. The template itself is tiny — about 12 files. But the dependency tree it pulls in is massive because Next.js is a meta-framework that bundles its entire toolchain.
The package.json that ships with the starter lists about 15 top-level dependencies. But each of those has its own dependencies, and those have more. The tree fans out exponentially. next itself depends on @next/env, @next/swc, postcss, cssnano, styled-jsx, and 80+ other packages. eslint-config-next alone pulls in @typescript-eslint/parser, eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-import, and 40+ transitive deps.
The real shocker: 40% of those packages are never loaded at runtime. They exist only for build-time linting, type checking, or edge cases you'll never hit.
eslint-config-next, @next/bundle-analyzer, and @types/* devDependencies, it dropped to 1,800. The app worked identically.npm ls --depth=0 immediately after scaffolding to audit top-level deps--turbo flag replaces Webpack with Turbopack, cutting ~400 transitive depsThe Project Structure — What Every Folder Actually Does
After scaffolding, you get app/, public/, next.config.js, tailwind.config.ts, tsconfig.json, and postcss.config.mjs. The app/ directory is where you'll live — it's the App Router, the filesystem-based routing layer introduced in Next.js 13 and fully stabilized in 16. Every file inside app/ becomes a route, an API handler, or a layout.
The public/ folder serves static assets at the root URL. next.config.js is your escape hatch for Webpack/Turbopack config, image optimization settings, and redirects. tailwind.config.ts controls Tailwind's content scanning — and misconfiguring it is the #1 cause of 12MB CSS files (covered in Article 5).
What beginners miss: layout.js and page.js are not optional. Every route segment needs a page.js. Every shared UI needs a layout.js. If you delete layout.js from the root, your entire app returns 404. The filesystem is not a suggestion — it's the contract.
/image.png)The Build Pipeline — Webpack vs Turbopack vs SWC
Next.js 16 ships with two build engines: Webpack (default) and Turbopack (opt-in via --turbo). Webpack has been the backbone of Next.js since v6, but it's old — first released in 2012. It processes JavaScript through loaders and plugins, and it's incredibly flexible. That flexibility comes at a cost: 400+ packages just for the Webpack ecosystem.
Turbopack is the Rust-based successor. It's 10x faster at cold starts and 4x faster at HMR (hot module replacement). But it's not a drop-in replacement — some Webpack plugins don't work with Turbopack yet. If you use @next/mdx, custom Webpack loaders, or next-compose-plugins, you're stuck on Webpack.
SWC (Speedy Web Compiler) is the Rust-based transpiler that replaced Babel in Next.js 13+. It's 20x faster at transforming JSX and TypeScript. But SWC doesn't support all Babel plugins — if you rely on babel-plugin-macros or custom Babel transforms, you'll need to opt out of SWC entirely.
--turbo flag is stable in Next.js 16 — use it for all new projectsThe 12 Files You Actually Get — And What Each One Does
After scaffolding, you get: app/layout.tsx, app/page.tsx, app/globals.css, app/favicon.ico, next.config.js (or .ts), tailwind.config.ts, postcss.config.mjs, tsconfig.json, package.json, next-env.d.ts, .eslintrc.json, and README.md. That's it. Twelve files. But each one has a specific job that beginners often misunderstand.
app/layout.tsx is the root layout — it wraps every page in your app. It's required. Without it, Next.js throws a 500 error on every route. The <html> and <body> tags go here. app/page.tsx is your index route — it maps to /. Add app/about/page.tsx and you get /about. No config, no router setup, no BrowserRouter wrapper.
globals.css is imported in the root layout and applies globally. tailwind.config.ts tells Tailwind which files to scan for class names. If you add files outside the configured content paths, Tailwind won't generate those classes — and you'll wonder why bg-red-500 doesn't work.
app/about/page.tsx = /about. Period.app/blog/page.tsx but also added a pages/blog.js file (Pages Router). Both routes compiled, but the Pages Router version took priority. They spent 4 hours wondering why their App Router layout wasn't wrapping the blog page.The package.json — What Each Dependency Actually Does
Your scaffolded package.json lists next, react, react-dom, and a few dev dependencies. But next alone pulls in 80+ packages. Let's break down the major groups.
next itself is a meta-package that depends on: @next/env (environment variable loading), @next/swc-* (Rust-based transpiler binaries for each OS), postcss (CSS processing), cssnano (CSS minification), styled-jsx (built-in CSS-in-JS), and sharp (image optimization, macOS only). Each of those has its own tree.
react and react-dom are clean — they pull in almost nothing. But @types/react and @types/react-dom (if you use TypeScript) add 12 more packages. tailwindcss pulls in postcss, postcss-import, postcss-nesting, autoprefixer, and cssnano — many of which are already installed by Next.js. This duplication is why npm dedupe can save 200+ packages.
npm dedupe after install — it can remove 200+ duplicate transitive packages.npm dedupe on a 6-month-old Next.js project and it removed 340 packages. The app still compiled. The build output was identical. The only difference: CI was 90 seconds faster.next is a meta-package — it pulls in 80+ transitive dependencies on its ownreact and react-dom are lean — they add almost nothing to the treeeslint-config-next is the single biggest contributor to dependency bloatnpm dedupe after every major install to collapse duplicate versionsThe Hidden Cost of TypeScript Types
If you scaffold with --typescript, you get @types/react, @types/react-dom, @types/node, and typescript itself. These are dev dependencies — they don't ship to production. But they still take up disk space and install time. @types/react alone is 2MB of type definitions. typescript is 45MB.
The real cost is in CI. Every CI pipeline installs dev dependencies by default. If you're not using npm ci --production or NODE_ENV=production, you're installing TypeScript and all type definitions in your production build step. That's 50MB+ of unnecessary downloads.
Pro tip: Use next build which automatically strips TypeScript from the production bundle. But the type-checking step still runs during build — and it requires typescript to be installed. If you want to skip type-checking in CI, set ignoreBuildErrors: true in next.config.js and run tsc --noEmit as a separate CI step.
node_modules into the production stage — including typescript, all @types/* packages, and eslint. Switching to multi-stage builds with npm ci --production in the final stage dropped the image to 180MB.npm ci --production in Docker production stages to skip dev dependenciesnext build strips TypeScript from the bundle, but still runs type-checking during buildignoreBuildErrors: true in next.config.js if you run tsc separately in CIThe Lockfile — Your Only Source of Truth
package-lock.json (or yarn.lock / pnpm-lock.yaml) is the most important file in your project that you'll never read. It pins every single dependency to an exact version, including transitive dependencies. Without it, two developers running npm install on the same package.json could get different trees — and one of them will hit a bug that the other can't reproduce.
Next.js 16's lockfile is typically 8,000-12,000 lines. That's not a bug — it's npm being precise. Every entry includes the resolved URL, integrity hash, and dependency tree. If you delete package-lock.json and reinstall, you'll get different minor versions of everything, and something will break.
Commit your lockfile. Never add it to .gitignore. If you use npm ci in CI (which you should), it reads the lockfile and installs exact versions. npm install ignores the lockfile and resolves fresh — which is why CI builds break on Monday after a package published a breaking change on Sunday.
npm install in CI. One Monday, eslint-plugin-react published a patch that broke their custom lint rule. Every PR failed. The fix: commit the lockfile and use npm ci in CI.npm ci in CI, not npm install — ci respects the lockfile, install resolves freshnpm audit after every install — 3,400 packages means 3,400 potential CVEspnpm for new projects — it uses a content-addressable store and deduplicates globallyThe Dev Server — What Happens When You Run next dev
When you run next dev, Next.js starts a development server on port 3000. But behind the scenes, it's doing a lot more. It compiles your code on-the-fly using SWC (or Babel if configured), starts a file watcher, and sets up Hot Module Replacement (HMR). HMR means when you edit a file, only that module is replaced — not the whole page.
With Turbopack (next dev --turbo), HMR is nearly instant. Changes to React components reflect in under 50ms. Without Turbopack, Webpack's HMR takes 200-500ms for the same change. The difference is noticeable when you're iterating on UI — 50ms feels instant, 500ms feels like a delay.
But here's the catch: Turbopack doesn't support all Webpack features. If your next.config.js has a custom webpack function, Turbopack is disabled. If you use next-compose-plugins, Turbopack is disabled. If you use @next/mdx with a custom loader, Turbopack is disabled. Read the Turbopack compatibility table before migrating.
--turbo flag is stable in Next.js 16 — use it for all new projectsThe Production Build — What next build Actually Produces
When you run next build, Next.js compiles your app into a .next folder. This folder contains: server/ (server-side rendered pages and API routes), static/ (client-side JavaScript chunks), chunks/ (shared code), standalone/ (if output: 'standalone' is configured), and build-manifest.json (maps routes to chunks).
The build process: 1) SWC transpiles all TypeScript/JSX to JavaScript, 2) Webpack/Turbopack bundles everything into chunks, 3) Next.js generates static pages (if using generateStaticParams), 4) The output is optimized with tree-shaking and minification.
What beginners don't realize: next build also runs type-checking. If you have TypeScript errors, the build fails. This is intentional — you shouldn't ship code with type errors. But if you're migrating a large codebase, you can set typescript.ignoreBuildErrors: true in next.config.js to skip type-checking during build and run tsc --noEmit separately.
typescript.ignoreBuildErrors: true in next.config.js and ran tsc --noEmit as a pre-commit hook. Builds never failed for type errors again..next/ is generated — never commit itnext build runs type-checking by default — configure typescript.ignoreBuildErrors if you run tsc separately.next/output: 'standalone' for Docker deployments — it minimizes the production imageEnvironment Variables — The @next/env Package
Next.js includes @next/env to load .env.local, .env.development, .env.production, and .env files. The priority order is: .env.local > .env.development/.env.production > .env. Variables are loaded at build time and inlined into the client bundle if prefixed with NEXT_PUBLIC_.
This is a common source of confusion: variables without NEXT_PUBLIC_ prefix are only available on the server. If you try to access process.env.API_KEY in a client component, it'll be undefined. The NEXT_PUBLIC_ prefix tells the bundler to inline the value at build time — which means it's baked into the JavaScript bundle. Anyone who reads your source code can see it.
Never put secrets in NEXT_PUBLIC_ variables. They're visible in the browser's DevTools. Use server-only environment variables for API keys, database URLs, and auth secrets. Access them only in Server Components, API routes, or Server Actions.
NEXT_PUBLIC_STRIPE_SECRET variable. It was visible in the browser's DevTools network tab. Attackers used it to create subscriptions on behalf of other users. The fix: move all secrets to server-only env vars and use API routes as proxies..env.local overrides all other env files — never commit it@next/env programmatically if you need env loading outside of Next.jsThe Public Folder — Static Assets and the Root URL
The public/ folder maps to the root URL of your site. A file at public/logo.png is served at /logo.png. A file at public/images/hero.jpg is served at /images/hero.jpg. This is the only way to serve static files in Next.js — you can't put images in app/ and expect them to be served.
But there's a trap: files in public/ are copied as-is during build. They're not optimized, not hashed, and not compressed. If you put a 5MB hero image in public/, it ships as 5MB. Use next/image for optimization instead — it generates multiple sizes, serves WebP/AVIF, and lazy-loads by default.
Another trap: public/ is served at the root, not at /public/. A file at public/images/logo.png is at /images/logo.png, not /public/images/logo.png. Beginners often link to /public/images/logo.png and get a 404.
public/ without using next/image. Their Lighthouse score was 42 for performance. After migrating to next/image with next.config.js image optimization, the score jumped to 94. The hero image alone went from 2.4MB to 180KB.next/image for optimization — files in public/ are served rawremotePatterns in next.config.js for external imagesThe Config Files — next.config.js, tailwind.config.ts, tsconfig.json
next.config.js (or .ts in Next.js 16) is your escape hatch. It's where you configure image domains, redirects, headers, Webpack, and experimental features. The default config is empty — Next.js works without any configuration. But you'll quickly need to add images.remotePatterns for external images, redirects for URL changes, and headers for security policies.
tailwind.config.ts controls which files Tailwind scans for class names. The default content array includes ./app/*/.{js,ts,jsx,tsx} and ./components/*/.{js,ts,jsx,tsx}. If you add files outside these paths (e.g., in lib/ or utils/), Tailwind won't generate those classes. This is the #1 cause of "my Tailwind class doesn't work" — the class is correct, but Tailwind never scanned the file.
tsconfig.json is generated by Next.js and extends next.config.ts's types. The key settings: moduleResolution: "bundler" (required for Next.js 16), jsx: "preserve" (SWC handles JSX, not TypeScript), and paths with @/ mapping to ./src/ or ./*. Never manually edit the compilerOptions section that Next.js manages — it gets overwritten on every build.
strict: false in tsconfig.json to suppress errors. Next.js overwrote it back to true on the next build. The lesson: Next.js enforces strict mode. If you want to relax it, you need to set strict: false in a separate tsconfig.custom.json and extend it.The First Page — From Scaffold to Production
Your first page is app/page.tsx. It's a Server Component by default — meaning it runs on the server, sends only HTML to the client, and never ships JavaScript. This is the most important concept in Next.js 16: every component is a Server Component unless you add 'use client'.
Server Components can be async — you can await a database query or a call directly in the component body. No fetch()useEffect, no useState, no loading states. The component renders on the server, the HTML is sent to the client, and the JavaScript for that component is zero bytes.
But Server Components can't use hooks, event handlers, or browser APIs. If you need useState, useEffect, onClick, or window access, you need a Client Component. The boundary is explicit: add 'use client' at the top of the file. Everything below that boundary runs on the client.
await fetch() or await db.query() directly in a Server Component. No useEffect needed.'use client' on every file because they didn't understand Server Components. Their bundle was 480KB for a blog. After moving data fetching to Server Components, the bundle dropped to 120KB. Time to First Byte (TTFB) improved by 40%.The node_modules Rabbit Hole — What You Can Safely Delete
Not everything in node_modules is needed. Dev dependencies (devDependencies in package.json) are only used during development and build. They're not included in the production bundle, but they're still installed on disk. You can safely delete: @types/ (type definitions), eslint and all eslint-plugin- packages, typescript itself, prettier, husky, and lint-staged.
But there's a catch: some packages are both dev and production dependencies depending on how you use them. tailwindcss is a dev dependency — it's only used at build time. But postcss is a dependency of tailwindcss, and postcss is also a dependency of next. So even if you remove tailwindcss, postcss stays because next needs it.
The best tool for auditing this is npm ls --depth=0 --production. This shows only the packages that will be installed with --production flag. Compare it to npm ls --depth=0 (all packages) to see what's dev-only. If you see a package in the full list but not in the production list, it's safe to remove from dependencies and move to devDependencies.
npm ls --depth=0 --production to see what actually ships to production.react-dev-utils in their production dependencies. It's a CRA utility that shows build errors in the browser. It was being bundled into their production app. Removing it saved 40KB from the bundle and eliminated a security advisory.npm ls --depth=0 --production to audit what actually shipsnpm prune --production to strip dev deps from node_modulesThe First Deploy — What You Need to Know Before Shipping
Before you deploy, there are three things you must configure: environment variables, image optimization, and the build command. Environment variables for production go in .env.production (or your hosting platform's dashboard). Never commit .env.local — it's in .gitignore by default, but double-check.
Image optimization requires configuring remotePatterns in next.config.js if you're loading images from external domains. Without this, next/image will refuse to serve external images in production. The error message is clear: "The requested resource isn't a valid image." But beginners often miss it because it works in development.
The build command for production is next build && next start. On Vercel, this happens automatically. On other platforms (AWS, Railway, Fly.io), you need to configure the build command and start command explicitly. The output: 'standalone' option creates a minimal .next/standalone folder with only the files needed to run the app — perfect for Docker.
output: 'standalone' in next.config.js to get a minimal production folder.output: 'standalone'. Their Docker image was 1.8GB because it included the entire node_modules with dev dependencies. After switching to standalone output and multi-stage builds, the image was 180MB. Cold starts dropped from 45 seconds to 8 seconds.images.remotePatterns before deploying — external images fail silently in productionoutput: 'standalone' for Docker deployments to minimize image sizeNODE_ENV=production in your production environmentnext build && next start before deployingThe Scripts — dev, build, start, and lint
The scaffolded package.json includes four scripts: dev, build, start, and lint. dev starts the development server with HMR. build creates the production bundle. start serves the production bundle. lint runs ESLint across your codebase.
But there's a hidden script: next telemetry. Next.js collects anonymous usage data by default. Run npx next telemetry disable to opt out. It's not malicious — Vercel uses it to prioritize features — but you should know it's on by default.
Another hidden command: next info. This prints your system information (OS, Node version, npm version, Next.js version) — useful for debugging and bug reports. Run it before filing an issue on GitHub.
npx next telemetry disable on every new project. It's opt-out by default.next info and discovered they were on Node 18 while CI was on Node 20. The sharp image optimization library had different behavior across versions. Pinning Node to 20 in .nvmrc and CI fixed it.next info before filing bug reports — it prints system info automaticallynpx next telemetry disable--turbo flag is passed to next dev, not next build (build always uses Webpack/Turbopack)next lint runs ESLint with the Next.js config — no separate eslint command neededThe First Edit — Adding a Second Page
To add a second page, create app/about/page.tsx. That's it. The route /about now exists. No router config, no route registration, no BrowserRouter wrapper. The filesystem IS the router. This is the fundamental mental shift from React Router to Next.js.
But there's a subtlety: nested routes need nested folders. app/blog/[slug]/page.tsx creates /blog/:slug. The [slug] is a dynamic segment — you access it via params in the component props. For catch-all routes, use [...slug] (matches one or more segments) or [[...slug]] (matches zero or more).
Group routes use (groupName) folders — they don't affect the URL path. app/(marketing)/about/page.tsx still maps to /about. This is useful for organizing routes without changing URLs. Parallel routes use @slotName folders for rendering multiple pages in the same layout (e.g., a dashboard with independent feed and sidebar).
app/(dashboard)/settings/page.tsx and app/(marketing)/settings/page.tsx — two different settings pages at the same URL. Next.js threw a duplicate route error. The fix: use different URL paths or use route groups with different layouts.app/blog/[slug]/page.tsx → /blog/:slug(group) don't affect the URL — they're for organization only[param] syntax — catch-all uses [...param]@slot folders — they render independent UIs in the same layoutThe First Bug — Why Your Page Returns 404
The most common beginner bug: you create app/blog/page.tsx but the route returns 404. The cause is almost always one of: missing layout.js in a parent segment, a typo in the folder name, or a file named page.tsx instead of page.tsx (yes, the extension matters).
Next.js requires every route segment to have a page.js (or page.tsx) file. If you create app/blog/ but only put a layout.tsx there without a page.tsx, the route returns 404. The layout wraps the page — it doesn't replace it.
Another common issue: case sensitivity. app/Blog/page.tsx and app/blog/page.tsx are different routes on Linux (case-sensitive filesystem) but the same on macOS (case-insensitive by default). This causes "works on my machine" bugs. Always use lowercase for folder names.
app/Blog/ ≠ app/blog/ in production.app/About/page.tsx on macOS. It worked locally. On the Linux CI server, /about returned 404 because the filesystem saw About ≠ about. The fix: rename to lowercase and add a CI step that checks for uppercase folder names.The First Optimization — Reducing Your Dependency Count
You can reduce your dependency count from 3,400 to ~800 without losing any functionality. Here's how: 1) Use --turbo flag (removes Webpack and its 400 plugins), 2) Remove eslint-config-next and use a minimal ESLint config, 3) Remove @next/bundle-analyzer if you're not using it, 4) Use npm dedupe to collapse duplicate versions, 5) Switch to pnpm which uses a content-addressable store and hard links.
But don't optimize prematurely. 3,400 packages on disk is not a performance problem — it's a disk space and CI time problem. The production bundle is tree-shaken. Only the code you actually import ends up in the browser. The 3,400 packages in node_modules are just sitting there, taking up space, not affecting your users.
The real cost is CI time, disk space on your build server, and the attack surface for supply-chain vulnerabilities. Every package in node_modules is a potential vector for npm audit warnings. With 3,400 packages, you'll have at least 5-10 moderate-severity advisories on day one.
npm audit after every install.npm audit warnings for 6 months. When they finally ran it, they had 47 vulnerabilities — 3 critical. One was in a transitive dependency of eslint-config-next that allowed arbitrary code execution during linting. They removed eslint-config-next and the critical vulnerabilities disappeared.npm audit after every npm install — 3,400 packages means at least 5 advisoriesnpm audit --production to see only runtime vulnerabilities (not dev)pnpm — it deduplicates globally and has better security featuresThe Hidden Files — next-env.d.ts, .next, and Gitignore
next-env.d.ts is a TypeScript declaration file that Next.js generates and manages. It references Next.js's type definitions. Never edit it — Next.js overwrites it on every build. If you delete it, TypeScript will complain about missing types for next/image, next/link, and other Next.js APIs.
.next/ is the build output directory. It's generated by next build and next dev. It contains compiled code, cached SWC transforms, and static files. Never commit it. The default .gitignore from create-next-app includes .next/, node_modules/, and .env.local.
But there's a missing entry: .env.local files. The default .gitignore only ignores .env.local, not .env.development.local or .env.production.local. Add .local to catch all local env files.
.env.development.local with their AWS access keys. The repo was private, but a contractor had access. The keys were used to spin up $12,000 worth of EC2 instances. The fix: add *.local to .gitignore and rotate all keys.*.local to .gitignore — the default only ignores .env.local.next/ is generated — never commit itnext-env.d.ts is auto-managed — never edit itnext info to debug environment issues before filing bug reportsThe First Week — What to Learn and What to Ignore
In your first week with Next.js, focus on: the filesystem router (app/ directory), Server Components vs Client Components, data fetching in Server Components, and the build pipeline. Ignore: middleware (you don't need it yet), route handlers (API routes), parallel routes, intercepting routes, and Partial Prerendering (PPR).
The most common mistake beginners make is treating Next.js like React with a router. It's not. Next.js is a framework that makes assumptions about how you build. The filesystem is the router. Server Components are the default. Data fetching happens in the component, not in useEffect. If you fight these assumptions, you'll have a bad time.
Embrace the conventions: put pages in app/, put shared UI in components/, put utilities in lib/, put types in types/. This structure scales from 1 page to 1,000 pages without refactoring.
page.tsx and used client-side routing with next/navigation. They lost all the benefits of Server Components, code splitting, and ISR. Their bundle was 600KB for a marketing site. After refactoring to use the filesystem router and Server Components, it dropped to 120KB.The First Production Build — What to Expect
Your first next build will take 30-60 seconds. It compiles all your pages, generates static pages, optimizes images, and produces the output in .next/. The build output shows you: which routes are static vs dynamic, how large each page's JavaScript is, and whether any pages failed to compile.
Static routes (no dynamic data) are pre-rendered at build time. Dynamic routes (using , cookies(), headers()searchParams, or with fetch()cache: 'no-store') are rendered on each request. The build output marks each route with a symbol: ● for static, λ for dynamic, ○ for ISR (Incremental Static Regeneration).
If you see λ on a page you expected to be static, check if it uses , headers(), cookies()searchParams, or with fetch()cache: 'no-store'. Any of these force dynamic rendering. To make a page static, remove those dependencies or use generateStaticParams to pre-render specific paths.
λ (dynamic) because they used headers() to read the user's preferred language. They moved language detection to the client side and used generateStaticParams to pre-render all 1,000 posts. Build time went from 2 minutes to 30 seconds, and TTFB dropped from 800ms to 50ms.headers() and cookies() force dynamic rendering — use them sparinglygenerateStaticParams pre-renders dynamic routes at build time3,400 Dependencies — The CI Pipeline That Took 14 Minutes
npm install. The team assumed it was normal for Next.js.create-next-app includes eslint-config-next with 47 peer dependencies, @next/bundle-analyzer (unused), and the full Webpack config chain. The team never ran npm ls --depth=0 to audit what was installed. 1,200 of the 3,400 packages came from eslint-config-next alone.--turbo flag (Turbopack replaces Webpack), removed eslint-config-next, installed only eslint with a minimal config, and added .npmrc with legacy-peer-deps=false. CI install time dropped from 11 minutes to 2.4 minutes.- Run
npm ls --depth=0after everycreate-next-appto audit top-level dependencies - Use
--turboflag for new projects — it replaces Webpack with Turbopack and halves dependency count - Don't install
eslint-config-nextuntil you actually need it — it pulls in 1,200+ transitive deps - Add
.npmrcwithauto-install-peers=falseto prevent npm from pulling in optional peer deps
npm ls --depth=0 to see top-level packages. Look for eslint-config-next, @next/bundle-analyzer, and duplicate versions of React.du -sh node_modules/* | sort -rh | head -20 to find the largest packages.npm ci is being used instead of npm install. npm ci is faster and respects lockfile exactly.npm ls react to see if multiple React versions are installed. This happens when a package pins an older React version.npm ls --depth=0npm ls --depth=0 --all | wc -leslint-config-next and @next/bundle-analyzer if unused| File | Command / Code | Purpose |
|---|---|---|
| audit-deps.sh | npm ls --depth=0 --all 2>/dev/null | wc -l | What Actually Happens When You Run create-next-app |
| terminal.sh | npx create-next-app@latest my-app --turbo --typescript --tailwind --eslint --app... | The Project Structure |
| next.config.ts | const nextConfig: NextConfig = { | The Build Pipeline |
| app | export default function RootLayout({ | The 12 Files You Actually Get |
| package.json | { | The package.json |
| Dockerfile | FROM node:20-alpine AS deps | The Hidden Cost of TypeScript Types |
| ci-install.sh | npm ci | The Lockfile |
| terminal.sh | npm run dev -- --turbo | The Dev Server |
| app | export async function GET() { | Environment Variables |
| app | export default function Home() { | The Public Folder |
| app | export default async function Home() { | The First Page |
| audit.sh | npm ls --depth=0 --production 2>/dev/null | The node_modules Rabbit Hole |
| terminal.sh | npx next info | The Scripts |
| app | export default async function BlogPost({ | The First Edit |
| check-case.sh | find app -type d | grep -E '[A-Z]' && echo "ERROR: Uppercase folder names found ... | The First Bug |
| security-audit.sh | npm audit | The First Optimization |
| .gitignore | /node_modules | The Hidden Files |
| app | export const metadata: Metadata = { | The First Week |
| app | export async function generateStaticParams() { | The First Production Build |
Key takeaways
--turbo flag for new projects to replace Webpack with Turbopack, cutting ~400 packageseslint-config-next is the single biggest contributor to dependency bloatnpm ci in CI for reproducible, fast installsnpm dedupe after every install to collapse duplicate transitive dependenciesnpm ci --production in the final stage reduce image size by 70%Interview Questions on This Topic
What happens when you run `npx create-next-app`? Walk through the entire process.
create-next-app package from npm. It prompts for configuration (TypeScript, ESLint, Tailwind, src/ directory, import alias). Then it scaffolds the project files: app/layout.tsx, app/page.tsx, app/globals.css, next.config.js, tailwind.config.ts, tsconfig.json, postcss.config.mjs, and package.json. Finally, it runs npm install which resolves the dependency tree — next pulls in 80+ packages, eslint-config-next pulls in 1,200+, and the total reaches 3,400+. The lockfile is generated with exact versions for every transitive dependency.Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.
That's Next.js. Mark it forged?
12 min read · try the examples if you haven't