DEV Community

Shashank Trivedi
Shashank Trivedi

Posted on

# Never ask again what Vite does

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:

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:

  1. The browser loads index.html.
  2. index.html points directly to /src/main.tsx.
  3. The browser requests source modules as native ESM imports.
  4. Vite transforms TypeScript, JSX, CSS, and assets on demand.
  5. Dependencies from node_modules are pre-bundled with esbuild.
  6. 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:

  1. Vite reads index.html as the build entry.
  2. It follows imports from src/main.tsx and the rest of the app.
  3. TypeScript and JSX are transformed.
  4. CSS and assets are processed.
  5. Rollup tree-shakes unused code.
  6. 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
Enter fullscreen mode Exit fullscreen mode

Common scripts in package.json:

{
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  }
}
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>,
);
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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()],
});
Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

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"),
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Also add the alias to tsconfig.app.json so TypeScript understands it:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now imports can use the alias:

import { ProductCard } from "@/components/ProductCard";
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Use it in TypeScript:

const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
Enter fullscreen mode Exit fullscreen mode

Common built-in values:

import.meta.env.MODE;
import.meta.env.DEV;
import.meta.env.PROD;
import.meta.env.BASE_URL;
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

Use CSS modules for locally scoped class names:

import styles from "./ProductCard.module.css";

export function ProductCard() {
  return <article className={styles.card}>Product</article>;
}
Enter fullscreen mode Exit fullscreen mode

Import assets from src:

import logoUrl from "./assets/logo.svg";

export function Header() {
  return <img src={logoUrl} alt="Logo" />;
}
Enter fullscreen mode Exit fullscreen mode

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" />;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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 a ProductCard component.
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Install dependencies in the remote:

cd mf-remote
npm install
npm install @originjs/vite-plugin-federation --save-dev
cd ..
Enter fullscreen mode Exit fullscreen mode

Install dependencies in the host:

cd mf-host
npm install
npm install @originjs/vite-plugin-federation --save-dev
cd ..
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>,
);
Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

The remote will produce this file after build:

http://localhost:5001/assets/remoteEntry.js
Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>,
);
Enter fullscreen mode Exit fullscreen mode

Run The Microfrontend Example

Build and preview the remote first:

cd mf-remote
npm run build
npm run preview
Enter fullscreen mode Exit fullscreen mode

In another terminal, run the host:

cd mf-host
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open the host app:

http://localhost:5000
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then in another terminal:

cd mf-host
npm run build
npm run preview
Enter fullscreen mode Exit fullscreen mode

Open:

http://localhost:5000
Enter fullscreen mode Exit fullscreen mode

Why The Remote Is Built Before Preview

The host references this remote entry:

http://localhost:5001/assets/remoteEntry.js
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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"],
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"];
Enter fullscreen mode Exit fullscreen mode

It Works In Dev But Fails After Build

Run the production-like test:

npm run build
npm run preview
Enter fullscreen mode Exit fullscreen mode

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()],
});
Enter fullscreen mode Exit fullscreen mode

plugins

Adds framework support and pipeline extensions.

plugins: [react()];
Enter fullscreen mode Exit fullscreen mode

server

Controls the development server.

server: {
  port: 5173,
  strictPort: true,
}
Enter fullscreen mode Exit fullscreen mode

preview

Controls the server used by vite preview.

preview: {
  port: 4173,
  strictPort: true,
}
Enter fullscreen mode Exit fullscreen mode

resolve.alias

Creates import aliases.

resolve: {
  alias: {
    "@": path.resolve(__dirname, "./src"),
  },
}
Enter fullscreen mode Exit fullscreen mode

build

Controls production output.

build: {
  outDir: "dist",
  sourcemap: true,
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.ts small and intentional.
  • Use aliases for stable import paths, but do not overuse them.
  • Keep environment-specific values in .env files.
  • Run build and preview before 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

  1. Create a plain React + TypeScript Vite app.
  2. Understand how index.html, src/main.tsx, and vite.config.ts connect.
  3. Learn dev, build, and preview.
  4. Add environment variables and a path alias.
  5. Build a few components and import CSS/assets.
  6. Create the host and remote microfrontend example.
  7. Replace the localhost remote URL with a deployed remote URL.
  8. Study Rollup options only when you need custom production output.

Quick Reference

Create an app:

npm create vite@latest my-app -- --template react-ts
Enter fullscreen mode Exit fullscreen mode

Start development:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Build production assets:

npm run build
Enter fullscreen mode Exit fullscreen mode

Preview production assets:

npm run preview
Enter fullscreen mode Exit fullscreen mode

Install module federation plugin:

npm install @originjs/vite-plugin-federation --save-dev
Enter fullscreen mode Exit fullscreen mode

Remote exposes a module:

federation({
  name: "remoteApp",
  filename: "remoteEntry.js",
  exposes: {
    "./ProductCard": "./src/components/ProductCard.tsx",
  },
  shared: ["react", "react-dom"],
});
Enter fullscreen mode Exit fullscreen mode

Host consumes a remote:

federation({
  name: "hostApp",
  remotes: {
    remoteApp: "http://localhost:5001/assets/remoteEntry.js",
  },
  shared: ["react", "react-dom"],
});
Enter fullscreen mode Exit fullscreen mode

Top comments (0)