Notes9 min · 14 Jun 2026

What does Next.js do with my code?

npm run dev turned out to mean next dev, a server that lives inside the project. From there: what gets built ahead of time, who makes the HTML and when, and why only some of a page's code reaches the browser.

  1. 01Where it started
  2. 02Where the Next server is
  3. 03Words after the command
  4. 04Two times: build and request
  5. 05Fonts, and the @ in CSS
  6. 06Who makes the HTML, and when
  7. 07Server Components
  8. 08Hydration
  9. 09Code splitting
  10. 10Back to npm run dev

01Where it started

A small surprise the day before: npm run dev doesn't do anything itself. It runs the dev script in package.json, and that script is next dev. So there is a Next server running, and its round N sits in the corner of the page while you work. Which raised the real question: what is Next.js doing between the files in the project and the page in the browser? There is a .next folder, CSS gets bundled and served, and no clear picture of any of it.

This picks up from How does my terminal find a tool?, on how the shell finds a tool, and What is actually in Bun?, on what Node and Bun are.

02Where the Next server is

Node and Bun were installed for the whole machine. Next.js wasn't installed anywhere you'd look. It is in the project: npm install read package.json and put Next into node_modules/next, beside React and everything else the project lists.

npm run finds it with the trick from Typing a name. While a script runs, it puts the project's node_modules/.bin at the front of , so the word next in next dev is found there first. In this project, which uses pnpm, node_modules/.bin/next is a short shell script pnpm wrote, and its last line runs node on Next's own starting file. So every project carries its own Next, at its own version, and two projects never fight over one.

Then the layers stack. Your pages are code Next.js calls. Next.js is a , a JavaScript program that decides when your code runs: it routes requests, renders pages, bundles files. Next.js runs on Node, the from What is actually in Bun?: V8 to execute the JavaScript, and a harness in C++ for files, network and the HTTP server. Next doesn't rebuild Node's layer. It uses it, the way your pages use Next. Bun can mostly sit in Node's place, because it copies Node's harness.

Node runs Next, and Next runs your code. The dev server is a Node program that lives in the project's own folder.

Running locally, that is one Node process answering every request. Deployed on Vercel it isn't one server at all: pages made ahead of time are files on a CDN, and what runs per request runs as functions, as Where it runs describes.

03Words after the command

The fix for a stuck dev server, rm -rf .next, raised a side question: how does -rf change what rm does? The doesn't know. It splits the line into words and hands the whole list to the program as its . rm receives rm, -rf and .next, and its own code reads them: an r means go into folders, an f means don't ask and don't complain if it isn't there.

Fig 01
%rm -rf .next
What the program receives
  1. [0]
    "rm"Which program. Found through PATH, in /bin
  2. [1]
    "-rf"Two flags bunched: r, go into folders; f, don't ask
  3. [2]
    ".next"What to remove
Pick a command. The shell only splits it into words; what each word means is up to the program.

The rest is convention, kept by most tools. Single letters after one dash can be bunched, so -rf is -r -f. Most have a long form for scripts, --recursive. Quotes keep words together, which is how a commit message with spaces arrives as one argument. man rm or rm --help lists what a tool accepts.

rm has no bin to fish things back out of, so it is worth reading the path twice. The famous rm -rf / is refused by rm on a Mac, and the system it would wipe is sealed anyway (The folders); your home folder has no such protection.

04Two times: build and request

Next.js works at two times. At build time, before anyone visits, it turns the project into what a browser and a server can use. At request time, when a browser asks for a page, it answers with HTML and the files that page needs.

The build is mostly a 's work, here Turbopack. TypeScript becomes JavaScript. Hundreds of source files become a few . The CSS runs through Tailwind into a couple of files. Every file gets a name made from its contents, so a browser can keep a copy for ever and still notice when it changes. It all goes into .next, which is output, never edited, and safe to delete. This whole site builds into 15 JavaScript and CSS files in .next/static/chunks, its fonts in .next/static/media, and an HTML file for every page that could be made ahead, in .next/server/app.

next dev doesn't build everything first. It compiles a route the first time you open it, which is why the first visit to a page is slow and the second isn't. While you work, swaps the changed code into the open page without a reload.

When an edit doesn't show. Usually one of two things. Tailwind only makes CSS for class names it can find written out whole in the source. Build one from pieces and it never sees the finished name, so the rule is never generated. Or the cache in .next has fallen out of step: stop the server, delete .next, start again, and everything compiles fresh.

Fig 02
Source<h1 className={big ? "text-3xl" : "text-xl"}>
Tailwind finds
text-3xltext-xl
CSS it makes
.text-3xl { font-size: var(--text-3xl); … }.text-xl { font-size: var(--text-xl); … }

The heading changes size. Both rules were made.

Tailwind reads the source as text. A class name it can't find whole gets no CSS.

05Fonts, and the @ in CSS

A font on the web is a file, usually .woff2. Text in a font the visitor doesn't have waits for that file, and CSS has to say where to get it.

That is what @font-face does. A rule starting with @ is an : not a style for an element, but an instruction to the browser's CSS engine. @font-face registers a font: when anything asks for this family, here is the file. @media applies what's inside only when a condition holds, such as a narrow screen. @import fetches more CSS, @keyframes names an animation. An engine skips at-rules it doesn't know, which is how new ones arrive without breaking old browsers.

The old way to use Google Fonts was a <link> to Google: the browser fetched a stylesheet from Google, and that stylesheet sent it back to Google for the font file, while the text showed in a fallback font and then jumped. next/font moves the work to build time. It takes the font files, from Google or from the project, serves them from your own site, writes the @font-face rules itself and sizes the fallback font to take up the same room, so the swap barely moves anything. The visitor's browser never talks to Google.

This site does it with its own files. Switzer is four .woff2 files loaded with next/font/local; Geist Mono comes from the geist package, which uses the same thing. Both end up in .next/static/media with hashed names. Each sets a CSS variable, --font-switzer and --font-geist-mono, on the <html> element, and everything inherits from there.

@theme is a different kind of at-rule: the browser never sees it. It is Tailwind 4's, read at build time. The variables inside it are the design tokens Tailwind makes its classes from, so --color-ink there is why text-ink exists. Here it also says --font-sans: var(--font-switzer), ….

@theme inline is for exactly that kind of line, a token that points at another variable. A CSS variable that refers to another is worked out where it is defined, not where it is used. Without inline, font-sans would ask for --font-sans, worked out on :root, where --font-switzer might not exist yet. With inline, Tailwind writes var(--font-switzer) straight into the class, so it is worked out on the element using it. This site gets away without it because both variables happen to live on <html>.

06Who makes the HTML, and when

Rendering is turning components into HTML. <h1>Hello {user.name}</h1> is not HTML until something runs it. SSR, SSG and CSR are three answers to who runs it and when, and that's all the letters mean.

  • CSR, client-side rendering: the browser does it, on every visit. The server sends a nearly empty page and the JavaScript, and the content appears when that has run.
  • SSR, server-side rendering: the server does it, on every visit. The page arrives with its content, made fresh for that request.
  • SSG, static site generation: the build does it, once. Every visitor gets the same finished file, as fast as a file can be sent, and nothing new appears until the next build.
Fig 03
  1. Build
    Bundles the JavaScript
    Runs the components, makes the HTML
  2. Server
    Sends the finished file, from a CDN
  3. Browser
    Shows the HTML at once
    Hydrates the interactive parts
First thing on screen
The whole page, made before anyone asked
Data is fresh
Only at the next build
On this site
Every note and the home page
Pick one and follow a page from the build to the screen. Ink is where the HTML is made.

In the App Router, Next.js decides per page. A page is made at build time unless it needs something only a request has, such as cookies, headers or the query in the address, and then it is made per request. This site's own build says which it got: /notes is ○ static, each note is ● made ahead from the list generateStaticParams gives, and the 404 under /dive is ƒ dynamic because it can't know the address in advance. getServerSideProps, from the chat's example, belongs to the older Pages Router.

07Server Components

Server rendering had a catch. The page arrived made, but then the JavaScript for every component on it followed, even a footer that would never do anything, so React in the browser could take the page over. Fast HTML, and still the whole download.

React Server Components split the components in two. A , the default in the App Router, runs only on the server or at build. Its code never reaches the browser, so it can read a database or a file directly, and it can't hold state or answer a click. A starts with "use client". It is still rendered on the server for the first HTML, then its code is sent so it can come alive in the browser.

"use client" draws a line, not a label. Everything that file imports and renders is on the browser's side of it too. Where the line sits decides how much code travels.

Fig 04
  • page.tsxReads the address, picks the noteHTML only
  • "use client"
    NoteViewLays out the note, opens termsCode ships
  • TextTurns the words' markup into linksCode ships
  • FigureOne of these figuresCode ships
  • TogglesThe buttons you just pressedCode ships

How this site is built. The page stays on the server; the note view and all under it ship.

Move the line. Ink ships JavaScript to the browser; outlined parts arrive as HTML only.

Server Components don't send nothing, though. Their rendered result goes to the browser as the , with a placeholder where each client component goes and the files it needs. For this note it is .next/server/app/notes/nextjs.rsc, about 8 KB beside 72 KB of HTML, listing chunk files by name. React uses it to fit the two halves together, and Next.js fetches it alone when you move between pages.

SSR is about when the HTML is made. Server Components are about how much code crosses to the browser.

08Hydration

Server-made HTML arrives looking finished and does nothing. Buttons don't press, nothing has state. is React taking it over: once the JavaScript has loaded, it runs the client components, matches what they produce against the HTML already on screen, and attaches the event handlers to the elements that are there. It doesn't draw the page again.

Fig 05
Nothing yet
An empty tab

Load it, then press Like before the bar fills.

Load the page and press the button straight away. The JavaScript is slowed down so the gap can be seen.

Matching is the rule that bites. What the browser renders on its first pass has to be what the server rendered. Anything that differs between the two (the time, Math.random(), window, which doesn't exist on a server) breaks the match, and React reports a hydration error. The way out is useEffect, which only ever runs in the browser and only after hydration, so the first pass can agree and the browser-only part arrives a moment later.

Server Components skip all of this. They sent no code, so there is nothing to hydrate, and React only walks the client parts.

09Code splitting

The last piece is already in the folder names. The bundler doesn't make one file of the whole site: it splits the code into chunks, and a page asks only for the ones it uses. A route's own code is its own chunk. Code shared by many pages, React among it, is a chunk that every page reuses. Because each name comes from the contents, the browser keeps the shared ones between pages and between visits, and fetches only what's new.

A component can be split off further with next/dynamic, so its code loads only when it is first shown. This site does it once: the dials panel is loaded with dynamic(), and only on the dev server, so a visitor never downloads it.

The older picture in the chat, with chunks called framework, main and pages/index, was the Pages Router with webpack. Here the 15 files have names that are only hashes, and the RSC payload is what says which page needs which.

10Back to npm run dev

So: npm run dev runs next dev, found in the project's node_modules/.bin. Node runs Next, and Next compiles a route when you open it. Server Components are rendered on the server into HTML and a payload. Client Components are rendered there too, then their chunks are sent and hydrated in the browser. next build does the same work ahead of time for every page that doesn't need a request, and leaves the rest for when one arrives.

Connected

  • Note · same thingSuggested
    How does my terminal find a tool?

    npm run finds next by putting node_modules/.bin at the front of PATH, the same search that note draws.

  • Note · same thingSuggested
    What is actually in Bun?

    Next is a framework on the runtime that note opens up: Node's V8 and C++ harness, or Bun's in its place.

  • Note · same thingSuggested
    What actually runs my code?

    Build time and request time are where that note's serverless functions and static files come from: SSG pages are the files on the CDN, SSR pages are the functions.

  • Chat · no note yet
    Turbopack

    The chat ended by offering it: why Next.js's new bundler is faster than webpack.

Terms

Checked

What the chat had wrong or only half right, against sources. The note above already says the corrected thing.

  1. The Next.js logo on the dev server is Next's own welcome page, served from node_modules.The round N in the corner is the dev indicator, shown only by next dev. The starter welcome page is a page.tsx in your own project, written by create-next-app.Next.js docs, devIndicators
  2. rm -rf / deletes your entire operating system.rm on a Mac refuses to remove /, and the system volume is read only. Pointed at your home folder, it would do real damage.macOS, man rm
  3. SSG puts ready-made HTML in .next/static.Prerendered pages go in .next/server/app, as an .html file and an .rsc payload each. .next/static holds the JavaScript, CSS and fonts.This site's own build, 23 October 2026
  4. A page uses getServerSideProps to be server-rendered.That is the Pages Router. In the App Router a page is made at build time unless it reads request data, such as cookies or headers.Next.js docs, Caching
  5. A 'use client' component runs in the browser only.It is rendered on the server for the first HTML as well, then hydrated in the browser. The chat says so itself a message later.Next.js docs, Server and Client Components
  6. Server Components send nothing to the browser.Their code isn't sent, but their rendered result is, as the RSC payload, with placeholders and file references for the client components.Next.js docs, Server and Client Components
  7. @theme inline keeps variables off :root, so Tailwind won't fight next/font over fonts.inline makes the class use the variable's value, such as var(--font-inter), because a variable that refers to another is resolved where it is defined. It isn't about a conflict.Tailwind CSS docs, Theme variables
  8. .next/static/chunks holds framework.js, main.js and a pages folder with a file per page.That was the Pages Router built with webpack. With the App Router and Turbopack the chunks are named only by hash; this site has 15.This site's own build, 23 October 2026
  9. When someone visits your site, a Node.js process in your project answers.Locally, yes. On Vercel, prerendered pages come from a CDN and the rest run as functions; no one process serves the site.Vercel docs, Functions