TLDR: Đừng dùng Creact React App (CRA). Với project nhỏ, hãy dùng Vite, với project lớn, hãy sử dụng Rsbuild.
Đã bao giờ bạn gặp một project React to đến mức mỗi lần viết console.log(data)
bạn phải đợi 2-3 phút trước khi thấy data bạn cần debug hiển thị lên màn hình chưa? Đã bao giờ bạn phải mất 5 phút chỉ để ngồi đợi server dev start up? Nếu vậy có lẽ bạn sẽ thích bài viết này: mình đã làm thế nào để giúp một project React enterprise run dev nhanh hơn gấp 5 lần.
Đợi 30 phút mỗi lần save
Mình được tiếp xúc với một dự án React siêu to khổng lồ khoảng 10.000 - 20.000 file ở chỗ làm. Mình nhớ lần đầu tiên đợi server dev start up phải mất tới 30 phút. Project đó to đến mức hot reload trên máy tính mình còn không chạy (vì tràn RAM!). Vì thế mỗi lần save mình phải chạy lại server dev (npm run start
), tức là ngồi đợi khoảng 30 phút để thấy những gì mình code hiển thị lên màn hình.
Project React siêu to khổng lồ này là một monorepo bao gồm ít nhất 7 project nhỏ:
main_app_1
vàmain_app_2
: 2 phần chính của dự án- 2 phần chính này dùng module nhỏ hơn là
lib
vàcommon
lib
vàcommon
dùng 2 module nhỏ hơn làcomponents
vàui_elements
components
chứa những component "phức tạp" dựa trên những component "đơn giản" hơn trong moduleui_elements
- 6 module nhỏ này sẽ được import vào trong
container
dùng Create React App (CRA) để chạy cả dự án
Cách chạy project đó như thế này. Hãy tưởng tượng 6 project main_app_1
, main_app_2
, lib
, common
, components
, ui_elements
giống như một "thư viện", hay là một "package". Đầu tiên mình phải compile 6 "thư viện" đó, tiếp theo container
sẽ import (npm install
) 6 "thư viện" đó vào, rồi dùng Create React App để chạy.
Project này được viết bằng Typescript, vì thế nó sử dụng tsc
, một package đính kèm với Typescript để compile Typescript sang Javascript.
Mỗi lần mình chạy:
npm run build
tức là tương đương với:
tsc
để compile những file viết bằng Typescript sang Javascript, máy tính mình lúc bấy giờ mất khoảng 10-15 phút. Mỗi lần mình chạy:
npm run start
để chạy server dev, mình phải ngồi đợi thêm 5-10 phút nữa.
2 bottleneck chính: tsc và Create React App (CRA)
Nhìn vào quá trình build dev rồi run dev, mình thấy có 2 bottleneck chính: tsc
(Typescript compiler) và Create React App (CRA).
tsc
có nhiệm vụ compile file Typescript thành Javascript và generate ra những file .d.ts
để thông báo cho project khác những function hay component của project đó có type gì.
Ví dụ project main_app_1
import component CustomInput
từ project component
. Project component
sẽ đưa cho project main_app_1
file d.ts
để main_app_1
biết CustomInput
có những props gì. Tưởng tượng bạn gõ:
<CustomInput on
rồi đợi VSCode gợi ý có những props gì bắt đầu bằng "on", thì đó vai trò của file .d.ts
.
Tuy nhiên, trước khi generate ra những file .d.ts
này, tsc
sẽ phải vào từng file và đọc tất cả những type trong toàn project đó.
Nếu tsc
kiểm tra type cả tất cả 10.000 - 20.000 file, thì đúng là không ngạc nhiên nếu nó rất chậm và tốn rất nhiều RAM.
Tiếp đến là Create React App (CRA). Mỗi lần mình thay đổi 1 file là Create React App (CRA) sẽ bundle nguyên cả project 10.000 - 20.000 file thành 1 file main.js
lại từ đầu. Hiển nhiên là việc này mất cực kỳ nhiều thời gian và không được... hiệu quả lắm.
Thay tsc bằng swc
Đầu tiên, mình giải quyết vấn đề tsc
. Việc generate file .d.ts
là cần thiết nhưng nó chỉ cần thiết cho một project khác sử dụng project này.
Nhưng vì phần lớn thời gian mọi người chỉ làm việc trong 1 project, việc generate đi generate lại những file .d.ts
cho toàn bộ cả 6 project là không cần thiết. Khi làm việc trong 1 project, VSCode sẽ tự động "hiểu" những function và component trong project đó có type như thế nào (Typescript Language Server built-in). Đó là lý do bạn thường không phải viết phải viết file d.ts
cho chính mình và hiếm khi bạn gặp file này trừ khi bạn tự viết một thư viện.
Vậy có cách nào để trực tiếp compile Typescript thành Javascript mà không cần phải check type của toàn bộ 2.000 - 5.000 file không? Sau một chút search Google, mình tìm thấy swc
.
SWC cực kỳ nhanh. So với tsc
mất khoảng 5 - 10 phút để compile cả project từ Typescript sang Javascript, swc
mất khoảng... 10 giây. Trong khi tsc watch
máy mình còn không chạy nổi vì thiếu RAM và CPU thì swc
mất khoảng 100 - 200 ms
để compile!
SWC rất nhanh vì nó không check type cho cả project và cũng không generate ra file .d.ts
.
Type checking và đảm bảo code đúng type là điều tốt nhưng trong quá trình code mình không muốn đánh đổi mỗi lần save phải đợi 30 phút mới thấy thay đổi trên màn hình để đảm bảo type đã chính xác. Mỗi lần code xong trước khi tạo Pull Request / Merge Request mình mới chạy lại tsc
để đảm bảo tất cả những type mình viết là chính xác.
Tiếp đến là đến bottleneck Creact React App (CRA). So với việc thay thế tsc
bằng swc
thì việc này phức tạp hơn nhiều.
Thay CRA (Create React App) bằng Vite
Khi nhắc đến những giải pháp thay thế cho CRA (Create React App), có lẽ đầu tiên bạn sẽ nghĩ đến Vite. Mình cũng vậy. Vì thế đầu tiên mình thử thay thế CRA bằng Vite.
Đầu tiên, mình tạo một project React dùng Create React App nhỏ trên máy mình rồi thử thay thế nó bằng Vite. Mình khá bất ngờ khi thực ra nó khá đơn giản. Cơ bản việc này chỉ có 3 bước:
npm install
Vite và plugin React- Tạo file
vite.config.ts
với plugin React - Tạo file
index.html
(có div với idroot
) ở trong thư mục ngoài cùng thay vì trong thư mụcpublic
Sau khi thành công với project nhỏ, mình thử làm tương tự như vậy với project siêu to khổng lồ.
Sau khi đọc và hiểu sơ qua những config cần thiết cho project đó, mình config tương tự với Vite và cài những plugin tương tự. Tuy nhiên khi chạy npm run dev
, mình gặp lỗi này:
Error: COMMITHASH is undefined
Tại sao lại thế? Vì project này sử dụng git-revision-webpack-plugin
nên mình cài plugin tương tự phía Vite là vite-plugin-git-revision
. Tuy nhiên 2 plugin này lại có những khác biệt nhỏ. Ở git-revision-webpack-plugin
dùng biến global COMMITHASH, thì ở vite-plugin-git-revision
lại dùng biến global GITCOMMITHASH.
Mình không muốn thay đổi source code mà chỉ muốn thay đổi config, nên mình tìm đọc thử documentation của vite-plugin-git-revision
. Tuy nhiên sau khi nhận ra plugin này thực ra khá đơn giản, mình tự viết một "plugin" nhỏ trong config của mình.
Đây là ý tưởng của mình:
Đây là config demo ở trong vite.config.ts
:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc'
import { exec } from "child_process"; const getCommitHash = new Promise<string>((resolve, reject) => { exec("git rev-parse HEAD", (err, stdout) => { if (err) { reject(err); } else { resolve(stdout); } });
}); const getVersion = new Promise<string>((resolve, reject) => { exec("git describe --always", (err, stdout) => { if (err) { reject(err); } else { resolve(stdout); } });
}); const getBranch = new Promise<string>((resolve, reject) => { exec("git rev-parse --abbrev-ref HEAD", (err, stdout) => { if (err) { reject(err); } else { resolve(stdout); } });
}); const getLastCommitDateTime = new Promise<string>((resolve, reject) => { exec("git log -1 --format=%cI", (err, stdout) => { if (err) { reject(err); } else { resolve(stdout); } });
}); export default defineConfig(async () => { const [commitHash, version, branch, lastCommitDateTime] = await Promise.all([ getCommitHash, getVersion, getBranch, getLastCommitDateTime, ]); return { plugins: [react()], source: { define: { VERSION: JSON.stringify(version), LASTCOMMITDATETIME: JSON.stringify(lastCommitDateTime), BRANCH: JSON.stringify(branch), COMMITHASH: JSON.stringify(commitHash), }, }, };
});
Nhưng lần này mình vẫn tiếp tục gặp:
Error: process.env is undefined
Tại sao lại thế? Hóa là với Vite, mọi biến environment phải bắt đầu VITE_
. Ví dụ, nếu Create React App bạn dùng biến environment với tên DEFAULT_LANGUAGE
, thì trong Vite nó phải là VITE_DEFAULT_LANGUAGE
. Nhưng còn nữa, trong khi CRA dùng process.env
, thì Vite lại dùng import.meta
. Tức là thay vì dùng process.env.DEFAULT_LANGUAGE
thì mình phải dùng import.meta.VITE_DEFAULT_LANGUAGE
.
Tất nhiên mình không muốn vào từng file trong cả project tìm process.env
rồi thay đổi bằng import.meta
. Vì thế sau một hồi search Google, mình tìm thấy giải pháp ở bài viết này:
import { defineConfig, loadEnv } from 'vite'; export default defineConfig(({ command, mode }) => { const env = loadEnv(mode, process.cwd(), ''); return { define: { 'process.env.YOUR_STRING_VARIABLE': JSON.stringify(env.YOUR_STRING_VARIABLE), 'process.env.YOUR_BOOLEAN_VARIABLE': env.YOUR_BOOLEAN_VARIABLE, // If you want to exposes all env variables, which is not recommended // 'process.env': env }, };
});
Thay vì dùng process.env
, thì với config này, Vite sẽ tìm và thay thế tất cả những string là process.env.YOUR_STRING_VARIABLE
bằng giá trị mà bạn chọn. Ví dụ, nếu trong code mình có process.env.DEFAULT_LANGUAGE
, Vite sẽ tìm và thay thế tất cả những string đó bằng "en
" chẳng hạn.
Sau khi cẩn thận tìm kiếm và sử dụng cách trên cho tất cả những global variable có trong project, khi mình chạy npm run dev
cuối cùng server dev cũng lên!
So với CRA server dev mất khoảng 10 phút chỉ để khởi động lên, server dev của Vite chạy sẵn sàng ở http://localhost:5173
gần như ngay lập tức. Mặc dù vậy mỗi lần mình gõ URL vào trình duyệt, mình vẫn phải đợi gần 5 phút để trang web bắt đầu hiển thị lên. Dù như thế là khá lâu, nhưng quả thực với việc đó cũng không... nằm ngoài dự đoán với một project có khoảng 10.000 - 20.000 file.
Tuy nhiên khi mình chỉnh sửa file rồi save, những thay đổi mình viết không hiển thị lên màn hình! Vite HMR không hoạt động. Mình lại phải stop, restart lại server, rồi ngồi đợi 5 phút.
Mình thử cấu hình tương tự với project nhỏ của mình nhưng Vite vẫn hoạt động bình thường. Có vẻ như Vite HMR chỉ không hoạt động khi gặp project React siêu to khổng lồ đến mức này.
Thay vì phải đợi 30 phút thì giờ xuống 5 phút đã là một bước tiến đáng kể, nhưng mình vẫn muốn tìm cách để làm quá trình dev còn nhanh hơn nữa.
Sau khi search Google "Vite alternative", mình tìm thấy Rsbuild.
Thay Vite bằng Rsbuild
Ngay từ đầu Rsbuild đã được thiết kế có config giống và có thể dễ dàng thay thế cho những giải pháp dựa trên Webpack như Create React App (CRA),... Đúng như vậy, có nhiều Webpack plugin có thể dùng được trực tiếp trong Rsbuild. Ví dụ như plugin git-revision-webpack-plugin
mà mình tốn mấy tiếng đồng hồ để tìm documentation, đọc hiểu sourcecode rồi tạo lại mà có thể dùng được luôn trong rsbuild.config.ts
.
Khác với CRA và giống với Vite, Rsbuild cũng sử dụng import.meta
thay vì process.env
. Nhưng giải pháp cũng tương tự như vậy: thêm vào trong source.define
ở rsbuild.config.ts
và Rsbuild sẽ thay thế tất cả những string process.env....
bằng giá trị mình lựa chọn.
Sau khi config xong, mình chạy npm run dev
và đợi khoảng 30 giây, server dev bắt đầu bật ở http://localhost:3000
. Mình gõ URL và trang web gần như hiển thị ngay lập tức. Thật là khả quan.
Tiếp đó mình thêm console.log('hi there')
và hồi hộp đợi. Liệu lần này HMR (Hot module reload) có chạy không?
Khoảng 4 giây sau, hi there
hiển thị console. Mình thêm <div>Hi there</div>
, và chỉ khoảng 5 giây sau, "Hi there" hiển thị trên màn hình.
Thật kỳ diệu! So với Creact React App (CRA) phải mất 30 phút mới lên và thường xuyên chết vì tràn RAM, Vite chỉ mất 5 phút nhưng HMR (Hot module reload) không chạy, Rsbuild chỉ mất 20 - 30 giây để chạy server và HMR chỉ mất có 5 giây cảm giác như tà thuật ma giáo được bí truyền từ 20 đời nay lại vậy.
Theo như benchmark ở trang chủ, Rsbuild nhanh hơn Vite + SWC khoảng 3 lần. Mới đầu thì mình cũng có hơi... không tin lắm. Nhưng trong project này Rsbuild không chỉ nhanh hơn Vite gấp 3 lần, nếu tính đến việc HRM của Vite không chạy, thì với một lập trình viên như mình nó phải nhanh hơn gấp từ 10 - 20 lần khi phải stop rồi restart lại dev server của Vite!
Kết luận
Và đó là quá trình mình giúp những thành viên trong dự án tiết kiệm nhiều thời gian và effort bằng cách giúp run dev nhanh hơn gấp 5 - 10 lần.
Đừng sử dụng Create React App, nó thậm chí còn không được recommend ở documentation chính thức của React nữa. Nếu bạn không có nhu cầu sử dụng Server Side Rendering, với project nhỏ, hãy sử dụng Vite, với những project React lớn, hãy sử dụng Rsbuild.
Credits
Nếu bạn thích con cá mà mình sử dụng, hãy xem: https://thenounproject.com/browse/collection-icon/stripe-emotions-106667/.