Vite is a modern frontend build tool. In the React ecosystem, it is commonly used to create fast React + TypeScript apps with a lightweight dev server, instant module updates, and production builds powered by Rollup.
Official source anchors to read later:
- https://vite.dev/
- https://vite.dev/guide/
- https://vite.dev/guide/features.html
- https://vite.dev/config/
- https://github.com/originjs/vite-plugin-federation
Mental Model
Think of Vite as two connected tools:
- A development server that serves source files as native browser ES modules.
- A production build pipeline that bundles, optimizes, and outputs static assets.
During development, Vite does not bundle your whole app before the browser opens. It lets the browser request modules directly, transforms files only when they are requested, and updates changed modules through HMR.
During production builds, Vite uses Rollup to create optimized bundles. This is where tree shaking, chunk splitting, CSS extraction, asset hashing, and minification happen.
The practical result is:
- Development startup is usually very fast.
- File changes update quickly.
- Production output is still bundled and optimized.
- React + TypeScript setup needs less manual configuration than older webpack-style setups.
Internal Architecture: How It Works
Vite has different behavior in development and production.
Development Flow
When you run npm run dev, Vite starts a local server.
The flow is:
- The browser loads
index.html. -
index.htmlpoints directly to/src/main.tsx. - The browser requests source modules as native ESM imports.
- Vite transforms TypeScript, JSX, CSS, and assets on demand.
- Dependencies from
node_modulesare pre-bundled with esbuild. - HMR updates only the changed modules instead of refreshing the whole page.
This is different from older bundlers that often build one large development bundle before the app starts.
Dependency Pre-Bundling
Vite pre-bundles dependencies with esbuild for two main reasons:
- Convert CommonJS or mixed-format packages into ESM that the browser can import.
- Reduce many tiny dependency files into fewer browser requests.
Your source code is transformed on demand. Your dependencies are optimized up front and cached.
HMR
HMR means Hot Module Replacement.
In a React app, when a component file changes, Vite and React Fast Refresh try to replace that module while preserving component state. If a change cannot be safely hot-reloaded, Vite falls back to a full page reload.
Production Build Flow
When you run npm run build, Vite uses Rollup.
The production flow is:
- Vite reads
index.htmlas the build entry. - It follows imports from
src/main.tsxand the rest of the app. - TypeScript and JSX are transformed.
- CSS and assets are processed.
- Rollup tree-shakes unused code.
- Output files are written to
dist/with hashed filenames.
Use npm run preview to serve the built dist/ output locally. preview is not the same as dev; it tests the production build output.
Setup
Create a React + TypeScript app:
npm create vite@latest my-vite-app -- --template react-ts
cd my-vite-app
npm install
npm run dev
Common scripts in package.json:
{
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
}
}
Common project structure:
my-vite-app/
index.html
package.json
tsconfig.json
tsconfig.app.json
tsconfig.node.json
vite.config.ts
src/
main.tsx
App.tsx
index.css
assets/
The important difference from many older React setups is that index.html is part of the source graph. It usually contains this script:
<script type="module" src="/src/main.tsx"></script>
React + TypeScript Basics
src/main.tsx mounts your React app:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import { App } from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);
src/App.tsx is a normal React component:
type Feature = {
title: string;
description: string;
};
const features: Feature[] = [
{
title: "Fast dev server",
description: "Vite serves source files as native browser modules.",
},
{
title: "Production bundling",
description: "Vite uses Rollup to create optimized production assets.",
},
];
export function App() {
return (
<main>
<h1>Vite React TypeScript</h1>
{features.map((feature) => (
<article key={feature.title}>
<h2>{feature.title}</h2>
<p>{feature.description}</p>
</article>
))}
</main>
);
}
Vite Config
The main config file is vite.config.ts.
Basic React config:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
});
Add a dev server port:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
strictPort: true,
},
});
Add a path alias:
import path from "node:path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});
Also add the alias to tsconfig.app.json so TypeScript understands it:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
Now imports can use the alias:
import { ProductCard } from "@/components/ProductCard";
Environment Variables
Vite exposes environment variables through import.meta.env.
Only variables prefixed with VITE_ are exposed to client-side code.
.env:
VITE_API_BASE_URL=https://api.example.com
Use it in TypeScript:
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
Common built-in values:
import.meta.env.MODE;
import.meta.env.DEV;
import.meta.env.PROD;
import.meta.env.BASE_URL;
Do not put secrets in VITE_ variables. They become part of the browser bundle.
CSS And Assets
Import CSS directly from TypeScript or TSX:
import "./App.css";
Use CSS modules for locally scoped class names:
import styles from "./ProductCard.module.css";
export function ProductCard() {
return <article className={styles.card}>Product</article>;
}
Import assets from src:
import logoUrl from "./assets/logo.svg";
export function Header() {
return <img src={logoUrl} alt="Logo" />;
}
Use public/ for files that should be copied as-is and referenced from the root path:
export function Header() {
return <img src="/logo.svg" alt="Logo" />;
}
Important Vite Concepts
index.html Is An Entry File
Vite treats index.html as part of the app. This makes script entry, title, root element, and static tags easy to inspect.
Dev And Build Are Different Pipelines
Development uses native ESM and on-demand transforms. Production uses Rollup bundling. If something works in dev but not in build, test with:
npm run build
npm run preview
TypeScript Checking Is Separate
Vite transpiles TypeScript quickly, but type checking usually happens through tsc.
That is why the default React TypeScript build script often includes:
tsc -b && vite build
Plugins Extend The Pipeline
Vite plugins can transform files, add framework support, change build behavior, or integrate tools.
React support comes from:
import react from "@vitejs/plugin-react";
Microfrontends With Vite
Microfrontend architecture splits a frontend into independently built and deployed pieces.
The common terms are:
- Host: the app users open directly.
- Remote: an app that exposes components, pages, or utilities.
- Exposed module: a module made available by a remote.
- Remote entry: the generated JavaScript file that tells the host how to load exposed modules.
- Shared dependency: a package such as React that host and remote should avoid duplicating.
Vite does not include module federation in core. A common way to learn this architecture with Vite is @originjs/vite-plugin-federation.
The example below creates two apps:
-
mf-remote: exposes aProductCardcomponent. -
mf-host: loads and renders that remote component.
Final Folder Layout
vite-mf-demo/
mf-remote/
src/
components/
ProductCard.tsx
App.tsx
main.tsx
vite.config.ts
package.json
mf-host/
src/
remotes.d.ts
App.tsx
main.tsx
vite.config.ts
package.json
Create The Apps
mkdir vite-mf-demo
cd vite-mf-demo
npm create vite@latest mf-remote -- --template react-ts
npm create vite@latest mf-host -- --template react-ts
Install dependencies in the remote:
cd mf-remote
npm install
npm install @originjs/vite-plugin-federation --save-dev
cd ..
Install dependencies in the host:
cd mf-host
npm install
npm install @originjs/vite-plugin-federation --save-dev
cd ..
Remote App
The remote app owns the component it exposes.
Create mf-remote/src/components/ProductCard.tsx:
export type ProductCardProps = {
name: string;
price: number;
badge?: string;
};
export function ProductCard({ name, price, badge }: ProductCardProps) {
return (
<article
style={{
border: "1px solid #d0d7de",
borderRadius: 8,
padding: 16,
maxWidth: 320,
fontFamily: "sans-serif",
}}
>
{badge ? <strong>{badge}</strong> : null}
<h2>{name}</h2>
<p>${price.toFixed(2)}</p>
<button type="button">Add to cart</button>
</article>
);
}
Use it locally in mf-remote/src/App.tsx so the remote can be developed by itself:
import { ProductCard } from "./components/ProductCard";
export function App() {
return (
<main style={{ padding: 32 }}>
<h1>Remote App</h1>
<ProductCard name="Wireless Keyboard" price={89} badge="Remote" />
</main>
);
}
Make sure mf-remote/src/main.tsx imports the named App export:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import "./index.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);
Configure federation in mf-remote/vite.config.ts:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import federation from "@originjs/vite-plugin-federation";
export default defineConfig({
plugins: [
react(),
federation({
name: "remoteApp",
filename: "remoteEntry.js",
exposes: {
"./ProductCard": "./src/components/ProductCard.tsx",
},
shared: ["react", "react-dom"],
}),
],
server: {
port: 5001,
strictPort: true,
},
preview: {
port: 5001,
strictPort: true,
},
build: {
modulePreload: false,
target: "esnext",
minify: false,
cssCodeSplit: false,
},
});
The remote will produce this file after build:
http://localhost:5001/assets/remoteEntry.js
That is the file the host needs.
Host App
The host app loads the remote module at runtime.
Configure federation in mf-host/vite.config.ts:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import federation from "@originjs/vite-plugin-federation";
export default defineConfig({
plugins: [
react(),
federation({
name: "hostApp",
remotes: {
remoteApp: "http://localhost:5001/assets/remoteEntry.js",
},
shared: ["react", "react-dom"],
}),
],
server: {
port: 5000,
strictPort: true,
},
preview: {
port: 5000,
strictPort: true,
},
build: {
modulePreload: false,
target: "esnext",
minify: false,
cssCodeSplit: false,
},
});
Create mf-host/src/remotes.d.ts:
declare module "remoteApp/ProductCard" {
export type ProductCardProps = {
name: string;
price: number;
badge?: string;
};
export function ProductCard(props: ProductCardProps): JSX.Element;
}
TypeScript needs this declaration because remoteApp/ProductCard does not exist on disk in the host project. It is resolved by the federation runtime in the browser.
Use the remote component in mf-host/src/App.tsx:
import { lazy, Suspense } from "react";
const RemoteProductCard = lazy(async () => {
const module = await import("remoteApp/ProductCard");
return {
default: module.ProductCard,
};
});
export function App() {
return (
<main style={{ padding: 32, fontFamily: "sans-serif" }}>
<h1>Host App</h1>
<p>This component is loaded from the remote app at runtime.</p>
<Suspense fallback={<p>Loading remote product card...</p>}>
<RemoteProductCard
name="Mechanical Keyboard"
price={129}
badge="Loaded from remote"
/>
</Suspense>
</main>
);
}
Make sure mf-host/src/main.tsx imports the named App export:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import "./index.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);
Run The Microfrontend Example
Build and preview the remote first:
cd mf-remote
npm run build
npm run preview
In another terminal, run the host:
cd mf-host
npm run dev
Open the host app:
http://localhost:5000
You should see the host page render ProductCard from the remote app.
For a production-like local test, build and preview both apps:
cd mf-remote
npm run build
npm run preview
Then in another terminal:
cd mf-host
npm run build
npm run preview
Open:
http://localhost:5000
Why The Remote Is Built Before Preview
The host references this remote entry:
http://localhost:5001/assets/remoteEntry.js
That file is generated during the remote build. Running vite preview serves the built dist/ directory, including assets/remoteEntry.js.
This makes the example close to how deployment works: the remote is built, deployed somewhere, and the host loads the deployed remote entry URL.
Production Deployment Model
In production, each app can be deployed separately.
Example:
https://products.example.com/assets/remoteEntry.js
https://shop.example.com/
The host config changes from localhost to the deployed URL:
federation({
name: "hostApp",
remotes: {
remoteApp: "https://products.example.com/assets/remoteEntry.js",
},
shared: ["react", "react-dom"],
});
The deployment rule is simple: the remote entry URL must be reachable by the browser before the host tries to load it.
Microfrontend Troubleshooting
The Host Shows A Failed Dynamic Import Error
Check that the remote is running and the URL is correct:
http://localhost:5001/assets/remoteEntry.js
Open that URL directly in the browser. If it returns 404, rebuild and preview the remote.
The Host Port Changed Automatically
Use strictPort: true in both apps. Without it, Vite may choose another port if the configured one is busy.
TypeScript Cannot Find The Remote Module
Add a declaration file like src/remotes.d.ts in the host.
The import is resolved at runtime by module federation, not by TypeScript's normal file lookup.
React Hooks Break Or State Behaves Strangely
Make sure host and remote use compatible React versions and share React through the federation config:
shared: ["react", "react-dom"];
It Works In Dev But Fails After Build
Run the production-like test:
npm run build
npm run preview
Do this for both host and remote. Module federation issues often appear only when the final output path or remote entry URL changes.
Important APIs And Config Options
defineConfig
Wraps Vite config and gives better TypeScript support.
export default defineConfig({
plugins: [react()],
});
plugins
Adds framework support and pipeline extensions.
plugins: [react()];
server
Controls the development server.
server: {
port: 5173,
strictPort: true,
}
preview
Controls the server used by vite preview.
preview: {
port: 4173,
strictPort: true,
}
resolve.alias
Creates import aliases.
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
}
build
Controls production output.
build: {
outDir: "dist",
sourcemap: true,
}
Vite Compared With Create React App
Create React App was a common default for React projects for years, but Vite is now a more common choice for new React apps.
| Topic | Vite | Create React App |
|---|---|---|
| Dev server | Native ESM and on-demand transforms | webpack dev bundle |
| Build | Rollup-based production build | webpack build |
| Config | Direct vite.config.ts
|
Hidden config unless ejected or overridden |
| Speed | Usually faster startup and updates | Often slower on larger apps |
| Ecosystem | Modern plugin ecosystem | Legacy React app setup |
When To Use Vite
Choose Vite when:
- You are creating a new React + TypeScript app.
- You want fast local development.
- You want simple build configuration.
- You need a plugin-based frontend toolchain.
- You want to learn modern React tooling.
Be more careful when:
- Your app depends on Node-only APIs in browser code.
- You are migrating a very custom webpack setup.
- You need a framework with server rendering, routing, and backend conventions built in. In that case, consider Next.js, Remix, or another full-stack framework.
Common Mistakes
Using Secret Values In VITE_ Variables
Anything exposed through VITE_ is visible in browser code.
Use backend environment variables for secrets.
Forgetting To Run npm run build
The dev server can hide production-only issues. Always run:
npm run build
Expecting TypeScript Errors To Block Dev Transforms
Vite transforms TypeScript quickly, but full type checking is usually handled separately by tsc.
Importing Server-Only Packages Into Client Code
Browser code cannot use Node APIs such as fs, path, or server-only SDKs unless they are properly isolated.
Mismatching Host And Remote URLs
In microfrontends, the host's remote URL must match the actual served remoteEntry.js location.
Architecture Tips
- Keep
vite.config.tssmall and intentional. - Use aliases for stable import paths, but do not overuse them.
- Keep environment-specific values in
.envfiles. - Run
buildandpreviewbefore deploying. - In microfrontends, define clear ownership boundaries between host and remote apps.
- Share React between host and remotes.
- Version remote component props carefully because the host depends on that contract.
- Prefer exposing focused components or route modules instead of exposing an entire application too early.
Learning Path
- Create a plain React + TypeScript Vite app.
- Understand how
index.html,src/main.tsx, andvite.config.tsconnect. - Learn
dev,build, andpreview. - Add environment variables and a path alias.
- Build a few components and import CSS/assets.
- Create the host and remote microfrontend example.
- Replace the localhost remote URL with a deployed remote URL.
- Study Rollup options only when you need custom production output.
Quick Reference
Create an app:
npm create vite@latest my-app -- --template react-ts
Start development:
npm run dev
Build production assets:
npm run build
Preview production assets:
npm run preview
Install module federation plugin:
npm install @originjs/vite-plugin-federation --save-dev
Remote exposes a module:
federation({
name: "remoteApp",
filename: "remoteEntry.js",
exposes: {
"./ProductCard": "./src/components/ProductCard.tsx",
},
shared: ["react", "react-dom"],
});
Host consumes a remote:
federation({
name: "hostApp",
remotes: {
remoteApp: "http://localhost:5001/assets/remoteEntry.js",
},
shared: ["react", "react-dom"],
});
Top comments (0)