Comprehensive Learning Guide for Vite (Version: v7.0.0)
This guide provides a comprehensive overview of Vite, focusing on its latest stable version, v7.0.0, and significant changes from previous versions (v4.x, v5.x, v6.x). It’s designed for software engineers with foundational programming knowledge and familiarity with modern web development concepts. We’ll explore new features, performance optimizations, best practices, and practical application through guided projects.
Chapter 1: Introduction to Vite 7.0
1.1: What is Vite?
Vite (pronounced /viːt/, like “veet” in French for “quick”) is a next-generation frontend tooling that offers a significantly faster and leaner development experience for modern web projects. It addresses the performance bottlenecks associated with traditional bundlers (like Webpack) by leveraging native ES Modules (ESM) in development and Rollup for optimized production builds.
1.2: Key Philosophy and Advantages
Vite’s core philosophy revolves around speed and simplicity. Its primary advantages include:
- Near-instant server start: By serving code as native ES modules, Vite avoids the lengthy bundling process during development, leading to incredibly fast startup times.
- Lightning-fast Hot Module Replacement (HMR): Changes to your code are reflected in the browser almost instantly, without a full page reload, significantly boosting developer productivity.
- Optimized production builds: While development uses native ESM, Vite employs Rollup for highly optimized production bundles, including features like tree-shaking, code-splitting, and minification.
- Framework Agnostic: Vite offers first-class support for popular frameworks like React, Vue, Svelte, and Lit, along with a rich plugin API for extensibility.
1.3: Evolution of Vite: From v4 to v7
Vite has continuously evolved, with each major version introducing significant improvements.
- Vite 4.x: Introduced Rollup 3 integration, improved dev server performance, and Node.js 18+ requirement. Focused on API cleanup and stability.
- Vite 5.x: Further enhanced build performance with Rollup 4. Deprecated the CJS Node API and reworked
defineandimport.meta.env.*replacement strategies for consistency. Introducedserver.warmupfor faster startup. - Vite 6.x: Introduced the experimental Environment API, allowing for more flexible runtime environments. This laid the groundwork for deeper integration with serverless functions and edge runtimes.
- Vite 7.0 (Latest Stable):
- Node.js Support: Requires Node.js 20.19+ or 22.12+. Node.js 18 has reached its End-of-Life (EOL) and is no longer supported. This transition to ESM-only distribution.
- Default Browser Target: Changed from
'modules'to'baseline-widely-available'. This ensures compatibility with features well-established across core browsers for at least 30 months, aligning with web platform standards. - Rolldown: A significant long-term initiative. Rolldown is a Rust-based next-generation bundler designed to eventually replace both Rollup and Esbuild in Vite’s core, promising substantial build and dev performance improvements. It’s currently available as an opt-in package (
rolldown-vite). - Vite DevTools: A collaborative effort between VoidZero and NuxtLabs to provide deeper debugging and analysis for Vite-based projects.
- Environment API: Continued as experimental in Vite 7, with a new
buildApphook for plugins to coordinate environment building. This enables advanced scenarios like Cloudflare’s Vite plugin for Workers. - Deprecations: Removal of legacy features like Sass legacy API support and
splitVendorChunkPlugin.
1.4: Getting Started with Vite 7.0
Scaffolding a new Vite project is straightforward. You can use the create vite command from your package manager:
# npm
npm create vite@latest my-vite-app -- --template react-ts
# yarn
yarn create vite my-vite-app --template react-ts
# pnpm
pnpm create vite my-vite-app --template react-ts
Replace react-ts with your preferred framework template (e.g., vue, vue-ts, react, preact, preact-ts, lit, lit-ts, svelte, svelte-ts, vanilla, vanilla-ts).
After scaffolding, navigate into your project directory and install dependencies:
cd my-vite-app
npm install # or yarn install or pnpm install
Then, start the development server:
npm run dev # or yarn dev or pnpm dev
Your application will typically be available at http://localhost:5173.
Chapter 2: Core Concepts and Architecture
Vite’s distinct architecture provides its speed and efficiency.
2.1: Native ES Modules in Development
What it is: During development, Vite serves your code as native ES Modules directly to the browser. This means that instead of bundling your entire application into a single JavaScript file, the browser requests individual modules as needed.
Why it was introduced/changed: Traditional bundlers (like Webpack) would bundle the entire application, leading to long startup times as the project grew. Native ESM allows the browser to perform most of the work, fetching only the modules required for the current view.
How it works: When the browser requests a module, Vite performs on-demand transformations (e.g., converting JSX/TypeScript) and serves it. This lazy loading of modules drastically reduces the initial server startup time and makes HMR incredibly fast.
Example:
Consider a simple React application structure:
src/
├── App.jsx
├── main.jsx
└── components/
└── Button.jsx
In a traditional bundler, main.jsx, App.jsx, and Button.jsx would be bundled into a single file before being served. With Vite, the browser requests main.jsx first, which then requests App.jsx, and so on.
2.2: Production Bundling with Rollup
What it is: For production builds, Vite utilizes Rollup, a highly optimized JavaScript bundler.
Why it was introduced/changed: While native ESM is great for development, shipping unbundled ESM in production can be inefficient due to numerous network requests (even with HTTP/2). Rollup provides superior optimizations for production deployment. Vite has continuously updated its Rollup integration, leveraging Rollup 3 in Vite 4 and Rollup 4 in Vite 5, bringing performance boosts and API refinements.
How it works:
When you run vite build, Rollup takes your source code, performs optimizations like tree-shaking (removing unused code), code-splitting (breaking the app into smaller chunks for on-demand loading), and minification (compressing code), resulting in a highly optimized and performant production bundle.
Example: Manual Code Splitting for Vendors
You can configure Rollup options within your vite.config.js to control how chunks are created. This is particularly useful for separating vendor (third-party) code from your application code, improving caching.
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc'; // Using SWC for faster React compilation
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: {
// Create a 'vendor' chunk for React and ReactDOM
vendor: ['react', 'react-dom'],
// Example: Create a separate chunk for a specific utility library
'my-utilities': ['lodash-es'],
},
},
},
},
});
Explanation:
This configuration tells Rollup to create a separate chunk named vendor.js containing react and react-dom. It also creates a my-utilities.js chunk for lodash-es. This can lead to better caching strategies for users, as vendor code changes less frequently than application code.
2.3: Hot Module Replacement (HMR)
What it is: HMR allows you to see changes to your code reflected in the browser without a full page reload, preserving the application’s state.
Why it was introduced/changed: HMR significantly improves the development feedback loop. In traditional setups, a full page reload for every change could be slow and disruptive. Vite’s native ESM approach inherently makes HMR faster.
How it works: Vite’s development server establishes a WebSocket connection with the browser. When a file changes, Vite sends a message to the browser, informing it which module has been updated. The browser then hot-replaces only that module (and its direct dependents) without reloading the entire page.
Example:
If you modify a React component’s styling:
// src/components/Button.jsx
import React from 'react';
import './Button.css'; // Assume this is changed
function Button({ children, onClick }) {
return (
<button className="my-button" onClick={onClick}>
{children}
</button>
);
}
export default Button;
/* src/components/Button.css */
.my-button {
background-color: blue; /* Changed from red */
color: white;
padding: 10px 20px;
border-radius: 5px;
}
Upon saving Button.css, Vite’s HMR mechanism will inject the new CSS rules directly into the page without a full refresh, and any React components using this CSS will update instantly.
Chapter 3: New Features and Major Changes in Vite 7.0
Vite 7.0 builds upon its predecessors, bringing critical updates for enhanced performance, compatibility, and developer experience.
3.1: Node.js Version Support
What it is: Vite 7.0 now requires Node.js versions 20.19+ or 22.12+. Support for Node.js 18 has been dropped as it reached its End-of-Life (EOL) in April 2025.
Why it was introduced/changed: This update aligns Vite with the latest Node.js releases, allowing it to leverage newer features, performance improvements, and security patches. Crucially, it enables Vite 7.0 to be distributed as ESM-only without preventing the Vite JavaScript API from being required by CJS modules, thanks to Node.js’s improved require(esm) support.
How it works: Simply ensure your development environment runs on a compatible Node.js version. If you are using an older version, you will need to upgrade to run Vite 7.0 projects.
Example: Checking and Updating Node.js Version
# Check your current Node.js version
node -v
# If you need to update, use a version manager like nvm (recommended)
# Install nvm (if not already installed)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# Install the latest Node.js LTS version (e.g., 22.x)
nvm install 22
# Use the newly installed version
nvm use 22
# Verify the update
node -v
3.2: Default Browser Target: Baseline Widely Available
What it is: In Vite 7.0, the default build.target configuration has changed from 'modules' to 'baseline-widely-available'. This targets a set of browser versions compatible with features widely available across browsers for at least 30 months (as per the “Baseline” initiative).
Why it was introduced/changed: This change provides more predictability for the default browser target across future releases. It ensures that the generated production bundles are optimized for a modern, well-supported baseline of web platform features without needing to explicitly define a list of browser versions.
How it works: By default, your production build will now target:
- Chrome 107+
- Edge 107+
- Firefox 104+
- Safari 16.0+
You can still customize build.target if you need to support older browsers (e.g., es2015) and use @vitejs/plugin-legacy for polyfills.
Example: Customizing build.target for broader compatibility
If your project requires supporting slightly older browsers than the baseline-widely-available default, you can specify a custom target.
// vite.config.js
import { defineConfig } from 'vite';
import legacy from '@vitejs/plugin-legacy';
export default defineConfig({
plugins: [
// Include plugin-legacy for older browser support if needed
legacy({
targets: ['defaults', 'not IE 11'], // Example: target default modern browsers excluding IE11
}),
],
build: {
// Override the default 'baseline-widely-available' to 'es2015'
// This will result in larger bundles, but broader compatibility
target: 'es2015',
},
});
Tip: Only use es2015 or similar if your target audience uses older browsers, as it can significantly increase bundle size. For most modern web applications, the baseline-widely-available default in Vite 7.0 is sufficient.
3.3: Introduction of Rolldown (Rust-based Bundler)
What it is: Rolldown is a new, experimental Rust-based bundler being developed by VoidZero. It’s intended to become the default bundler for Vite in the future, replacing both Rollup and Esbuild. You can currently try it out as a drop-in replacement via the rolldown-vite package.
Why it was introduced/changed: JavaScript-based tooling, even highly optimized ones like Esbuild and Rollup, can hit performance bottlenecks in large-scale projects. Rust, being a compile-to-native language, offers significant performance advantages, especially for CPU-intensive tasks like bundling. Rolldown aims to deliver even faster build times and improve consistency between development and production environments. Early reports indicate significant improvements (e.g., production builds dropping from ~27s to ~9-10s).
How it works (Currently Opt-in):
To use Rolldown, you install the rolldown-vite package and replace vite with it in your package.json scripts or directly in your project.
Example: Trying out Rolldown
Install
rolldown-vite:npm install rolldown-viteUpdate
package.jsonscripts:{ "name": "my-vite-app", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "rolldown-vite dev", "build": "rolldown-vite build", "preview": "rolldown-vite preview" }, "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "@vitejs/plugin-react-swc": "^3.5.0", "rolldown-vite": "^0.x.x", # Ensure you have the latest version installed "vite": "^7.0.0" } }
Note: rolldown-vite is a drop-in replacement, meaning it should work without changes to your existing vite.config.js. It is still under active development, so use with caution in production critical applications, though it is becoming increasingly stable.
3.4: Vite DevTools
What it is: A new project spearheaded by Anthony Fu in partnership with VoidZero and NuxtLabs, aimed at providing deeper and more insightful debugging and analysis capabilities for all Vite-based projects and frameworks.
Why it was introduced/changed: As Vite’s ecosystem grows and projects become more complex, advanced debugging and performance analysis tools become essential. Vite DevTools will offer a unified and powerful way to inspect, understand, and optimize Vite applications.
How it works: Specific usage and integration details are still emerging as DevTools are under active development. Keep an eye on official announcements and documentation for its release and adoption.
3.5: Environment API Enhancements
What it is: The experimental Environment API, introduced in Vite 6, continues to evolve in Vite 7.0. It provides new capabilities for building tools and workflows that closely mimic production environments, especially for server-side rendering (SSR) and edge computing. A new buildApp hook has been added to allow plugins to coordinate environment building more effectively.
Why it was introduced/changed: The web development landscape is shifting towards more distributed and diverse runtime environments (e.g., serverless functions, Edge Workers). The Environment API provides a standardized way for frameworks and tools to interact with Vite’s core to build and run applications in these varied environments consistently.
How it works: For most application developers, you might interact with the Environment API indirectly through framework integrations (e.g., a React framework leveraging it for SSR). Plugin and framework authors will use these APIs to build more robust integrations.
Example: Conceptual use of the buildApp hook (for plugin authors)
This is a simplified, conceptual example for understanding the buildApp hook’s purpose. Actual plugin development is more involved.
// vite.config.ts (simplified conceptual plugin)
import { defineConfig, Plugin } from 'vite';
function myEnvironmentPlugin(): Plugin {
return {
name: 'my-environment-plugin',
// The new buildApp hook
buildApp(app, options) {
console.log('Building application for environment:', app.name);
// Example: Coordinate specific build steps for this environment
// This could involve generating environment-specific assets,
// or tailoring the build output.
if (app.name === 'cloudflare-workers') {
// Perform Cloudflare Workers specific bundling logic
console.log('Applying Cloudflare Workers specific build optimizations...');
// Example: Override Rollup options for this specific app
options.rollupOptions.output.format = 'esm'; // Force ESM output
}
},
// Other Vite plugin hooks...
};
}
export default defineConfig({
plugins: [myEnvironmentPlugin()],
// ... other configurations
});
3.6: Deprecations and API Cleanups
What it is: Vite 7.0 removes several already deprecated features, including Sass legacy API support and the splitVendorChunkPlugin.
Why it was introduced/changed: Removing deprecated features helps to streamline the Vite codebase, reduce maintenance overhead, and encourage developers to adopt modern best practices. The splitVendorChunkPlugin was made redundant by Rollup’s improved default code splitting and manual chunking capabilities.
How it works: If your project used any of these deprecated features, you would need to update your configuration or code to use the modern alternatives before upgrading to Vite 7.0. For Sass, this means ensuring you’re using the modern Dart Sass API.
Migration Guide: Always refer to the official Vite 7 Migration Guide for a detailed list of all breaking changes and migration steps.
Chapter 4: Advanced Configuration and Customization
Vite’s vite.config.js (or vite.config.ts) file is your central hub for advanced customization.
4.1: vite.config.js / vite.config.ts Deep Dive
What it is: This file exports a configuration object (or a function that returns one) that Vite uses to tailor its behavior for development and production.
Why it was introduced/changed: It provides a flexible and programmatic way to configure every aspect of your Vite project, from plugins to build optimizations, without resorting to complex CLI arguments or conventions.
How it works:
The defineConfig helper from vite provides autocompletion for the configuration options.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'path';
export default defineConfig({
// Global base path for the deployed application
base: '/',
// Project root directory (where index.html is located)
root: process.cwd(),
// Directory to serve static assets from during development
publicDir: 'public',
// Plugins to use
plugins: [
react(), // React support with SWC for faster compilation
],
// Development server options
server: {
host: 'localhost', // specify host to bind to
port: 3000, // port number
open: true, // open browser on server start
proxy: {
// proxy API requests
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
cors: true, // enable CORS for development
// Warm up frequently used files to improve startup time (Vite 5+)
warmup: {
clientFiles: ['./src/App.jsx', './src/main.jsx'],
},
},
// Build options
build: {
outDir: 'dist', // output directory for production build
assetsDir: 'assets', // directory for assets inside outDir
sourcemap: true, // generate sourcemaps
minify: 'esbuild', // 'terser' or 'esbuild'
emptyOutDir: true, // empty the output directory on build
target: 'esnext', // browser target
rollupOptions: {
// Custom Rollup options for fine-grained control
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
// Resolve options (e.g., path aliases)
resolve: {
alias: {
'@': path.resolve(__dirname, './src'), // @ alias for src directory
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'], // extensions to resolve
},
// Environment variables
define: {
// Define global constants replaced during build
__APP_VERSION__: JSON.stringify('1.0.0'),
// Expose process.env variables (ensure they are properly stringified)
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},
// CSS options (e.g., preprocessors, CSS Modules)
css: {
preprocessorOptions: {
scss: {
// Example: global SCSS variables
additionalData: `@import "./src/styles/variables.scss";`,
},
},
modules: {
// Enable CSS Modules by default for .module.css files
generateScopedName: '[name]__[local]--[hash:base64:5]',
},
},
});
Precision & Context: The vite.config.ts file shown above demonstrates a comprehensive setup covering common scenarios. Each section within the defineConfig object directly maps to specific functionalities of Vite, allowing granular control over development, build, and asset handling processes.
4.2: Path Aliases
What it is: Path aliases allow you to create shorthand names for common directories, making import statements cleaner and more maintainable.
Why it was introduced/changed: As projects grow, relative paths (e.g., ../../../components/Button) can become long, error-prone, and difficult to refactor. Aliases simplify imports, improving readability and developer experience.
How it works:
You define aliases in the resolve.alias option within vite.config.js.
Example:
// vite.config.js
import { defineConfig } from 'vite';
import path from 'path'; // Node.js path module for resolving absolute paths
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'), // Alias '@' to the 'src' directory
'@components': path.resolve(__dirname, './src/components'),
'@utils': path.resolve(__dirname, './src/utils'),
},
},
});
Usage in Code:
// Before aliases
import Button from '../../components/Button';
import { formatDate } from '../../../utils/date';
// After aliases
import Button from '@components/Button';
import { formatDate } from '@utils/date';
Tip: For TypeScript projects, you also need to update tsconfig.json to inform the TypeScript compiler about these aliases:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
}
}
4.3: Environment Variables
What it is: Vite handles environment variables, making them accessible in your client-side code during development and build.
Why it was introduced/changed: Securely handling environment-specific configurations (e.g., API keys, build modes) is crucial. Vite provides a robust mechanism to inject these variables safely. It also ensures only VITE_ prefixed variables are exposed to the client to prevent accidental exposure of sensitive server-side variables.
How it works:
- Vite exposes environment variables on the
import.meta.envobject. - Only variables prefixed with
VITE_are exposed to your client-side code. - You can define environment variables in
.envfiles (e.g.,.env,.env.development,.env.production).
Example:
Create
.env.developmentand.env.productionfiles:# .env.development VITE_API_URL=http://localhost:5000/api VITE_APP_MODE=development# .env.production VITE_API_URL=https://api.myapp.com/api VITE_APP_MODE=productionAccess in your application code:
// src/App.jsx import React from 'react'; function App() { const apiUrl = import.meta.env.VITE_API_URL; const appMode = import.meta.env.VITE_APP_MODE; const isDevelopment = import.meta.env.DEV; // Vite provides this boolean const isProduction = import.meta.env.PROD; // Vite provides this boolean return ( <div> <h1>Vite Environment Variables</h1> <p>API URL: {apiUrl}</p> <p>App Mode: {appMode}</p> <p>Is Development: {isDevelopment.toString()}</p> <p>Is Production: {isProduction.toString()}</p> </div> ); } export default App;
Tip: Use import.meta.env.MODE to get the current mode string (development, production, or a custom mode like staging if specified via --mode staging).
4.4: Configuring the Dev Server
What it is: The server option in vite.config.js allows you to customize the behavior of the development server.
Why it was introduced/changed: Flexible server configuration is crucial for local development, enabling seamless integration with backend APIs, custom ports, and host settings.
How it works:
You define properties within the server object.
Example:
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 8080, // Run on port 8080
host: '0.0.0.0', // Listen on all network interfaces
open: '/dashboard', // Open '/dashboard' path in browser on startup
cors: {
origin: 'http://localhost:3001', // Allow requests from a specific origin
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true,
},
proxy: {
'/api': {
target: 'http://localhost:4000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
// Configure WebSockets proxy for '/socket'
ws: true,
},
'/uploads': 'http://my-cdn.com', // Simple proxy for static assets
},
// Enable HTTPS (requires certificate setup, e.g., with vite-plugin-mkcert)
https: true,
},
});
4.5: Optimizing Production Builds with Rollup Options
What it is: The build.rollupOptions in vite.config.js allows you to pass specific Rollup configuration options directly to the underlying bundler.
Why it was introduced/changed: While Vite provides sensible defaults, large or complex applications often benefit from fine-tuned control over the bundling process. This allows developers to leverage Rollup’s full power for advanced optimizations.
How it works:
You provide a Rollup options object within the build.rollupOptions.
Example: Advanced Code Splitting and Asset Management
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import { visualizer } from 'rollup-plugin-visualizer'; // For bundle analysis
export default defineConfig({
plugins: [
react(),
visualizer({
filename: 'bundle-analysis.html', // Output file for analysis
open: true, // Open in browser after build
gzipSize: true, // Show gzip size
brotliSize: true, // Show brotli size
}),
],
build: {
sourcemap: true, // Useful for debugging production builds
minify: 'esbuild', // Faster minification with esbuild
rollupOptions: {
input: {
// Define multiple entry points for multi-page applications
main: './index.html',
admin: './admin.html',
},
output: {
// Control output file naming and chunking strategies
entryFileNames: 'assets/[name]-[hash].js',
chunkFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
manualChunks(id) {
// Custom chunking strategy:
// Separate large third-party libraries into their own chunks
if (id.includes('node_modules')) {
const moduleName = id.split('node_modules/')[1].split('/')[0];
if (['react', 'react-dom', 'redux'].includes(moduleName)) {
return 'vendor-react';
}
if (['antd', 'material-ui'].includes(moduleName)) {
return 'vendor-ui';
}
return 'vendor'; // Generic vendor chunk for others
}
},
},
},
},
});
Explanation:
visualizerplugin: Helps visualize the bundle size, allowing you to identify large dependencies or components.input: Defines multiple HTML entry points for multi-page applications, allowing Vite/Rollup to optimize each page independently.output.entryFileNames,chunkFileNames,assetFileNames: Customize the naming convention for your output files, including hashes for cache busting.output.manualChunks: Provides a powerful way to manually group modules into specific chunks. In this example,react,react-dom, andreduxgo into avendor-reactchunk, UI libraries intovendor-ui, and all othernode_modulesinto a genericvendorchunk. This can lead to highly efficient caching.
4.6: Plugin Development and Ecosystem
What it is: Vite’s plugin ecosystem allows developers to extend its functionality and integrate with various tools and frameworks.
Why it was introduced/changed: Vite’s core is lean by design. Plugins provide a powerful way to add features (e.g., framework support, asset transformation, custom build steps) without bloating the core.
How it works: Vite plugins follow the Rollup plugin interface, with some Vite-specific hooks. They are JavaScript objects with defined properties and methods that Vite calls during different phases (config, server startup, module resolution, transformation, build).
Example: A Simple Custom Vite Plugin
Let’s create a plugin that injects a banner into all JavaScript files.
Create
plugins/my-banner-plugin.js:// plugins/my-banner-plugin.js function myBannerPlugin(options = {}) { const bannerText = options.text || '/* Built with My Vite Plugin */'; return { name: 'my-banner-plugin', // Required: A unique name for your plugin // This hook is called for each module transform transform(code, id) { // Only apply to JavaScript/TypeScript files and exclude node_modules if (/\.(js|jsx|ts|tsx)$/.test(id) && !id.includes('node_modules')) { return bannerText + '\n' + code; } return null; // Return null to indicate no transformation }, // This hook is called during the build process to add a banner to the final output renderChunk(code, chunk) { // Add banner to entry chunks if (chunk.isEntry) { return bannerText + '\n' + code; } return null; }, }; } export default myBannerPlugin;Use the plugin in
vite.config.js:// vite.config.js import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react-swc'; import myBannerPlugin from './plugins/my-banner-plugin'; // Import your custom plugin export default defineConfig({ plugins: [ react(), myBannerPlugin({ text: '/* My Custom App - Version 1.0 */' }), // Use your plugin ], });
Common Plugins:
@vitejs/plugin-react-swc: Faster React compilation with SWC.@vitejs/plugin-vue: Vue 3 support.vite-plugin-pwa: Progressive Web App (PWA) features.vite-plugin-eslint: Integrate ESLint for linting.vite-plugin-mkcert: Automatically generate SSL certificates for HTTPS development.
Chapter 5: Performance Tuning and Optimization
While Vite is fast by default, optimizing large-scale applications requires specific techniques.
5.1: Leveraging SWC for Faster Compilation
What it is: SWC (Speedy Web Compiler) is a Rust-based platform for the web, providing extremely fast JavaScript/TypeScript compilation and transformation. Vite’s official React plugin offers an SWC variant: @vitejs/plugin-react-swc.
Why it was introduced/changed: Replacing Babel with SWC dramatically speeds up development builds and hot module reloads, especially in large React projects. This directly translates to a smoother developer experience with faster feedback loops.
How it works:
You simply install @vitejs/plugin-react-swc and configure your vite.config.js to use it instead of @vitejs/plugin-react.
Example:
Install SWC plugin:
npm install -D @vitejs/plugin-react-swcUpdate
vite.config.js:// vite.config.js import { defineConfig } from 'vite'; import reactSWC from '@vitejs/plugin-react-swc'; export default defineConfig({ plugins: [reactSWC()], });
Tip: If you have custom Babel plugins that are not yet supported by SWC, you might need to stick with @vitejs/plugin-react (which uses Babel) or find SWC equivalents/alternatives. For most applications, SWC offers a superior development experience.
5.2: Understanding and Optimizing optimizeDeps
What it is: optimizeDeps is a Vite feature that pre-bundles your project’s dependencies using esbuild during development. This process converts CommonJS/UMD modules (which are common in node_modules) into native ES Modules, and also bundles multiple internal modules of a dependency into a single module.
Why it was introduced/changed: Browsers do not perform bundling. If a dependency has hundreds of internal modules, the browser would make hundreds of HTTP requests, leading to “request waterfalls” and slow development server startup. Pre-bundling turns these into fewer, larger requests, significantly speeding up cold starts. Vite 2.0 switched to esbuild for this, making it 10-100x faster than previous Rollup-based pre-bundling.
How it works:
Vite automatically detects and pre-bundles dependencies. You can fine-tune this behavior using optimizeDeps in vite.config.js.
Example:
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
optimizeDeps: {
// Include specific dependencies that might not be automatically detected
// or that you want to force pre-bundling for (e.g., a heavily nested library)
include: ['lodash-es', 'moment'],
// Exclude specific dependencies from pre-bundling.
// Use this if a dependency breaks when pre-bundled or is very small/simple.
exclude: ['my-small-utility-library'],
// Control esbuild options for dependency pre-bundling
esbuildOptions: {
// Example: Enable JSX transformation for specific files within dependencies
jsx: 'automatic',
// Example: Define global constants for dependencies if needed
define: {
'__some_feature_flag__': 'true',
},
},
},
});
Tip: Monitor your cold startup times. If they are slow, inspect the network requests in your browser’s dev tools for many individual requests from node_modules. Adjusting optimizeDeps.include can help.
5.3: Dynamic Imports and Code Splitting
What it is: Dynamic imports (import()) allow you to load modules on demand, effectively splitting your application’s code into smaller, asynchronously loaded chunks. Vite automatically handles code splitting based on dynamic imports.
Why it was introduced/changed: Improves initial page load performance by deferring the loading of non-critical code until it’s actually needed (e.g., a dashboard component only loaded when a user navigates to the admin page). This reduces the initial bundle size and accelerates Time To Interactive (TTI).
How it works:
You use the import() syntax, often combined with React’s lazy and Suspense for UI components.
Example: Lazy Loading a React Component
// src/App.jsx
import React, { lazy, Suspense } from 'react';
import LoadingSpinner from './components/LoadingSpinner'; // A simple loading component
// Lazy load the AdminDashboard component
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
function App() {
const [showAdmin, setShowAdmin] = React.useState(false);
const [showProfile, setShowProfile] = React.useState(false);
return (
<div>
<nav>
<button onClick={() => setShowAdmin(!showAdmin)}>
Toggle Admin Dashboard
</button>
<button onClick={() => setShowProfile(!showProfile)}>
Toggle User Profile
</button>
</nav>
<main>
{showAdmin && (
<Suspense fallback={<LoadingSpinner message="Loading Admin Dashboard..." />}>
<AdminDashboard />
</Suspense>
)}
{showProfile && (
<Suspense fallback={<LoadingSpinner message="Loading User Profile..." />}>
<UserProfile />
</Suspense>
)}
{!showAdmin && !showProfile && (
<p>Welcome! Click a button to load a section.</p>
)}
</main>
</div>
);
}
export default App;
// src/pages/AdminDashboard.jsx (a larger component)
import React from 'react';
const AdminDashboard = () => {
// Simulate some heavy computations/data fetching for a large component
const data = Array(1000).fill(0).map((_, i) => `Item ${i}`);
return (
<div>
<h2>Admin Dashboard</h2>
<p>Loaded: {new Date().toLocaleTimeString()}</p>
{/* Display some large data or complex UI */}
<ul>
{data.slice(0, 10).map((item) => (
<li key={item}>{item}</li>
))}
<li>... (showing first 10 of {data.length} items)</li>
</ul>
</div>
);
};
export default AdminDashboard;
Explanation:
When AdminDashboard is clicked, the AdminDashboard.jsx chunk is fetched asynchronously. While it’s loading, the LoadingSpinner is displayed. This ensures the initial bundle remains small, and the user’s browser only downloads the necessary code when a specific feature is accessed.
5.4: Analyzing and Reducing Bundle Size
What it is: Understanding what constitutes your final production bundle is key to optimizing its size and load performance.
Why it was introduced/changed: Smaller bundles load faster, especially on slower networks. Identifying and eliminating unnecessary code or large dependencies directly improves user experience.
How it works:
Use Rollup plugins like rollup-plugin-visualizer to get a graphical representation of your bundle.
Example: Using rollup-plugin-visualizer
Install:
npm install -D rollup-plugin-visualizerConfigure in
vite.config.js:// vite.config.js import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react-swc'; import { visualizer } from 'rollup-plugin-visualizer'; export default defineConfig({ plugins: [ react(), visualizer({ open: true, // Automatically open the analysis HTML in your browser filename: 'dist/bundle-analysis.html', // Output file name gzipSize: true, // Show gzip compressed size brotliSize: true, // Show brotli compressed size }), ], build: { sourcemap: false, // Turn off source maps during the actual build for better performance (re-enable for debugging) }, });Run build:
npm run buildAfter the build, a
bundle-analysis.htmlfile will open in your browser, showing an interactive treemap of your bundle’s contents.
Tips for Reduction:
- Tree-shakeable libraries: Prefer libraries that are designed with ES Modules and pure functions (e.g.,
lodash-esinstead oflodash). - Remove unused code: Regularly review your codebase for dead code.
- Lazy load: Apply dynamic imports for routes, components, and large utilities.
- Image optimization: Compress and use modern formats (WebP, AVIF).
- CSS optimization: Use PurgeCSS or CSS Modules to remove unused CSS.
5.5: Common Performance Pitfalls and Solutions
| Pitfall | Description | Solution |
|---|---|---|
| Excessive Plugin Usage | Too many Vite or Rollup plugins, especially those performing heavy transformations, can slow down dev server startup and build times. | Keep your vite.config.js lean. Only include necessary plugins. Audit community plugins for performance impact using vite --debug plugin-transform or vite-plugin-inspect. |
| Disabling Browser Caching in DevTools | Vite heavily relies on browser caching (especially for pre-bundled dependencies and 304 responses). Disabling it in dev tools for debugging purposes can significantly slow down development. | Ensure “Disable Cache” is not enabled in your browser’s DevTools Network tab during regular development. Create a separate browser profile without extensions or use incognito mode for development. |
| Barrel Files in Libraries | Re-exporting many modules from a single “barrel” file (index.js/index.ts) can lead to unnecessary loading and transformation of modules, even if only one is imported. | Avoid barrel files for large modules or critical paths. Instead, import directly from the specific module (e.g., import { utilA } from './utils/utilA'; instead of import { utilA } from './utils';). |
| Request Waterfalls | In development, if a module takes a long time to transform, subsequent modules that depend on it will wait, creating a waterfall effect and slowing down the initial page load. | Use server.warmup in vite.config.js to pre-transform frequently accessed files (e.g., App.jsx, main entry points). |
| Implicit File Extensions | Importing files without explicit extensions (e.g., import Component from './Component') forces Vite to check multiple possible extensions (.js, .jsx, .ts, etc.), leading to more filesystem operations and slower resolution. | Use explicit file extensions in your imports (e.g., import Component from './Component.jsx'). While Vite’s defaults are generally good, this can offer marginal gains in very large projects. |
| Unoptimized Images | Large, unoptimized images directly included in your public directory or imported without processing can bloat your final bundle and slow down page load. | Use image optimization plugins (vite-plugin-imagemin), compress images with tools like ImageOptim/TinyPNG, and consider modern formats (WebP, AVIF). Use responsive images (srcset). |
| CSS Bloat | Unused CSS rules or large CSS frameworks can lead to significantly larger stylesheets than necessary. | Leverage CSS Modules or utility-first CSS frameworks (like Tailwind CSS with PurgeCSS enabled) to ensure only used CSS is bundled. Consider vite-plugin-purgecss. |
| Unnecessary SSR Externalization | Incorrectly configuring SSR externalization can lead to bundling Node.js-specific modules in client bundles or vice versa, causing errors or bloating. | Ensure your ssr.external configuration (if used) correctly identifies modules that should or should not be bundled for server-side vs. client-side. Vite 5+ aligned SSR externalized modules to match production behavior by default, reducing inconsistencies. |
| Ignoring Node.js Version Requirements | Running an incompatible Node.js version can lead to cryptic errors or unexpected behavior during development and build, especially with ESM-only changes in Vite 7. | Always ensure your Node.js version meets Vite’s requirements (Node.js 20.19+ or 22.12+ for Vite 7). Use a version manager like nvm. |
5.6: Image and CSS Optimization
Image Optimization:
What it is: Reducing the file size of images without significant loss of quality, and using modern image formats.
Why it’s important: Images often constitute a large portion of a web page’s total size, directly impacting load times and user experience.
How it works:
- Compression: Tools can remove metadata and apply lossy/lossless compression.
- Modern Formats: WebP and AVIF offer superior compression and quality compared to JPEG/PNG.
- Responsive Images: Serve different image sizes based on the user’s device and viewport.
Example: Using vite-plugin-imagemin
Install:
npm install -D vite-plugin-imageminConfigure in
vite.config.js:// vite.config.js import { defineConfig } from 'vite'; import imagemin from 'vite-plugin-imagemin'; export default defineConfig({ plugins: [ imagemin({ gifsicle: { optimizationLevel: 7, interlaced: false }, optipng: { optimizationLevel: 7 }, mozjpeg: { quality: 80 }, // Adjust quality for JPEG pngquant: { quality: [0.8, 0.9], speed: 4 }, // Adjust quality range for PNG svgo: { plugins: [{ name: 'removeViewBox', active: false }], // Example SVGO plugin }, }), ], // Ensure images are processed through Vite's build pipeline, // typically by importing them directly in JS/CSS or using `publicDir` for static assets. });
CSS Optimization:
What it is: Reducing the size of your CSS bundles by removing unused rules, minifying, and leveraging CSS Modules or component-scoped CSS.
Why it’s important: Large CSS files can block page rendering. Optimizing CSS improves initial paint and overall performance.
How it works:
- Tree-shaking CSS: Removing CSS rules not used in the final HTML/components.
- Minification: Removing whitespace and comments.
- CSS Modules: Locally scope CSS classes to components to prevent naming conflicts and enable simpler tree-shaking.
Example: Using CSS Modules (Built-in)
/* src/components/Button.module.css */
.button {
background-color: #007bff;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.button:hover {
background-color: #0056b3;
}
// src/components/Button.jsx
import React from 'react';
import styles from './Button.module.css'; // Import as a module
function Button({ children, onClick }) {
return (
<button className={styles.button} onClick={onClick}>
{children}
</button>
);
}
export default Button;
Example: Using vite-plugin-purgecss (for global CSS or frameworks)
Install:
npm install -D vite-plugin-purgecss @fullhuman/postcss-purgecssConfigure
vite.config.js:// vite.config.js import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react-swc'; import purgeCss from 'vite-plugin-purgecss'; export default defineConfig({ plugins: [ react(), purgeCss({ // Paths to your source files that contain classes to be scanned content: ['./index.html', './src/**/*.html', './src/**/*.jsx', './src/**/*.tsx'], // Specify default extractors for common cases (e.g., Tailwind CSS) // defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || [] }), ], });
Tip: For robust CSS optimization, especially with large frameworks like Tailwind CSS or Bootstrap, consider setting up PostCSS with postcss-preset-env and PurgeCSS directly.
Chapter 6: Practical Applications and Best Practices
6.1: SSR and SSG with Vite
What it is:
- Server-Side Rendering (SSR): Renders the initial HTML of your application on the server and sends it to the client. The client-side JavaScript then “hydrates” this static markup, making it interactive.
- Static Site Generation (SSG): Pre-renders your application to HTML files at build time. These static HTML files can then be served from a CDN.
Why it’s important:
- Improved SEO: Search engine crawlers can easily parse fully rendered HTML, which is beneficial for indexing.
- Faster Initial Load: Users see content much quicker because the HTML is delivered immediately, improving perceived performance.
- Better User Experience: Provides a good baseline experience even on slow networks or devices with limited JavaScript support.
How it works with Vite: Vite provides native SSR support, requiring a separate entry point for server-side code. For SSG, you often integrate with meta-frameworks (like Astro, Nuxt, SvelteKit) that leverage Vite, or use tools like VitePress.
Example: Basic SSR Setup with React (Conceptual)
A full SSR setup is complex and often handled by frameworks. This outlines the core idea:
server.js(Node.js server):// server.js (Conceptual Node.js server) import express from 'express'; import { createServer as createViteServer } from 'vite'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function createServer() { const app = express(); // Create Vite server in middleware mode. // This will automatically re-load new modules and // invalidate caches on file changes. const vite = await createViteServer({ server: { middlewareMode: true }, appType: 'custom', // don't serve Vite's own HTML }); // Use vite as a middleware app.use(vite.middlewares); app.use('*', async (req, res, next) => { const url = req.originalUrl; try { // 1. Read index.html let template = await vite.transformIndexHtml( url, fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8') ); // 2. Load the server entry. ssrLoadModule automatically transforms // ESM source code to be usable in Node.js, and also applies // Vite transforms like built-in HMR. const { render } = await vite.ssrLoadModule('/src/entry-server.jsx'); // 3. Render the app HTML. const appHtml = await render(url); // 4. Inject the app-rendered HTML into the template. template = template.replace(`<!--ssr-outlet-->`, appHtml); // 5. Serve the modified HTML. res.status(200).set({ 'Content-Type': 'text/html' }).end(template); } catch (e) { // If an error is caught, let Vite fix the stacktrace so it maps back to // your actual source code. vite.ssrFixStacktrace(e); next(e); } }); app.listen(3000, () => { console.log('Server listening on http://localhost:3000'); }); } createServer();src/entry-server.jsx(Server-side entry point):// src/entry-server.jsx import React from 'react'; import ReactDOMServer from 'react-dom/server'; import App from './App'; export function render(url) { // You can use the URL to determine which route to render // For simplicity, we just render the main App component const html = ReactDOMServer.renderToString(<App />); return html; }index.html(with SSR outlet):<!-- index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vite SSR App</title> </head> <body> <div id="root"><!--ssr-outlet--></div> <script type="module" src="/src/main.jsx"></script> </body> </html>
Best Practice: For complex SSR/SSG, leverage a meta-framework (e.g., Nuxt for Vue, Next.js for React, SvelteKit for Svelte, Astro for static sites) that already integrates Vite’s SSR capabilities.
6.2: Developing Libraries with Vite
What it is: Vite can be used not just for building applications but also for authoring and bundling JavaScript/TypeScript libraries for distribution on npm.
Why it’s important: Vite provides a fast development experience for libraries (with HMR for component libraries), efficient bundling, and support for multiple output formats (ESM, CommonJS, UMD/IIFE).
How it works:
Vite’s build.lib option in vite.config.js configures the library build mode.
Example: Building a Simple React Component Library
Project Setup:
my-component-lib/ ├── package.json ├── vite.config.js ├── src/ │ ├── index.js // Library entry point │ └── components/ │ └── MyButton.jsx └── dist/ // Output directorysrc/components/MyButton.jsx:// src/components/MyButton.jsx import React from 'react'; import './MyButton.css'; function MyButton({ label, onClick }) { return ( <button className="my-button" onClick={onClick}> {label} </button> ); } export default MyButton;src/index.js(Library Entry Point):// src/index.js // Export all components/functions you want to expose from your library export { default as MyButton } from './components/MyButton'; export * from './utils'; // Example: export utilitiesvite.config.jsfor library mode:// vite.config.js import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react-swc'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ plugins: [react()], build: { lib: { entry: path.resolve(__dirname, 'src/index.js'), // Your library's entry point name: 'MyComponentLib', // Global variable name for UMD/IIFE builds formats: ['es', 'umd', 'cjs'], // Output formats: ESM, UMD, CommonJS fileName: (format) => `my-component-lib.${format}.js`, // Naming convention }, rollupOptions: { // Exclude React from the bundle, assuming it will be a peer dependency external: ['react', 'react-dom'], output: { globals: { react: 'React', 'react-dom': 'ReactDOM', }, }, }, // Don't empty outDir in development if you're building a library alongside an example app emptyOutDir: true, // Set to true for production library builds }, });package.jsonfor library metadata:{ "name": "my-component-lib", "version": "1.0.0", "type": "module", "main": "./dist/my-component-lib.cjs.js", // CommonJS entry "module": "./dist/my-component-lib.es.js", // ESM entry "exports": { ".": { "import": "./dist/my-component-lib.es.js", "require": "./dist/my-component-lib.cjs.js" }, "./style.css": "./dist/style.css" }, "files": ["dist"], "scripts": { "dev": "vite", "build": "vite build" }, "peerDependencies": { "react": "^18.x", "react-dom": "^18.x" }, "devDependencies": { "vite": "^7.0.0", "@vitejs/plugin-react-swc": "^3.5.0", "react": "^18.3.1", "react-dom": "^18.3.1" } }
Building the Library:
npm run build
This will generate my-component-lib.es.js, my-component-lib.umd.js, and my-component-lib.cjs.js in the dist directory.
6.3: Handling Common Errors and Troubleshooting
| Error Message | Possible Cause(s) | Solution(s) |
|---|---|---|
[plugin:vite:import-analysis] Failed to resolve import "..." | - Incorrect file path or name (typo, case sensitivity).- Missing file extension.- Component not exported correctly.- Path alias misconfiguration.- Vite cache issue. | - Verify File Path & Name: Double-check exact path and case-sensitive filename.- Add File Extension: Explicitly add .js, .jsx, .ts, .tsx, etc.- Check Export: Ensure component is export default or export const and imported correctly.- Review Alias: Verify resolve.alias in vite.config.js and tsconfig.json (for TS).- Clear Cache & Restart: rm -rf node_modules/.vite && npm run dev. Sometimes rm -rf node_modules && npm install is needed. |
Vite manifest not found at: public/build/manifest.json | - Production build not run (npm run build).- Output directory changed from default public/build. | - Run Build: Ensure you’ve run npm run build (or equivalent).- Check build.outDir: If you changed build.outDir in vite.config.js, update your server configuration to point to the correct manifest location. If using frameworks like Laravel, ensure their Vite integration knows the custom path. |
Uncaught ReferenceError: $ is not defined | - jQuery not correctly imported/exposed in a modern ES Module context.- Global variables from legacy scripts not available. | - Import jQuery: import $ from 'jquery';.- Expose Globally (Legacy): For older scripts, consider window.$ = $; or use a plugin that exposes jQuery globally (e.g., vite-plugin-external-globals).- Refactor: Best practice is to avoid global jQuery in favor of modern JS or component-based approaches. |
vite: Permission denied | - Insufficient file permissions on Linux/macOS. | - Check Permissions: ls -l on your project folder. Try chmod +x node_modules/.bin/vite.- Run as Sudo (Avoid if possible): sudo npm run dev. Better to fix permissions. |
Requests are stalled forever / Network requests stop loading | - Linux file descriptor/inotify limits.- Self-signed SSL certificate issues (Chrome). | - Increase Limits (Linux): ulimit -Sn 10000, sudo sysctl fs.inotify.max_user_watches=524288 (and other inotify settings).- Trusted SSL Cert: Use vite-plugin-mkcert for trusted local HTTPS, or a properly signed certificate. Chrome ignores caching for self-signed certs. |
431 Request Header Fields Too Large | - Large HTTP headers (e.g., very long cookies). | - Reduce Header Size: Clear browser cookies for localhost.- Increase Node.js Limit (Dev Only): Use --max-http-header-size Node.js flag (e.g., NODE_OPTIONS=--max-http-header-size=16384 vite dev). |
[plugin:vite:react-jsx] Cannot read properties of undefined | - Mismatch between React plugin and JSX settings (e.g., jsxRuntime).- Babel/SWC configuration issues. | - Check React Plugin: Ensure you have @vitejs/plugin-react or @vitejs/plugin-react-swc installed and configured.- tsconfig.json (for TS/JSX): Ensure compilerOptions.jsx is set to react-jsx or react-jsxdev and experimentalDecorators/useDefineForClassFields are correct based on your needs. |
| Slow Dev Server / HMR | - Many browser extensions.- Heavy plugins.- Unoptimized optimizeDeps.- Barrel files. | - Browser Profile: Use a dedicated browser profile without extensions or incognito mode.- Audit Plugins: Review vite.config.js, remove unnecessary plugins. Profile with vite --debug plugin-transform.- OptimizeDeps: Use optimizeDeps.include and exclude as needed.- Avoid Barrel Files: Import directly from modules. |
Missing Global process.env | - Vite uses import.meta.env instead of process.env by default for client-side environment variables. | - Use import.meta.env: Access variables via import.meta.env.VITE_MY_VAR.- Define process.env (Legacy/Compat): If a third-party library strictly relies on process.env, you can manually define it in vite.config.js using the define option: define: { 'process.env': {} }. Be cautious, as this can expose all process.env values. |
Chapter 7: Guided Projects
These projects integrate the concepts learned and demonstrate practical application with Vite 7.0.
7.1: Project 1: Basic Vite 7.0 React Todo App with API Integration
This project will guide you through building a simple React Todo application using Vite 7.0, integrating with a mock REST API.
Features:
- Display a list of todos.
- Add new todos.
- Mark todos as complete/incomplete.
- Delete todos.
- Fetch/update data from a local JSON server (mock API).
Prerequisites: Node.js 20.19+ or 22.12+, npm or yarn.
Steps:
Initialize Project:
npm create vite@latest vite-todo-app -- --template react-ts cd vite-todo-app npm installInstall JSON Server (Mock API):
npm install -D json-serverCreate
db.jsonfor Mock API: Create a file nameddb.jsonin the root of your project:// db.json { "todos": [ { "id": "1", "text": "Learn Vite 7.0", "completed": false }, { "id": "2", "text": "Build a React App", "completed": true }, { "id": "3", "text": "Explore Rolldown", "completed": false } ] }Add
json-serverscript topackage.json:{ "name": "vite-todo-app", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "api": "json-server --watch db.json --port 3001" // Add this line }, "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/parser": "^7.13.1", "@vitejs/plugin-react-swc": "^3.5.0", "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", "json-server": "^1.0.0-beta.1", "typescript": "^5.2.2", "vite": "^5.3.1" } }Configure Vite Proxy for API: Create/update
vite.config.ts:// vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react-swc'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], server: { proxy: { '/api': { target: 'http://localhost:3001', // Your JSON server changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), // Remove /api prefix }, }, }, });Update
src/App.tsx:// src/App.tsx import React, { useState, useEffect } from 'react'; import './App.css'; interface Todo { id: string; text: string; completed: boolean; } function App() { const [todos, setTodos] = useState<Todo[]>([]); const [newTodo, setNewTodo] = useState(''); const API_BASE_URL = '/api/todos'; // Proxied API URL useEffect(() => { fetchTodos(); }, []); const fetchTodos = async () => { try { const response = await fetch(API_BASE_URL); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data: Todo[] = await response.json(); setTodos(data); } catch (error) { console.error('Error fetching todos:', error); } }; const addTodo = async (e: React.FormEvent) => { e.preventDefault(); if (!newTodo.trim()) return; const todo: Omit<Todo, 'id'> = { text: newTodo, completed: false, }; try { const response = await fetch(API_BASE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(todo), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const addedTodo: Todo = await response.json(); setTodos((prevTodos) => [...prevTodos, addedTodo]); setNewTodo(''); } catch (error) { console.error('Error adding todo:', error); } }; const toggleTodo = async (id: string) => { const todoToUpdate = todos.find((todo) => todo.id === id); if (!todoToUpdate) return; const updatedTodo = { ...todoToUpdate, completed: !todoToUpdate.completed }; try { const response = await fetch(`${API_BASE_URL}/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updatedTodo), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } setTodos((prevTodos) => prevTodos.map((todo) => (todo.id === id ? updatedTodo : todo)) ); } catch (error) { console.error('Error toggling todo:', error); } }; const deleteTodo = async (id: string) => { try { const response = await fetch(`${API_BASE_URL}/${id}`, { method: 'DELETE', }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id)); } catch (error) { console.error('Error deleting todo:', error); } }; return ( <div className="App"> <h1>Vite Todo App</h1> <form onSubmit={addTodo}> <input type="text" value={newTodo} onChange={(e) => setNewTodo(e.target.value)} placeholder="Add a new todo" /> <button type="submit">Add Todo</button> </form> <ul> {todos.map((todo) => ( <li key={todo.id} className={todo.completed ? 'completed' : ''}> <span onClick={() => toggleTodo(todo.id)}>{todo.text}</span> <button onClick={() => deleteTodo(todo.id)}>Delete</button> </li> ))} </ul> </div> ); } export default App;Update
src/index.css(Optional Styling):/* src/index.css */ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { margin: 0; display: flex; place-items: center; min-width: 320px; min-height: 100vh; } #root { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; width: 100%; } .App { background-color: #333; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); } h1 { font-size: 2.2em; line-height: 1.1; color: #646cff; } form { display: flex; margin-bottom: 20px; justify-content: center; } form input { padding: 10px; border: 1px solid #646cff; border-radius: 4px; margin-right: 10px; background-color: #1a1a1a; color: white; flex-grow: 1; max-width: 300px; } form button { padding: 10px 15px; background-color: #646cff; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.25s; } form button:hover { background-color: #535bf2; } ul { list-style: none; padding: 0; text-align: left; max-width: 400px; margin: 0 auto; } li { display: flex; justify-content: space-between; align-items: center; padding: 10px; margin-bottom: 8px; background-color: #444; border-radius: 4px; color: white; } li span { cursor: pointer; flex-grow: 1; } li.completed span { text-decoration: line-through; color: #999; } li button { background-color: #dc3545; color: white; border: none; padding: 6px 10px; border-radius: 4px; cursor: pointer; transition: background-color 0.25s; } li button:hover { background-color: #c82333; }Run the application: Open two terminal windows:
- Terminal 1 (for API):
npm run api - Terminal 2 (for Vite dev server):
npm run dev
Now, open your browser to
http://localhost:5173and interact with the Todo app. The Vite proxy seamlessly redirects API calls to your JSON server.- Terminal 1 (for API):
7.2: Project 2: Performance-Optimized Image Gallery with Lazy Loading
This project demonstrates how to create an image gallery using Vite 7.0 and implement performance optimizations like image lazy loading and dynamic imports.
Features:
- Display a grid of image thumbnails.
- Lazy load full-size images when they enter the viewport.
- Use a modal to display the full-size image.
- Implement basic route-based code splitting for the modal component.
Prerequisites: Node.js 20.19+ or 22.12+, npm or yarn.
Steps:
Initialize Project:
npm create vite@latest vite-image-gallery -- --template react-ts cd vite-image-gallery npm installPrepare Images: Create a
public/imagesdirectory and place some sample images (thumbnails and full-size versions) in it. For simplicity, use the same images and just reference them as if they were different sizes. In a real app, you’d have actual different sizes or use an image CDN.public/ └── images/ ├── thumb-1.jpg ├── full-1.jpg ├── thumb-2.jpg ├── full-2.jpg └── ...(You can use placeholders like
https://via.placeholder.com/150/0000FF/FFFFFF?text=Thumbandhttps://via.placeholder.com/800/FF0000/FFFFFF?text=Fullfor testing if you don’t have images ready).Create Components:
src/components/ImageCard.tsx: Displays a thumbnail and handles click to open modal.// src/components/ImageCard.tsx import React from 'react'; import './ImageCard.css'; interface ImageCardProps { thumbnailSrc: string; fullSrc: string; alt: string; onClick: (fullSrc: string, alt: string) => void; } const ImageCard: React.FC<ImageCardProps> = ({ thumbnailSrc, fullSrc, alt, onClick }) => { return ( <div className="image-card" onClick={() => onClick(fullSrc, alt)}> <img src={thumbnailSrc} alt={alt} loading="lazy" // Native lazy loading /> </div> ); }; export default ImageCard;src/components/ImageCard.css:/* src/components/ImageCard.css */ .image-card { border: 1px solid #555; border-radius: 8px; overflow: hidden; cursor: pointer; transition: transform 0.2s ease-in-out; display: flex; justify-content: center; align-items: center; background-color: #333; } .image-card:hover { transform: translateY(-5px); } .image-card img { width: 100%; height: 100%; object-fit: cover; display: block; }src/components/ImageModal.tsx: The modal component (will be lazy loaded).// src/components/ImageModal.tsx import React, { useEffect, useRef } from 'react'; import './ImageModal.css'; interface ImageModalProps { src: string; alt: string; onClose: () => void; } const ImageModal: React.FC<ImageModalProps> = ({ src, alt, onClose }) => { const modalRef = useRef<HTMLDivElement>(null); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (modalRef.current && !modalRef.current.contains(event.target as Node)) { onClose(); } }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [onClose]); return ( <div className="image-modal-overlay"> <div className="image-modal-content" ref={modalRef}> <button className="close-button" onClick={onClose}> × </button> <img src={src} alt={alt} /> <p>{alt}</p> </div> </div> ); }; export default ImageModal;src/components/ImageModal.css:/* src/components/ImageModal.css */ .image-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; z-index: 1000; } .image-modal-content { background-color: #2a2a2a; padding: 20px; border-radius: 8px; position: relative; max-width: 90%; max-height: 90%; overflow: auto; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; align-items: center; } .image-modal-content img { max-width: 100%; max-height: 80vh; /* Limit height to viewport height */ display: block; margin-bottom: 15px; } .image-modal-content p { color: white; font-size: 1.1em; text-align: center; } .close-button { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 2em; color: white; cursor: pointer; z-index: 1001; } .close-button:hover { color: #ff0000; }
Update
src/App.tsx:// src/App.tsx import React, { useState, lazy, Suspense } from 'react'; import ImageCard from './components/ImageCard'; import './App.css'; // Main styling for App // Lazy load the ImageModal component const ImageModal = lazy(() => import('./components/ImageModal')); // Sample image data (replace with your actual image paths in public/images) const images = [ { id: '1', thumb: '/images/thumb-1.jpg', full: '/images/full-1.jpg', alt: 'Beautiful Landscape 1' }, { id: '2', thumb: '/images/thumb-2.jpg', full: '/images/full-2.jpg', alt: 'Abstract Art Piece 2' }, { id: '3', thumb: '/images/thumb-3.jpg', full: '/images/full-3.jpg', alt: 'City Skyline at Night 3' }, { id: '4', thumb: '/images/thumb-4.jpg', full: '/images/full-4.jpg', alt: 'Mountain Range Sunset 4' }, { id: '5', thumb: '/images/thumb-5.jpg', full: '/images/full-5.jpg', alt: 'Forest Path in Autumn 5' }, { id: '6', thumb: '/images/thumb-6.jpg', full: '/images/full-6.jpg', alt: 'Ocean Waves Breaking 6' }, // Add more images as needed ]; function App() { const [selectedImage, setSelectedImage] = useState<{ src: string; alt: string } | null>(null); const openModal = (src: string, alt: string) => { setSelectedImage({ src, alt }); }; const closeModal = () => { setSelectedImage(null); }; return ( <div className="App"> <h1>Vite Image Gallery</h1> <div className="gallery-grid"> {images.map((image) => ( <ImageCard key={image.id} thumbnailSrc={image.thumb} fullSrc={image.full} alt={image.alt} onClick={openModal} /> ))} </div> {selectedImage && ( <Suspense fallback={<div>Loading image modal...</div>}> <ImageModal src={selectedImage.src} alt={selectedImage.alt} onClose={closeModal} /> </Suspense> )} </div> ); } export default App;Update
src/index.css(Global Styling):/* src/index.css */ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { margin: 0; min-width: 320px; min-height: 100vh; display: flex; justify-content: center; align-items: flex-start; /* Align to top for scrolling */ padding: 2rem; box-sizing: border-box; } #root { max-width: 1200px; width: 100%; text-align: center; } .App { background-color: #333; padding: 30px; border-radius: 12px; box-shadow: 0 0 15px rgba(0, 0, 0, 0.6); width: 100%; } h1 { font-size: 2.8em; line-height: 1.1; color: #646cff; margin-bottom: 30px; } .gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; justify-content: center; }Run the application:
npm run devOpen your browser to
http://localhost:5173. Scroll down to see images lazy-loading. Click an image to open the modal, and observe how the modal component is loaded only when needed. You can verify this by checking the Network tab in your browser’s developer tools; theImageModalchunk will only appear after the first click.
7.3: Project 3: Micro-frontend Setup with Vite 7.0 and Module Federation (Conceptual)
This is a conceptual project to illustrate how Vite can fit into a micro-frontend architecture, specifically focusing on Module Federation, which is typically provided by Rollup or Webpack plugins. Vite itself doesn’t have built-in Module Federation like Webpack 5, but its ESM-first approach makes it highly compatible with solutions that build on top of it.
For a full implementation, you would typically use a dedicated Module Federation plugin for Rollup that can be integrated with Vite.
Concept: Imagine two separate Vite applications:
- Host Application: The main shell that loads and orchestrates micro-frontends.
- Remote Application (Micro-frontend): A standalone Vite application that exposes some of its components or modules to be consumed by the Host.
Key Idea: Each micro-frontend is a fully independent Vite application, developed and deployed separately. Module Federation allows them to share code and expose modules at runtime.
Prerequisites: Node.js 20.19+ or 22.12+, npm or yarn.
Steps (Conceptual - focuses on structure and Vite integration points):
Project Structure:
microfrontend-host/ ├── package.json ├── vite.config.ts ├── src/ │ ├── App.tsx │ └── main.tsx └── index.html microfrontend-remote/ ├── package.json ├── vite.config.ts ├── src/ │ ├── bootstrap.tsx // Entry point for remote │ └── components/ │ └── RemoteButton.tsx └── index.htmlmicrofrontend-remote(The Micro-frontend)microfrontend-remote/vite.config.ts: To expose modules for consumption by a host application using a Module Federation plugin for Rollup (likevite-plugin-federationwhich wraps Rollup’s MF capabilities), your remote’s Vite config would look something like this.// microfrontend-remote/vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react-swc'; // You would install and use a Module Federation plugin for Vite here // For example: import federation from '@module-federation/vite'; (Conceptual) export default defineConfig({ plugins: [ react(), // federation({ // Conceptual Module Federation plugin config // name: 'remote_app', // filename: 'remoteEntry.js', // This file will contain shared modules // exposes: { // './RemoteButton': './src/components/RemoteButton.tsx', // }, // shared: ['react', 'react-dom'], // Share common dependencies // }), ], build: { // Ensure this builds for a static host and generates entry points correctly target: 'esnext', minify: false, // For demonstration, prevent minification cssCodeSplit: false, }, server: { port: 3002, // Remote app runs on a different port }, });microfrontend-remote/src/components/RemoteButton.tsx:// microfrontend-remote/src/components/RemoteButton.tsx import React from 'react'; interface RemoteButtonProps { label: string; onClick?: () => void; } const RemoteButton: React.FC<RemoteButtonProps> = ({ label, onClick }) => { return ( <button style={{ backgroundColor: 'purple', color: 'white', padding: '10px 20px', borderRadius: '5px', border: 'none', cursor: 'pointer', margin: '5px', }} onClick={onClick} > {label} (from Remote) </button> ); }; export default RemoteButton;microfrontend-remote/src/bootstrap.tsx: (Main entry point for the remote, typically renders into a host-provided DOM element)// microfrontend-remote/src/bootstrap.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; // This file would typically be used to initialize the remote app // when mounted by the host. // For demonstration, let's just make sure RemoteButton is accessible // (the federation plugin handles exposing it) console.log('Remote micro-frontend bootstrapped.');microfrontend-remote/index.html: (Not directly accessed by host, but needed for dev server)<!-- microfrontend-remote/index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Remote Microfrontend</title> </head> <body> <div id="root"></div> <!-- The actual remote entry would be loaded by the federation runtime --> <script type="module" src="/src/bootstrap.tsx"></script> </body> </html>
microfrontend-host(The Host Application)microfrontend-host/vite.config.ts: The host’s Vite config would also use the Module Federation plugin to consume remotes.// microfrontend-host/vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react-swc'; // import federation from '@module-federation/vite'; (Conceptual) export default defineConfig({ plugins: [ react(), // federation({ // Conceptual Module Federation plugin config // name: 'host_app', // remotes: { // remote_app: 'http://localhost:3002/remoteEntry.js', // URL of the remote's entry file // }, // shared: ['react', 'react-dom'], // }), ], server: { port: 3000, // Host app runs on port 3000 }, });microfrontend-host/src/App.tsx: This is where the host would dynamically import and render the remote component.// microfrontend-host/src/App.tsx import React, { useState, Suspense, lazy } from 'react'; import './App.css'; // Dynamically import the remote component (conceptual) // This import statement would be handled by the Module Federation runtime const RemoteButton = lazy(() => import('remote_app/RemoteButton')); function App() { const [counter, setCounter] = useState(0); return ( <div className="App"> <h1>Host Application</h1> <p>Host Counter: {counter}</p> <button onClick={() => setCounter((prev) => prev + 1)}> Increment Host Counter </button> <h2>Remote Microfrontend Section</h2> <Suspense fallback={<div>Loading Remote Button...</div>}> <RemoteButton label="Click Me from Host!" onClick={() => alert('Remote Button Clicked!')} /> </Suspense> </div> ); } export default App;microfrontend-host/src/main.tsx&index.html: Standard Vite setup.
Execution (Conceptual):
- In one terminal, run
npm run devinmicrofrontend-remote(ornpm run buildfor a production build to serve). - In another terminal, run
npm run devinmicrofrontend-host.
The host application on localhost:3000 would then load and display the RemoteButton component from localhost:3002.
Challenges and Considerations:
- Module Federation Plugin: The biggest piece here is the actual Module Federation plugin for Vite/Rollup.
vite-plugin-federation(or similar) is a popular community solution. - Shared Dependencies: Carefully manage shared dependencies (like React) to avoid bundle duplication and ensure consistent versions.
- Runtime: Module Federation orchestrates loading at runtime, which is a powerful concept.
- Build Complexity: While powerful, micro-frontends add build and deployment complexity.
This conceptual project highlights Vite’s flexibility due to its ESM-first approach, which makes it suitable for integration with advanced patterns like Module Federation through the ecosystem’s plugins.
Chapter 8: Further Exploration & Resources
To continue your journey with Vite and stay up-to-date with the latest developments, here are some invaluable resources.
8.1: Official Documentation and Community
- Vite Official Documentation: The absolute best place to start and reference. It’s comprehensive, well-maintained, and covers everything from basic setup to advanced configuration and migration guides.
- Vite GitHub Repository: Explore the source code, open issues, and contribute to the project.
- Vite Discord Community: Join the official Vite Land Discord server to ask questions, get help, and connect with other developers.
8.2: Blogs, Articles, and Video Tutorials
- Vite Blog: Official announcements and deep dives into new features.
- Medium Articles: Search for “Vite 7.0,” “Vite performance,” “Vite React” on Medium. Reputable authors often publish detailed guides and case studies.
- Examples: “Vite 7.0 is out!”, “I Upgraded to Vite 7: What Got 10x Better and Why It Might be the Future of Frontend Dev.”, “Advanced Guide to Using Vite with React in 2025”
- DEV Community: Similar to Medium, many developers share insights and tutorials on DEV.to.
- YouTube Channels:
- Vue Mastery: Often covers Vite topics in relation to Vue.
- Fireship: Provides concise, high-level overviews of new technologies, including Vite.
- Official ViteConf Playlist: Recordings from past Vite conferences, featuring talks from core team members and ecosystem leaders.
8.3: Essential Third-Party Libraries and Tools
- Framework-Specific Plugins:
@vitejs/plugin-react-swc: For React with SWC for faster compilation.@vitejs/plugin-vue: For Vue 3.@vitejs/plugin-legacy: For supporting older browsers.
- Development & Build Tools:
rollup-plugin-visualizer: Analyze your bundle size visually.vite-plugin-imagemin: Image optimization during build.vite-plugin-pwa: Add Progressive Web App features.vite-plugin-eslint: Integrate ESLint.vite-plugin-mkcert: Automatically generate SSL certificates for local HTTPS.json-server: A simple tool for creating mock REST APIs for development.
- Testing Frameworks:
Vitest: A blazingly fast unit-test framework powered by Vite.
- Deployment Platforms:
- Netlify, Vercel, GitHub Pages: Excellent for static site deployments.
- Cloudflare Workers: For edge deployments, often integrated with Vite’s Environment API.
8.4: Additional Project Ideas
- Real-time Chat Application: Use Vite with a framework (React/Vue) and a WebSocket library (e.g., Socket.IO) to build a real-time chat app.
- Markdown Editor with Preview: Create an editor that allows users to write Markdown and see a live preview. Explore remark/rehype for parsing and rendering.
- Component Library with Storybook: Build a reusable UI component library using Vite and document it with Storybook. Focus on modularity and proper bundling for npm.
- PWA Weather App: Develop a Progressive Web App that fetches weather data from an API, utilizing Vite’s PWA plugin for offline capabilities and installability.
- Online Code Editor (Monaco Editor Integration): Integrate the Monaco Editor (VS Code’s editor) into a Vite application. This involves handling a large dependency efficiently.
- Interactive Data Visualization Dashboard: Build a dashboard using a charting library (e.g., Chart.js, D3.js) and display dynamic data. Focus on performance with large datasets.
- Simple E-commerce Product Page: Create a single product page that showcases images, descriptions, and dynamic pricing. Focus on asset optimization and responsive design.
- Server-Side Rendered Blog: Set up a simple blog using SSR with a framework like React or Vue, showing how Vite facilitates SSR builds.
- Vite Plugin Development: Create your own custom Vite plugin to solve a specific development workflow problem (e.g., a custom asset loader, a code transformation).
- Web Component Showcase: Build a collection of native Web Components and demonstrate how Vite can bundle and serve them efficiently.