· vite / webpack / typescript
Cách migrate từ Webpack sang Vite (dự án thực tế)
Hướng dẫn từng bước migrate Webpack 5 → Vite 8 trên dự án TypeScript + React: config trước/sau, đổi tên biến môi trường, CJS interop, và sáu vấn đề cần chú ý.
Bởi Ethan
2.506 từ · 13 phút đọc
Lưu ý: Một số liên kết trong bài là affiliate link — nếu bạn click và mua, toolchew nhận hoa hồng mà không tốn thêm chi phí của bạn. Chúng tôi chỉ link đến những tool chúng tôi đã tự dùng. Việc có affiliate không ảnh hưởng đến nhận xét; nếu một tool thua đối thủ không phải affiliate, chúng tôi sẽ nói thẳng.
Nếu bạn vẫn đang dùng Webpack 5 cho dự án TypeScript + React, chuyển sang Vite sẽ giảm thời gian cold start từ hàng chục giây xuống dưới một giây. Hướng dẫn này đi qua từng bước cụ thể — chuyển đổi config, đổi tên biến môi trường, xử lý CommonJS interop, quản lý asset, và cấu hình production build — để bạn biết rõ mình đang đối mặt với gì trước khi bắt đầu.
Bài này dành cho ai
Dành cho các developer có dự án Webpack 5 đang chạy với TypeScript + React (hoặc Vue) và muốn migrate sang Vite. Bạn cần biết chỉnh sửa package.json và các file cấu hình build. Không cần kinh nghiệm với Vite từ trước.
Nếu dự án của bạn dùng Webpack Module Federation cho micro-frontend, hãy đọc phần “Khi nào không nên migrate” trước — hướng dẫn này không giải quyết trường hợp đó.
Tại sao migrate năm 2026
Webpack 5 đang ở chế độ bảo trì. Tobias Koppers, tác giả gốc của webpack, đã chuyển hướng sang Turbopack; changelog của webpack vẫn nhận các bản cập nhật nhỏ và vá bảo mật, nhưng không có đợt viết lại kiến trúc lớn nào đang diễn ra. Vite đang chiếm thị phần nhanh trong các khảo sát State of JavaScript gần đây.
Sự chênh lệch hiệu năng là thực tế. Trên một ứng dụng React 200 component, cold start giảm từ 34.2s xuống 0.8s (nhanh hơn 42×), HMR từ 2.8s xuống 50ms (56×), và thời gian production build từ 142s xuống 38s (3.7×). Những con số này đến từ một benchmark đã được công bố — không phải marketing của vendor.
Vite 8 (bản stable hiện tại) cũng thay thế Rollup và esbuild bằng Rolldown, một bundler viết bằng Rust. Hầu hết các bước migrate trong hướng dẫn này đều hoạt động giống nhau trên Vite 5–8; chỗ nào v8 khác biệt sẽ được chỉ rõ.
Chưa chắc có đáng migrate không? Vite vs Webpack đi sâu hơn vào các điểm đánh đổi. Nếu Turbopack cũng nằm trong danh sách cân nhắc — đặc biệt khi bạn đang dùng Next.js — Turbopack vs Vite có so sánh trực tiếp với số liệu HMR và production build.
Điều kiện tiên quyết
- Node.js 20.19+ hoặc 22.12+ — kiểm tra bằng
node -v - Vite 8.0.10 (
npm install -D [email protected]) - @vitejs/plugin-react (phiên bản mới nhất — dùng Oxc Transformer bên trong)
- Vitest 4.1.6 nếu bạn cũng muốn migrate test suite
Nếu cần hỗ trợ trình duyệt cũ (thời IE 11): @vitejs/plugin-legacy — nhưng hãy cài thêm terser riêng. Plugin sẽ báo lỗi build nếu Terser chưa được cài, và thông báo lỗi không nói rõ nguyên nhân.
Muốn học sâu hơn về Vite và tooling JavaScript hiện đại sau khi hoàn tất migration? Khóa học Vite và modern JS tooling trên Udemy đi vào chi tiết cơ chế hoạt động bên trong.
Các bước migrate
Bước 1: Cài đặt Vite và plugin
npm install -D vite @vitejs/plugin-react
Gỡ Webpack và các package đi kèm:
npm uninstall webpack webpack-cli webpack-dev-server html-webpack-plugin \
css-loader style-loader ts-loader babel-loader \
mini-css-extract-plugin webpack-merge
Giữ lại typescript — Vite dùng nó cho type checking, dù quá trình biên dịch đi qua esbuild/Oxc.
Bước 2: Tạo vite.config.ts
Tạo file này ở thư mục gốc của dự án:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
base: '/',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
build: {
outDir: 'dist',
},
});
Trước (webpack.config.js):
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
},
resolve: {
alias: { '@': path.resolve(__dirname, 'src') },
},
module: {
rules: [{ test: /\.tsx?$/, use: 'ts-loader' }],
},
plugins: [new HtmlWebpackPlugin({ template: './public/index.html' })],
};
Bảng đối chiếu khái niệm:
| Webpack | Vite |
|---|---|
entry | index.html ở thư mục gốc |
output.publicPath | base |
output.path | build.outDir |
resolve.alias | resolve.alias (cùng cấu trúc) |
ts-loader | Built-in (Oxc/esbuild) |
css-loader + style-loader | Built-in |
HtmlWebpackPlugin | Không cần |
webpack-dev-server | Lệnh vite |
Bước 3: Chuyển index.html lên thư mục gốc
Vite coi index.html là entry point, không phải output artifact. Chuyển file từ public/index.html lên thư mục gốc, rồi thêm script module trỏ đến file main của bạn:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Xóa các tham chiếu %PUBLIC_URL% mà Create React App đã thêm — Vite không dùng quy ước đó. Dùng base trong vite.config.ts để thiết lập public URL thay thế.
Bước 4: Cập nhật biến môi trường
Chỉ các biến có tiền tố VITE_ mới được expose ra code phía client. Đổi tên toàn bộ:
# .env.development (trước)
REACT_APP_API_URL=https://api.example.com
REACT_APP_FEATURE_FLAGS=true
# .env.development (sau)
VITE_API_URL=https://api.example.com
VITE_FEATURE_FLAGS=true
Sau đó cập nhật mọi chỗ tham chiếu trong source code:
// Trước
const apiUrl = process.env.REACT_APP_API_URL;
const isDev = process.env.NODE_ENV === 'development';
// Sau
const apiUrl = import.meta.env.VITE_API_URL;
const isDev = import.meta.env.DEV; // boolean có sẵn
const isProd = import.meta.env.PROD; // boolean có sẵn
Để TypeScript nhận diện các biến tự định nghĩa, tạo src/vite-env.d.ts:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_FEATURE_FLAGS: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Nếu có thư viện bên thứ ba vẫn đọc process.env.NODE_ENV trực tiếp, thêm đoạn này vào vite.config.ts:
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},
Bước 5: Xử lý CommonJS interop
Vite tự động pre-bundle các npm package (lodash, moment, axios, v.v.) sang ESM khi dev. Vấn đề xảy ra với code của chính bạn.
Các lệnh require() trong code bạn viết — chuyển sang import:
// Trước
const fs = require('fs');
const { format } = require('date-fns');
// Sau
import fs from 'fs';
import { format } from 'date-fns';
require() động với biến — chuyển sang await import():
// Trước
const plugin = require(`./plugins/${name}`);
// Sau
const plugin = await import(`./plugins/${name}`);
Lỗi default import với CJS — nếu gặp “Element type is invalid: expected a string, but got: object”, package đó export theo kiểu CJS và default bị bọc thêm một lớp:
// Fix
import * as M from 'some-cjs-package';
const fn = M.default ?? M;
File config ESM — nếu vite.config.js dùng require(), đổi thành vite.config.ts hoặc thêm "type": "module" vào package.json.
Để giải quyết tạm thời, @originjs/vite-plugin-commonjs có thể tự động chuyển đổi CJS syntax trong source file. Plugin này xử lý được những trường hợp đơn giản; với dynamic require phức tạp thì có các vấn đề đã biết. Dùng để unblock trước, sau đó migrate thực sự.
Bước 6: Cập nhật path alias trong tsconfig.json
Bundler của Vite và type-checker của TypeScript hoạt động độc lập. Nếu chỉ thêm alias vào vite.config.ts, IDE và tsc vẫn không resolve được.
Thêm paths tương ứng vào tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
Hoặc cài vite-tsconfig-paths để tự động đồng bộ hai bên:
// vite.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
});
Bước 7: Cập nhật scripts trong package.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
Xóa các script liên quan đến webpack, webpack-dev-server, webpack --watch.
Bước 8: Thay thế asset loader
file-loader, raw-loader, và url-loader của Webpack không tồn tại trong Vite. Dùng query suffix thay thế:
import logo from './logo.svg'; // URL — tương đương file-loader
import shader from './shader.glsl?raw'; // nội dung file dạng string
import workletUrl from './audio-worklet.js?url'; // URL tường minh
import icon from './small-icon.png?inline'; // data URI base64
import MyWorker from './heavy.worker.ts?worker'; // Web Worker
SVG dưới dạng React component (trước đây dùng @svgr/webpack):
npm install -D vite-plugin-svgr
import { ReactComponent as Logo } from './logo.svg?react';
Bước 9: Cấu hình code splitting
splitChunks của Webpack tương ứng với manualChunks của Rollup trong Vite 5–7:
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor';
}
},
},
},
},
Lưu ý Vite 8: build.rollupOptions đổi thành build.rolldownOptions trong Vite 8. Dạng object của manualChunks đã bị loại bỏ; dạng function ở trên vẫn hoạt động. Xem hướng dẫn migrate Vite 8 để biết đầy đủ thay đổi.
Bước 10: Migrate test sang Vitest
Vitest 4.1.6 đọc vite.config.ts của bạn tự động và không cần cấu hình transform riêng như Jest nữa.
npm install -D vitest @vitest/coverage-v8 jsdom
Tạo vitest.config.ts (hoặc thêm vào vite.config.ts):
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.ts',
},
});
Các điểm khác biệt chính so với Jest:
| Tính năng | Jest | Vitest |
|---|---|---|
| Mock factory mặc định | Tự động | Phải export { default: ... } tường minh |
| Import module thực | jest.requireActual() | await vi.importActual() |
| Global APIs | Bật mặc định | Đặt globals: true trong config |
| Timer legacy mode | Hỗ trợ | Không hỗ trợ |
Nếu muốn giữ Jest — hợp lý khi test suite lớn — bạn có thể chạy Vite cho app và Jest cho test song song. Dùng ts-jest với preset: 'ts-jest'. Đây là hướng chậm hơn nhưng ít thay đổi hơn khi làm từng bước.
So sánh đầy đủ giữa hai test runner — tốc độ watch mode, coverage, và các điểm khác biệt quan trọng — có tại Vitest vs Jest.
Các vấn đề thường gặp
1. Path alias cần cấu hình ở hai chỗ. Alias trong vite.config.ts điều khiển bundler; paths trong tsconfig.json điều khiển TypeScript. Cả hai phải khớp nhau. Nếu chỉ thêm một bên, app có thể build được nhưng IDE hiển thị lỗi — hoặc ngược lại.
2. __dirname không tồn tại trong ESM config. Nếu chuyển vite.config.js sang ESM và gặp lỗi __dirname is not defined, dùng:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
3. process.env.NODE_ENV trong thư viện bên thứ ba. Các thư viện như react và styled-components đọc process.env.NODE_ENV lúc runtime. Vite không định nghĩa biến này mặc định. Thêm block define từ bước 4 hoặc bạn sẽ gặp lỗi runtime trong production build.
4. JSX trong file .js hoặc .ts bị lỗi. Theo mặc định Vite chỉ transform JSX trong .jsx và .tsx. Nếu code JSX nằm trong file .js, đổi tên hoặc cấu hình plugin:
react({ include: '**/*.{js,ts,jsx,tsx}' })
5. Thứ tự import CSS khác biệt. Vite không đảm bảo thứ tự CSS cross-module giống Webpack. Nếu bạn phụ thuộc vào cascade stylesheet theo thứ tự import qua các module riêng biệt, sẽ có lỗi hiển thị. Hãy khai báo độ ưu tiên CSS tường minh thay vì dựa vào thứ tự.
6. @vitejs/plugin-legacy báo lỗi mà không giải thích rõ khi thiếu Terser. Plugin yêu cầu Terser là peer dependency nhưng không nói rõ khi Terser chưa được cài — bạn chỉ thấy một lỗi build chung chung. Luôn chạy:
npm install -D @vitejs/plugin-legacy terser
So sánh hiệu năng
Từ benchmark ứng dụng React 200 component:
| Chỉ số | Webpack 5 | Vite 5 | Cải thiện |
|---|---|---|---|
| Cold start | 34.2s | 0.8s | 42× |
| HMR (component) | 2.8s | 50ms | 56× |
| HMR (CSS) | 1.2s | 20ms | 60× |
| Production build | 142s | 38s | 3.7× |
| Bộ nhớ (dev server) | 1.8 GB | 0.4 GB | 4.5× ít hơn |
Sự chênh lệch tốc độ dev mang tính cấu trúc. Webpack bundle toàn bộ module trước khi dev server phục vụ bất kỳ thứ gì. Vite phục vụ native ES module trực tiếp từ đĩa và chỉ pre-bundle npm dependency một lần. Với hot reload, Vite chỉ transform file thay đổi; Webpack phải duyệt lại toàn bộ dependency graph.
Mức cải thiện production build ít ấn tượng hơn (3.7×) nhưng vẫn đáng kể với các team tính tiền CI theo phút. Rolldown của Vite 8 được kỳ vọng mang lại thêm cải thiện trên codebase lớn khi bundler Rust-based này trưởng thành hơn.
Sau khi có build artifact, deploy lên Cloudflare Pages chỉ mất chưa đến một phút — output static của Vite đưa thẳng vào, không cần cấu hình server. Plugin Vite first-party của Cloudflare năm 2025 cũng xử lý Workers + Pages trong một pipeline build thống nhất.
Khi nào không nên migrate
Đừng migrate nếu dự án của bạn rơi vào các trường hợp sau:
Micro-frontend với Module Federation. @originjs/vite-plugin-federation tồn tại nhưng dev server vẫn còn nhiều điểm chưa ổn. Webpack Module Federation đã được kiểm chứng trong production cho các micro-frontend setup phức tạp hơn.
Bundle Electron main process. target: 'electron-main' của Webpack không có tương đương sạch trong Vite. Renderer process migrate được; main process thì không.
Multi-compiler setup. Nếu bạn chạy nhiều Webpack compiler riêng cho client, server, và Web Worker, Vite có thể xử lý các setup tương tự nhưng cần tái kiến trúc pipeline build, không chỉ đổi file config.
Hệ sinh thái loader tùy chỉnh phức tạp. Nếu build của bạn phụ thuộc vào Webpack loader không có Vite plugin tương đương — loader cho binary format tùy chỉnh, transform pipeline đặc thù — hãy tính thêm thời gian để viết replacement, nếu không sẽ bị block migration.
First page load cần 1,000+ module. Dev server của Vite fetch từng native ES module riêng qua mạng. Trên ứng dụng rất lớn, điều này có thể tạo ra hàng trăm request song song khi load lần đầu, chậm hơn một bundle đơn. Triệu chứng là cold start nhanh nhưng lần đầu mở trình duyệt vẫn chậm. Production build không bị ảnh hưởng — Rolldown bundle mọi thứ bình thường.
Tài liệu tham khảo
- Vite Getting Started
- Vite Migration Guide (v7 → v8)
- Vite Env & Mode
- Vite Asset Handling
- Vite Build Options
- @vitejs/plugin-legacy (GitHub)
- @originjs/vite-plugin-commonjs
- vite-tsconfig-paths
- Vitest Migration from Jest
- 200-component React benchmark (pockit.tools)
- 2024 State of JavaScript — Build Tools
- Webpack changelog (GitHub)
- Turbopack
- Module Federation for Vite