Vite#
Vite is a frontend build tool and local development server. It sits between your source code and the browser, transforming files that the browser cannot understand (TypeScript, JSX, CSS modules, image imports) into plain assets it can.
The Core Mental Model#
At the end of the day, a browser still just wants an HTML file with a <script>
tag pointing to some JS. That hasn’t changed since 1995. Vite’s entire job is:
Take your source code → do some transformations → output files the browser can load.
Everything else — hot reloading, TypeScript compilation, bundling, optimization — is in service of that one goal.
Two Modes: Dev vs Build#
Vite operates differently depending on what you need:
Development (``npm run dev``) starts a local HTTP server (typically
localhost:5173) backed by a running Node process. It does not bundle your code —
it serves files individually and uses native ES modules in the browser for speed.
Changes to source files trigger instant hot module replacement (HMR): only the changed
module updates in the browser without a full reload.
Production build (``npm run build``) bundles everything into an optimized
dist/ folder using Rollup under the hood. It:
Merges all your source files into one (or a few) JS files
Tree-shakes unused code from libraries
Minifies — removes whitespace, renames variables to single letters
Hashes filenames for cache busting
Handles code splitting for lazy loading
The dist/ output is what gets deployed to Netlify. node_modules/, your source
files, and all of Vite’s tooling never reach the user.
The Interception Layer#
Vite intercepts import statements and applies transformations before the browser
sees anything. This is what makes patterns like this possible:
import logo from './logo.png' // not valid JS — Vite handles it
import styles from './Button.module.css' // also not valid JS
import data from './config.json' // also not valid JS
These are instructions to Vite, not to the browser. Vite processes them during the build and replaces them with valid JS. The browser never sees the raw import — it receives the result.
This is a pattern you won’t find in Python or Java: some lines of code are consumed by the build tool and never execute. Coming from those ecosystems, this can feel like the rules are different — because they are.
Boundary Enforcement#
Vite knows which code is destined for the browser and enforces that boundary strictly.
If your app code (browser-bound) tries to import a Node-only module like fs:
import { readFileSync } from 'fs' // ❌ Node-only, not available in browser
Vite will fail the build with an error before producing any output. It refuses to ship code that would break at runtime. It does not silently delete the line — it stops and tells you to fix it.
Bundle Size#
Every library you add contributes its code to the final bundle. A heavy 3D library, a charting package, a full UI component library — bundles can easily reach multiple megabytes. On mobile data, that’s a real cost to users.
Vite addresses this through:
Tree shaking — if you import one function from a 500kb library, unused code is stripped from the bundle
Code splitting — the bundle is divided into chunks loaded on demand as the user navigates, not all upfront
Lazy loading — heavy components only load when the user actually needs them
For projects with heavy visualization libraries (Three.js, Plotly), code splitting so that visualizations only load on the relevant view is the right approach.
Note
node_modules/ size (developer disk) and bundle size (user download) are
different things. node_modules/ might be 300mb — the bundle Vite produces
from it might be 200kb.
Project Structure#
A standard Vite + React + TypeScript project looks like:
my-app/
├── dist/ ← build output, deployed to Netlify
├── node_modules/ ← installed packages, never committed
├── public/ ← static assets copied as-is to dist/
├── src/
│ ├── main.tsx ← entry point, mounts React
│ ├── App.tsx
│ └── components/
├── index.html ← shell, references the JS bundle via <script>
├── vite.config.ts
├── package.json
└── package-lock.json