src/ folder is not something a browser can load.Look at the top of any file in your app. import { Button } from "@/components/Button". import "./globals.css". A .tsx extension. HTML tags inside JavaScript. A browser can run a plain import "./x.js", but not one of these: the @/ alias, a bare package name, a CSS import, types, JSX.
Try it. Serve the file as-is and open it in a tab.
import { Button } from "@/components/Button"; // ① what's "@/"? import "./globals.css"; // ② importing CSS? import { format } from "date-fns"; // ③ from where? export default function Page({ user }: { user: User }) { // ④ types return <Button size="lg">Hi {user.name}</Button>; // ⑤ HTML in JS }
import. Transform each file. Glue the result.That's the whole job. The bundler reads page.tsx, sees three imports, opens each, sees their imports, and keeps going until it's seen everything reachable. Along the way it strips types, converts JSX, and rewrites paths.
Press Bundle and watch it walk. Then click any file to remove its import, and bundle again.
The bundler only includes code it can reach from the entry. Delete the import and the whole branch falls out. It's why "just install the package" costs nothing until you actually import from it.
Tree-shaking goes one step finer: import { format } from "date-fns" pulls in format and what it needs, not the other 200 functions in the package. The 18 kB node in the demo is the honest size of that slice, not the whole library.
And it's why node_modules being 400 MB doesn't mean your users download 400 MB. They get the reachable slice, minified.
next dev and next build are the same bundler in two moods.dev: bundle only what the current page needs, keep it in memory, don't minify, and when a file changes, swap just that module in the running page. That swap is hot module replacement.
build: bundle everything, minify, split into chunks, hash the filenames for caching, and write it all to .next/. Slow once, so that production is fast forever.
Edit the heading on the right. Watch the counter: it doesn't reset. Only the changed module was replaced; the page's state survived.
You'll meet all of these in config files, error messages and blog posts. They are interchangeable in the way that engines are: same job, different generation.
Next.js uses Turbopack for both dev and build. Vite is what you'd use for a React app without Next.js. You will never call any of these directly; they hide behind npm run dev.
| tool | written in | known for |
|---|---|---|
| webpack (2012) | JavaScript | The original. Powered Next.js for years. Slow but does everything. |
| esbuild (2020) | Go | 100× faster than webpack. Used inside Vite for dev. |
| SWC (2019) | Rust | The transformer (TS → JS, JSX → calls). Inside Next.js. |
| Vite (2020) | JS + esbuild + Rollup | Instant dev server. Default for non-Next React apps. |
| Turbopack (2022) | Rust | Vercel's webpack successor. Default for dev and build since Next 16. |
Everything so far has been getting code to the tab. From here up, it's about what the code does once it's there. That starts with React.