← All posts

Chapter 1 - Vite and React

HIWUTH #1: Vite and React — What’s Actually Happening Under The Hood

This chapter covers two things that actually come up in interviews: why Vite feels instant while you’re coding, and why React needs a Virtual DOM at all. Everything else about these tools is just detail sitting on top of these two ideas, so once these click, a lot of other questions get easier.

Vite: it’s really two different tools

Here’s the thing most people miss: Vite does not bundle your code while you’re developing. It only bundles when you build for production. These are two separate jobs, done by two separate tools, and mixing them up is the easiest way to get this question wrong.

Why it skips bundling while you code. Older tools like Webpack need to look at your entire app (every file, every import) before they can serve you anything. That’s because they produce one combined file (a “bundle”) for the browser to load. So even if you change one line, Webpack has to think about the whole app again, and on a big project that can take 30+ seconds just to start.

Vite’s idea is simpler: modern browsers can already load JavaScript files directly, one at a time, using import statements, without needing them glued into one big file first. So instead of pre-building everything, Vite just waits. When your browser asks for a file, Vite quickly converts that one file (say, turning JSX into plain JavaScript) and sends it back. Nothing else gets touched.

Browser asks for main.jsx
   -> Vite converts just that file
   -> browser reads it, sees it imports App.jsx
   -> browser asks for App.jsx
   -> Vite converts just that file
   -> and so on, one file at a time

So startup time stays fast no matter how big your app is, because Vite never looks at the whole app up front, it only reacts to whatever the browser happens to ask for. The tool doing the actual file conversion is called esbuild. It’s written in a compiled language (Go), not JavaScript, and runs things in parallel, which is why it’s roughly 10-100 times faster than older JavaScript-based tools like Babel at this same job.

Interview: Why is Vite faster than Webpack during development?
Good answer: Vite doesn’t bundle your app while you’re coding. It serves your files as-is and only converts a file the moment the browser asks for it, using esbuild. So starting the dev server takes about the same time whether your app has 10 files or 10,000, because nothing gets scanned upfront. Webpack has to build the whole bundle before it can serve anything, so it slows down as your app grows.

Follow-up: Doesn’t that mean tons of separate network requests?
Yes, and that was a real concern early on. It’s mostly fine because modern browsers can handle many small requests efficiently over one connection (HTTP/2), and because Vite pre-processes your dependencies (see below) so they don’t add to that problem.

One thing Vite does bundle upfront: your dependencies. Your own code gets served file-by-file, but packages from node_modules get bundled once, right when the dev server starts. There are two reasons. First, a lot of npm packages are still written in an older format the browser can’t load directly, so Vite needs to convert them. Second, some packages are split across hundreds of small internal files: if Vite didn’t combine them, using one function from a library could trigger hundreds of network requests just for that one import. So Vite flattens each dependency into a single file and caches the result in a folder called node_modules/.vite. If you’ve ever seen weird stale behavior in a dev server and fixed it by deleting that folder and restarting — that’s this cache. Worth knowing it exists.

Building for production is a completely different process. When you run vite build, Vite stops doing any of the above and hands the job to a proper bundler called Rollup. Rollup looks at your whole app and produces optimized files: it removes code that’s never actually used (this is called tree-shaking), splits things into separate chunks so users don’t download code they don’t need yet, and minifies everything to make it smaller. The reason Vite uses a different tool here is simple: while you’re coding, speed of rebuilding matters most, because you rebuild constantly and none of it ships to users. When building for production, you only build once, but the result runs for every single user, so quality and file size matter more than build speed.

Interview: Why not just use esbuild everywhere, since it’s the fast one?
Good answer: Speed only matters while you’re actively developing, since you rebuild constantly. In production you build once, so what matters is how good the output is; smaller files, better code splitting. Rollup has historically been stronger at that than esbuild, so Vite uses each tool for the job it’s actually good at, rather than picking one tool for consistency.

How your changes show up instantly (Hot Module Replacement). When you save a file, Vite’s file watcher notices, re-converts just that file, and pushes it to your browser over a live connection (a WebSocket). For React apps specifically, a plugin implements something called Fast Refresh: instead of throwing away the whole component and starting fresh, React swaps in the new version of the component while keeping its existing state alive, so if you had a counter at 5 and tweaked some styling, it stays at 5 instead of resetting. This only works if the order in which your component calls hooks (like useState) stays the same between renders, which is exactly why React has a strict rule about never calling hooks conditionally.

React: why the Virtual DOM exists

Don’t stop at “React uses a Virtual DOM”… that’s not really an answer, interviewers expect you to already know that much. The real question is why it needs one.

The problem it’s solving. The DOM is the browser’s live representation of your page and is the actual tree of elements you see on screen. Changing it isn’t just slow because of the change itself; each change can force the browser to recalculate the layout of the page and repaint pixels on screen, which is expensive. If your code makes a bunch of small changes to the DOM directly, one after another, each one might trigger this expensive recalculation separately.

React’s fix. Instead of comparing against the real page directly, React keeps its own lightweight copy of what the page should look like, made of plain JavaScript objects and this is the Virtual DOM. Whenever something changes, React builds a new version of this copy, compares it to the previous version (this comparison step is called reconciliation) asynchronously, figures out exactly what’s different, and only then makes the actual changes to the real page; all in one batch, instead of many small ones.

something changes -> React builds a new virtual copy of the page
                   -> compares it to the old copy
                   -> figures out the minimum real changes needed
                   -> applies all of them to the real page at once

Interview: Why does React need a Virtual DOM?
Good answer: Because changing the real page directly is expensive and it can trigger layout recalculation and repainting. React keeps a cheap, in-memory copy of the page, compares old and new versions there, and only touches the real page once, with the minimum set of changes needed. It’s not that this makes the browser itself any faster, it just makes sure React does as few expensive operations as possible.

Follow-up: Is this always faster than just changing the DOM yourself?
No. If you know exactly which three things changed and update them directly, you skip the comparison step entirely and that will always be faster. React’s approach trades a small amount of overhead for code that’s much easier to write correctly at scale, which is also why performance-critical things like games or complex animations often skip React’s model and touch the page directly.

Fiber: how React makes this comparison without freezing your page. Older versions of React did this comparison in one single, uninterruptible pass and once it started, it couldn’t stop until it finished walking the whole page structure. On a big enough app, this could freeze the page for a moment, making typing or clicking feel laggy. The fix, called Fiber, breaks this comparison into small pieces that React works through one at a time, checking in between each piece whether something more urgent (like you pressing a key) needs attention first. If so, it can pause and come back to it later.

comparison step  -> broken into small pieces, CAN be paused
final page update -> happens all at once, CANNOT be paused
                     (a half-applied update would look broken)

This is also the foundation for some newer React features that let you mark certain updates as “not urgent,” so React can prioritize things like typing responsiveness over less important updates happening at the same time.

Keys: how React tells list items apart. When comparing lists of items (like rows in a table), React needs some way to know “this is the same item as before, just moved” versus “this is a totally new item.” That’s what the key prop is for. Without good keys, React just compares items by their position in the list — so if you add a new item to the front of a list, React thinks every single item changed, because item 1 used to be item 2, item 2 used to be item 3, and so on. This can cause weird bugs, like a text input losing focus while you’re typing after a list gets reordered.

Interview: Why is using the array index as a key considered a bad idea?
Good answer: An index describes a position, not the actual identity of an item. If the list gets reordered, filtered, or has items added or removed, the index attached to any given item changes, even though the item itself is the same. React ends up applying updates to the wrong item. It’s only safe when the list will never change order or length.

Quick Revision

  • Vite has two separate modes: while coding, it serves files as-is and converts them one at a time with esbuild and it’s fast no matter how big the app is. When building for production, it switches to Rollup, a real bundler that removes unused code, splits files, and shrinks everything.
  • Vite still bundles your dependencies (node_modules) upfront, because some packages need format conversion and some are split into hundreds of files that would otherwise mean hundreds of requests. Cached in node_modules/.vite.
  • Saving a file triggers Hot Module Replacement over a live connection. React’s Fast Refresh keeps your component’s existing state alive across an edit, but only if the order of hook calls hasn’t changed.
  • The Virtual DOM exists because changing the real page is expensive (layout + repaint). React compares a cheap in-memory copy first, then applies only the necessary changes to the real page, all at once.
  • Fiber lets React break that comparison step into small, pausable chunks instead of one long freeze, and this is what makes newer “prioritize urgent updates” features possible.
  • Keys tell React which list items are the same across a re-render. Using array index as a key breaks this whenever the list can reorder, grow, or shrink.