+3

Mình đã giúp một project enterprise React run dev nhanh hơn 5 lần như thế nào

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

image.png

  • main_app_1main_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à libcommon
  • libcommon dùng 2 module nhỏ hơn là componentsui_elements
  • components chứa những component "phức tạp" dựa trên những component "đơn giản" hơn trong module ui_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.

image.png

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.

image.png

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

image.png

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

image.png

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.

image.png

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.

image.png

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.

image.png

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.

image.png

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.

image.png

Đầ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 id root) ở trong thư mục ngoài cùng thay vì trong thư mục public

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

image.png

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:

image.png

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

image.png

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.

image.png

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.

image.png

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.

image.png

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

image.png

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!

rsbuild-benchmark-homepage.png

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

image.png


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí