Dev.to WebDev 🛠 Dev 👁 0 📖 12 min read

# 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. Of

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

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 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

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.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

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"],
});
📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.