Comments
- Loading...
We often want to organize our code into separate files and make use of third-party packages. A bundler takes all of your code and its dependencies and produces optimized output files for the browser.
We also want to write TypeScript for type safety, use JSX for React components, and transform our CSS with PostCSS — all of which require some kind of build step.
Vite handles all of this. It provides a lightning-fast dev server with hot module replacement, and uses Rolldown — a Rust-based bundler — under the hood for both development and production builds. Unlike older tools like Webpack, Vite requires almost no configuration to get started.
Let's create a project from scratch:
mkdir my-app && cd my-appnpm init -ynpm i -D vite@npm:rolldown-vite typescript
Vite uses index.html as its entry point — unlike Webpack which started from a JavaScript file. Create one in the project root:
<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>My App</title></head><body><div id="root"></div><script type="module" src="/src/main.ts"></script></body></html>
And a simple TypeScript file:
document.querySelector('#root')!.innerHTML = '<h1>Hello world!</h1>'
Now start the dev server:
npx vite
Open http://localhost:5173 and you should see "Hello world!" — Vite handles TypeScript out of the box, no configuration needed.
Try editing src/main.ts and saving — the page updates instantly. Vite's dev server uses native ES modules and only transforms files on demand, which is why it starts up so fast regardless of project size.
npm i react react-domnpm i -D @vitejs/plugin-react @types/react @types/react-dom
Create a Vite config to enable the React plugin:
import { defineConfig } from 'vite'import react from '@vitejs/plugin-react'export default defineConfig({plugins: [react()],})
Now replace src/main.ts with a React entry point, and update the script tag in index.html to match:
import { createRoot } from 'react-dom/client'import { App } from './App'createRoot(document.querySelector('#root')!).render(<App />)
<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>My App</title></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>
A small React example:
import { useState } from 'react'export function App() {const [name, setName] = useState('world')return (<><h3>Hello {name}!</h3><input value={name} onChange={(e) => setName(e.currentTarget.value)} /></>)}
That's it — no Babel presets, no loader configuration, no resolve extensions. Vite handles TypeScript, JSX, and hot module replacement automatically.
Vite supports importing CSS files directly from your TypeScript:
import { useState } from 'react'import './App.css'export function App() {const [name, setName] = useState('world')return (<><h3>Hello {name}!</h3><input value={name} onChange={(e) => setName(e.currentTarget.value)} /></>)}
h3 {color: steelblue;}input {padding: 0.5rem;border: 1px solid #ccc;border-radius: 4px;}
For component-scoped styles, Vite supports CSS modules out of the box — any file ending in .module.css is treated as a CSS module:
.title {color: steelblue;}
import { useState } from 'react'import styles from './App.module.css'export function App() {const [name, setName] = useState('world')return (<><h3 className={styles.title}>Hello {name}!</h3><input value={name} onChange={(e) => setName(e.currentTarget.value)} /></>)}
Just like Babel once did transformations over JavaScript, PostCSS does for CSS. PostCSS alone doesn't actually do anything — it only provides a way to parse and transform CSS via plugins.
Vite has built-in support for PostCSS. Just add a config file and Vite applies it automatically:
npm i -D postcss-preset-env cssnano
module.exports = {plugins: {'postcss-preset-env': { stage: 0 },cssnano: process.env.NODE_ENV === 'production' ? {} : false,},}
postcss-preset-env lets you use future CSS features today by automatically adding vendor prefixes and polyfills based on your browserslist definition. cssnano minifies your CSS in production.
No loader configuration needed — Vite picks up postcss.config.js automatically.
npx vite build
This generates optimized output in the dist/ folder. Vite automatically:
assets/index-a1b2c3d4.js)You can preview the production build locally with:
npx vite preview
Vite transpiles TypeScript using Oxc (via Rolldown), which is extremely fast but does not perform type checking. For that, we'll use tsgo, the native Go port of the TypeScript compiler:
npm i -D @typescript/native-preview
tsgo is roughly 10x faster than tsc — the VSCode codebase (1.5 million lines) compiles in under 9 seconds instead of 89. It's a drop-in replacement for type checking:
{"compilerOptions": {"target": "ES2020","module": "ESNext","moduleResolution": "bundler","jsx": "react-jsx","strict": true,"noEmit": true,"skipLibCheck": true,"isolatedModules": true,"resolveJsonModule": true,"allowImportingTsExtensions": true},"include": ["src"]}
npx tsgo -p .
This will report any type errors without emitting files — Vite handles the actual compilation.
The oxc project provides fast, Rust-based replacements for ESLint and Prettier: oxlint for linting and oxfmt for formatting.
npm i -D oxlint
oxlint works out of the box with zero configuration — it has sensible defaults and understands TypeScript and JSX natively:
npx oxlint src
It's orders of magnitude faster than ESLint and catches many of the same issues. You can configure it with an oxlintrc.json if you need to adjust rules:
{"rules": {"no-unused-vars": "warn"}}
For formatting, oxfmt is a drop-in replacement for Prettier:
npx oxfmt src --write
You can also bring the power of linting to your CSS using stylelint:
npm i -D stylelint stylelint-config-standard
module.exports = {extends: 'stylelint-config-standard',}
We can save our different commands in package.json for easy re-use:
"scripts": {"dev": "vite","build": "tsgo -p . && vite build","preview": "vite preview","typecheck": "tsgo -p .","lint": "oxlint src","fmt": "oxfmt src --write","lint:css": "stylelint 'src/**/*.css'"}
During development you just need npm run dev. For CI or before deploying, npm run build will type-check first and then bundle.