Giới thiệu
Trong môi trường SPA (Single Page Application) hiện đại, bundle size đóng vai trò quyết định tới trải nghiệm người dùng. Khi kích thước bundle vượt quá mức tối ưu, thời gian tải trang tăng, SEO giảm và tỉ lệ thoát (bounce rate) tăng mạnh. Bài viết sẽ phân tích sâu nguyên nhân gây bundle lớn trong dự án React + TypeScript và đưa ra các chiến lược thực tiễn để giảm kích thước, đồng thời duy trì khả năng mở rộng và bảo trì mã nguồn.
Hiểu vấn đề bundle size trong React TypeScript
React không tự tạo ra bundle; công cụ bundler (Webpack, Vite, esbuild…) sẽ thu thập toàn bộ mã nguồn và các phụ thuộc, sau đó thực hiện tree‑shaking, minification và code splitting. Khi cấu hình không tối ưu hoặc lựa chọn thư viện không phù hợp, quá trình này sẽ sinh ra các file JavaScript nặng nề.
Phân tích các nguyên nhân chính gây bundle lớn
1. Import toàn bộ thư viện thay vì import theo module
Nhiều lập trình viên vẫn dùng cú pháp import _ from 'lodash' hoặc import * as moment from 'moment'. Điều này buộc bundler phải đưa toàn bộ nội dung thư viện vào bundle, ngay cả khi chỉ sử dụng một vài hàm.
2. Thư viện phụ thuộc nặng
Một số thư viện như moment, lodash, antd có kích thước hàng trăm kilobyte. Khi chúng được import trực tiếp, kích thước bundle sẽ tăng đáng kể. Thay thế bằng các thư viện nhẹ hơn (ví dụ dayjs thay cho moment) là một cách giảm đáng kể.
3. Không kích hoạt tree‑shaking
Tree‑shaking phụ thuộc vào định dạng module (ESM) và cấu hình bundler. Nếu dự án vẫn sử dụng require() hoặc cấu hình module không đúng, bundler sẽ không thể loại bỏ các phần không dùng.
4. Thiếu code splitting
React hỗ trợ lazy loading các component qua React.lazy và Suspense. Khi toàn bộ ứng dụng được đóng gói thành một file duy nhất, người dùng phải tải toàn bộ mã ngay cả khi chỉ truy cập một trang con.
5. Đánh giá sai các polyfill
Trong môi trường trình duyệt cũ, chúng ta thường thêm polyfill cho Promise, fetch, Object.assign… Nếu không kiểm soát, các polyfill này sẽ được đưa vào bundle dù không cần thiết.
Chiến lược giảm bundle size
1. Sử dụng import theo module
Thay vì import _ from 'lodash', hãy import từng hàm cần thiết:
import debounce from 'lodash/debounce'; import throttle from 'lodash/throttle';
Với Webpack hoặc Vite, các import này sẽ được tree‑shaking và chỉ đưa các hàm đã dùng vào bundle.
2. Thay thế thư viện nặng bằng các giải pháp nhẹ
Ví dụ, thay moment bằng dayjs (khoảng 2 KB so với 70 KB):
import dayjs from 'dayjs';
const formatted = dayjs().format('YYYY-MM-DD');
3. Kích hoạt tree‑shaking và sử dụng ESM
Đảm bảo package.json của dự án và các phụ thuộc đều khai báo trường "module" hoặc "exports" cho ESM. Đối với Webpack, cấu hình mode: 'production' và optimization.usedExports: true sẽ tự động bật tree‑shaking.
4. Áp dụng code splitting với React.lazy và dynamic import
Ví dụ, lazy load một trang Dashboard:
import React, { Suspense, lazy } from 'react';
const Dashboard = lazy(() => import('./components/Dashboard'));
function App() {
return (
<Suspense fallback=<div>Loading...</div>>
<Dashboard />
</Suspense>
);
}
Webpack hoặc Vite sẽ tạo ra một chunk riêng cho Dashboard, chỉ tải khi người dùng thực sự truy cập.
5. Tối ưu cấu hình bundler
Với Vite, có thể cấu hình manualChunks để tách các thư viện lớn thành các chunk riêng:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0];
}
},
},
},
},
});
Điều này giúp cache các thư viện độc lập, giảm tải khi chỉ thay đổi code ứng dụng.
6. Loại bỏ polyfill không cần thiết
Sử dụng plugin babel-plugin-transform-runtime để tránh đưa toàn bộ polyfill vào bundle. Thay vào đó, chỉ chèn polyfill khi trình duyệt không hỗ trợ.
7. Sử dụng Babel plugin để giảm lodash
Plugin babel-plugin-lodash sẽ chuyển các import import { map } from 'lodash' thành import riêng lẻ, tương tự như cách thủ công ở mục 1.
Thực hành: tối ưu một dự án React TypeScript thực tế
Giả sử chúng ta có một dự án tạo bằng create-react-app với cấu trúc:
src/
components/
Header.tsx
Dashboard.tsx
Chart.tsx
pages/
Home.tsx
Profile.tsx
App.tsx
index.tsx
Ban đầu, App.tsx import toàn bộ các trang:
import Home from './pages/Home';
import Profile from './pages/Profile';
import Dashboard from './pages/Dashboard';
function App() {
return (
<Router>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/profile" component={Profile} />
<Route path="/dashboard" component={Dashboard} />
</Switch>
</Router>
);
}
Với cấu hình mặc định, Webpack sẽ tạo một bundle duy nhất chứa Home, Profile và Dashboard. Để giảm kích thước, chúng ta thực hiện:
- Chuyển các trang sang lazy load:
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const Profile = lazy(() => import('./pages/Profile'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Suspense fallback=<div>Loading...</div>>
<Router>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/profile" component={Profile} />
<Route path="/dashboard" component={Dashboard} />
</Switch>
</Router>
</Suspense>
);
}
Kết quả: Vite hoặc Webpack sẽ sinh ra 3 chunk riêng, mỗi chunk tải khi người dùng truy cập tương ứng.
- Thay thế
lodashbằnglodash-esvà import theo module:
import debounce from 'lodash-es/debounce';
Với lodash-es, bundler có thể tree‑shake tốt hơn vì thư viện đã được viết dưới dạng ES module.
- Thêm cấu hình manualChunks trong
vite.config.tsđể táchreactvàreact‑router‑dom:
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react')) return 'react';
if (id.includes('react-router-dom')) return 'router';
return 'vendor';
}
}
Sau khi build, chúng ta kiểm tra bằng vite preview và công cụ source-map-explorer để xác nhận kích thước mỗi chunk.
Kỹ thuật đo lường và kiểm tra bundle size
Để đánh giá hiệu quả, chúng ta sử dụng một trong các công cụ sau:
- webpack-bundle-analyzer: Hiển thị biểu đồ cây các module, giúp xác định phần nào chiếm nhiều dung lượng.
- source-map-explorer: Phân tích file
.js.mapđể xem chi tiết các phần tử trong bundle. - vite-plugin-visualizer: Tương tự webpack‑bundle‑analyzer nhưng dành cho Vite.
Ví dụ, chạy npx source-map-explorer dist/assets/*.js sẽ xuất ra một trang HTML với biểu đồ kích thước. Từ đó, chúng ta có thể quyết định loại bỏ hoặc thay thế các thư viện nặng.
Kết luận
Việc giảm bundle size không chỉ cải thiện tốc độ tải mà còn nâng cao trải nghiệm người dùng, hỗ trợ SEO và giảm chi phí băng thông. Áp dụng các chiến lược như import theo module, lazy loading, cấu hình manualChunks và thay thế thư viện nặng sẽ mang lại lợi ích rõ rệt. Tham khảo khóa học "Lập trình Front-End với ReactJS + TypeScript" tại đây.





