Các dự án được xây dựng bằng các khuôn khổ dựa trên JavaScript thường cung cấp các gói JavaScript lớn mất nhiều thời gian để tải xuống, phân tích cú pháp và thực thi, chặn quá trình hiển thị trang và đầu vào của người dùng trong quá trình này. Vấn đề này rõ ràng hơn trên các mạng không đáng tin cậy và chậm cũng như các thiết bị cấp thấp. Trong bài viết này, chúng tôi sẽ đề cập đến các biện pháp thực hành tốt nhất về phân chia mã và giới thiệu một số ví dụ sử dụng React, do đó chúng tôi tải JavaScript tối thiểu cần thiết để hiển thị một trang và tải động các gói không quan trọng có kích thước lớn.
Các khuôn khổ dựa trên JavaScript như React đã giúp quá trình phát triển các ứng dụng web trở nên hợp lý và hiệu quả, dù tốt hay xấu. Quá trình tự động hóa này thường khiến các nhà phát triển coi khuôn khổ và các công cụ xây dựng là một hộp đen. Một quan niệm sai lầm phổ biến là mã được tạo ra bởi các công cụ xây dựng khung (ví dụ: Webpack) được tối ưu hóa hoàn toàn và không thể cải thiện thêm nữa.
Mặc dù các gói JavaScript cuối cùng được tree-shaken và thu nhỏ, nhưng thường thì toàn bộ ứng dụng web được chứa trong một hoặc chỉ một vài tệp JavaScript, tùy thuộc vào cấu hình dự án và các tính năng khung có sẵn. Có vấn đề gì nếu bản thân tệp được thu nhỏ và tối ưu hóa?

Ví dụ, khi người dùng truy cập vào trang chủ, toàn bộ gói
Làm thế nào chúng tôi có thể đảm bảo rằng người dùng tải JavaScript tối thiểu cần thiết để hiển thị trang mà họ đang truy cập? Ngoài ra, chúng tôi cũng cần đảm bảo rằng các gói cho các phần bị hạn chế của trang chỉ được tải bởi những người dùng được ủy quyền. Câu trả lời nằm ở phân tách mã.
Trước khi đi sâu vào chi tiết về phân tách mã, chúng ta hãy nhanh chóng nhắc lại những gì khiến JavaScript có tác động lớn đến hiệu suất tổng thể.
Giống như bất kỳ tệp nào được tham chiếu và sử dụng trên trang web, trước tiên tệp đó cần được tải xuống từ máy chủ. Tốc độ tải xuống của tệp phụ thuộc vào tốc độ kết nối và kích thước của chính tệp đó. Người dùng có thể duyệt Internet bằng các mạng chậm và không đáng tin cậy, do đó, việc thu nhỏ, tối ưu hóa và phân tách mã của các tệp JavaScript đảm bảo rằng người dùng tải xuống tệp nhỏ nhất có thể.

Không giống như tệp hình ảnh, ví dụ, chỉ cần được hiển thị sau khi tệp đã được tải xuống, các tệp JavaScript cần được phân tích cú pháp, biên dịch và thực thi. Đây là một hoạt động sử dụng nhiều CPU, chặn luồng chính khiến trang không phản hồi trong thời gian đó. Người dùng không thể tương tác với trang trong giai đoạn đó mặc dù nội dung có thể được hiển thị và dường như đã tải xong. Nếu tập lệnh mất quá nhiều thời gian để phân tích cú pháp và thực thi, người dùng sẽ có ấn tượng rằng trang web bị hỏng và rời đi. Đây là lý do tại sao Lighthouse và Core Web Vitals chỉ định các số liệu Độ trễ đầu vào đầu tiên (FID) và Tổng thời gian chặn (TBT) để đo lường tính tương tác của trang web và khả năng phản hồi đầu vào.
JavaScript cũng là một tài nguyên chặn hiển thị, nghĩa là nếu trình duyệt gặp phải một tập lệnh trong tài liệu HTML không bị trì hoãn, trình duyệt sẽ không hiển thị trang cho đến khi tải và thực thi tập lệnh đó. Các thuộc tính HTML
Hiệu suất của trang web không nhất quán trên các thiết bị. Có rất nhiều thiết bị trên thị trường với thông số kỹ thuật CPU và bộ nhớ khác nhau, vì vậy không có gì ngạc nhiên khi sự khác biệt về thời gian thực thi JavaScript giữa các thiết bị cao cấp và thiết bị trung bình là rất lớn.

Để đáp ứng nhiều thông số kỹ thuật thiết bị và loại mạng khác nhau, chúng tôi nên chỉ cung cấp mã quan trọng. Đối với các ứng dụng web dựa trên JavaScript, điều này có nghĩa là chỉ nên tải mã được sử dụng trên trang cụ thể đó, vì việc tải toàn bộ gói ứng dụng cùng một lúc có thể dẫn đến thời gian thực thi lâu hơn và đối với người dùng, thời gian chờ lâu hơn cho đến khi trang có thể sử dụng được và phản hồi với dữ liệu đầu vào.
Khi người dùng lần đầu truy cập trang chủ, gói nhà cung cấp chính chứa khung và các phụ thuộc được chia sẻ khác sẽ được tải cùng với gói cho trang chủ. Người dùng nhấp vào nút bật tắt chế độ tạo tài khoản. Khi người dùng tương tác với các đầu vào, thư viện kiểm tra độ mạnh mật khẩu đắt tiền được tải động. Khi người dùng tạo tài khoản và đăng nhập thành công, họ sẽ được chuyển hướng đến bảng điều khiển và chỉ khi đó, gói bảng điều khiển mới được tải. Điều quan trọng cần lưu ý là người dùng cụ thể này không có vai trò quản trị viên trên ứng dụng web, vì vậy gói quản trị viên không được tải.

Trong ví dụ sau, chúng ta có một bài đăng trên blog có phần bình luận. Chúng tôi muốn khuyến khích độc giả tạo tài khoản và để lại bình luận, vì vậy chúng tôi cung cấp một cách nhanh chóng để tạo tài khoản và bắt đầu bình luận bằng cách hiển thị biểu mẫu bên cạnh phần bình luận nếu họ chưa đăng nhập.

Biểu mẫu này sử dụng thư viện

Hãy cùng xem thành phần
Chúng tôi đang nhập trực tiếp thư viện

Chúng ta có thể đi đến kết luận tương tự khi xem đầu ra của Webpack Bundle Analyzer cho ứng dụng. Hình chữ nhật hẹp ở phía bên phải là thành phần bài đăng trên blog của chúng tôi.

Kiểm tra mật khẩu không phải là chức năng quan trọng đối với việc hiển thị trang. Chức năng của nó chỉ được yêu cầu khi người dùng tương tác với dữ liệu nhập mật khẩu. Vì vậy, hãy chia nhỏ
Chúng ta hãy xem ứng dụng của chúng ta hoạt động như thế nào sau khi chúng ta chuyển thư viện sang nhập động.
[*]
Như chúng ta có thể thấy từ video, tải trang ban đầu là khoảng 45kB chỉ bao gồm các phụ thuộc của khung và các thành phần trang bài đăng trên blog. Đây là trường hợp lý tưởng vì người dùng có thể nhận được nội dung nhanh hơn nhiều, đặc biệt là những người sử dụng kết nối mạng chậm hơn.
Khi người dùng bắt đầu nhập mật khẩu, chúng ta có thể thấy gói cho thư viện
Chúng tôi cũng có thể xác nhận rằng thư viện đã được chia mã thành một gói riêng biệt bằng cách kiểm tra đầu ra của Webpack Bundle Analyzer.


Chúng ta hãy xem thành phần
Đây là cách khá chuẩn mà hầu như bất kỳ ai cũng có thể tạo ra ứng dụng này. Hãy chạy Webpack Bundle Analyzer và xem các bundle trông như thế nào.

Giống như trong ví dụ trước, toàn bộ ứng dụng được tải trong một gói JavaScript duy nhất và
Điều đầu tiên chúng ta cần lưu ý là cửa sổ bật lên
Tiếp theo, chúng ta cần kiểm tra xem
Chúng ta hãy thay đổi thành phần
Đầu tiên, chúng ta cần xóa câu lệnh
Điều quan trọng cần lưu ý là

Mỗi thành phần trang đó đều có một export mặc định và hiện đang được import theo cách mặc định không lười biếng cho ví dụ này.
Như chúng tôi đã kết luận, các thành phần này được bao gồm trong gói chính theo mặc định (tùy thuộc vào khuôn khổ và công cụ xây dựng) có nghĩa là mọi thứ đều được tải bất kể người dùng truy cập vào tuyến đường nào. Cả thành phần Dashboard và About đều được tải trên tuyến đường trang chủ, v.v.

Hãy cấu trúc lại các câu lệnh
Và thế là xong! Các thành phần trang được chia thành các gói riêng biệt và được tải theo yêu cầu khi người dùng điều hướng giữa các trang. Hãy nhớ rằng bạn có thể cung cấp một thành phần dự phòng như trình quay vòng hoặc trình tải bộ xương để mang lại trải nghiệm tải tốt hơn trên các mạng chậm hơn và các thiết bị trung bình đến thấp.

[*]
Bạn có thể đã có một số ý tưởng về cách chọn đúng thành phần để chia mã từ các ví dụ mà chúng tôi đã đề cập. Sau đây là một tiêu chí cơ bản tốt cần tuân theo khi chọn ứng viên tiềm năng để chia mã:

Lúc đầu, việc được giao nhiệm vụ tối ưu hóa hiệu suất của toàn bộ ứng dụng web có thể hơi khó khăn. Một nơi tốt để bắt đầu là kiểm tra ứng dụng bằng Webpack Bundle Analyzer hoặc Source Map Explorer và xác định các gói cần được chia nhỏ mã và phù hợp với các tiêu chí đã đề cập ở trên. Một cách khác để xác định các gói đó là chạy kiểm tra hiệu suất trong trình duyệt hoặc sử dụng WebPageTest và kiểm tra xem gói nào chặn luồng chính của CPU lâu nhất.
Sau khi xác định các ứng viên phân chia mã, chúng ta cần kiểm tra phạm vi thay đổi cần thiết để phân chia mã thành phần này khỏi gói chính. Tại thời điểm này, chúng ta cần đánh giá xem lợi ích của việc phân chia mã có lớn hơn phạm vi thay đổi cần thiết và thời gian đầu tư cho phát triển và thử nghiệm hay không. Rủi ro này là tối thiểu hoặc không có trong giai đoạn đầu của chu kỳ phát triển.
Cuối cùng, chúng ta cần xác minh rằng thành phần đã được phân chia mã đúng cách và kích thước gói chính đã giảm. Chúng ta cũng cần xây dựng và thử nghiệm thành phần để tránh đưa ra các vấn đề tiềm ẩn.
Có rất nhiều bước để phân chia mã cho một thành phần hiện có, vì vậy chúng ta hãy tóm tắt các bước trong danh sách kiểm tra nhanh:

Chúng ta có thể tinh chỉnh ngân sách hiệu suất để phù hợp với các tình huống người dùng tệ nhất có thể xảy ra và sử dụng điều đó làm cơ sở để tối ưu hóa hiệu suất. Ví dụ: nếu chúng ta sử dụng kịch bản người dùng duyệt trang web trên kết nối không đáng tin cậy và chậm trên điện thoại trung bình có CPU chậm hơn làm cơ sở, chúng ta có thể cung cấp trải nghiệm người dùng tối ưu cho nhiều loại thiết bị và loại mạng của người dùng hơn nhiều.
Alex Russell đã đề cập đến chủ đề này rất chi tiết trong bài viết của ông về chủ đề ngân sách hiệu suất web trong thế giới thực và phát hiện ra rằng quy mô ngân sách tối ưu cho những tình huống xấu nhất nằm ở đâu đó giữa 130kB và 170kB.
Một cách để giải quyết là kiểm tra xem mã có đang chạy trên trình duyệt hay không, đây là giải pháp đơn giản, nếu không muốn nói là hơi phức tạp.
Tuy nhiên, giải pháp này còn lâu mới hoàn hảo. Nội dung sẽ không được hiển thị ở phía máy chủ, điều này hoàn toàn ổn đối với các hộp thoại và nội dung không cần thiết khác. Thông thường, khi chúng ta sử dụng SSR, mục đích là để cải thiện hiệu suất và SEO, vì vậy chúng ta muốn các thành phần giàu nội dung được hiển thị thành HTML, do đó trình thu thập thông tin có thể phân tích chúng để cải thiện thứ hạng kết quả tìm kiếm.
Cho đến khi React phiên bản 18 được phát hành, nhóm React khuyên bạn nên sử dụng thư viện các thành phần Loadable Components cho trường hợp chính xác này. Plugin này mở rộng các thành phần
Tuy nhiên, chia tách mã có thể dễ dàng bị lạm dụng và các nhà phát triển có thể trở nên quá nhiệt tình và tạo quá nhiều gói nhỏ gây hại cho khả năng sử dụng và hiệu suất. Tải động quá nhiều thành phần nhỏ hơn và không liên quan có thể khiến giao diện người dùng có cảm giác không phản hồi và bị chậm trễ, gây hại cho trải nghiệm người dùng nói chung. Việc chia tách mã quá mức thậm chí có thể gây hại cho hiệu suất trong trường hợp các gói được phục vụ qua HTTP 1.1 không có đa kênh.
Các khuôn khổ dựa trên JavaScript như React đã giúp quá trình phát triển các ứng dụng web trở nên hợp lý và hiệu quả, dù tốt hay xấu. Quá trình tự động hóa này thường khiến các nhà phát triển coi khuôn khổ và các công cụ xây dựng là một hộp đen. Một quan niệm sai lầm phổ biến là mã được tạo ra bởi các công cụ xây dựng khung (ví dụ: Webpack) được tối ưu hóa hoàn toàn và không thể cải thiện thêm nữa.
Mặc dù các gói JavaScript cuối cùng được tree-shaken và thu nhỏ, nhưng thường thì toàn bộ ứng dụng web được chứa trong một hoặc chỉ một vài tệp JavaScript, tùy thuộc vào cấu hình dự án và các tính năng khung có sẵn. Có vấn đề gì nếu bản thân tệp được thu nhỏ và tối ưu hóa?
Bundling Pitfalls
Chúng ta hãy xem một ví dụ đơn giản. Gói JavaScript cho ứng dụng web của chúng ta bao gồm sáu trang sau được chứa trong các thành phần riêng lẻ. Thông thường, các thành phần đó bao gồm nhiều thành phần phụ và các mục nhập khác, nhưng chúng tôi sẽ giữ nguyên để rõ ràng hơn.- Bốn trang công khai
Có thể truy cập ngay cả khi không đăng nhập (trang chủ, trang đăng nhập, trang đăng ký và trang hồ sơ). - Một trang riêng tư
Có thể truy cập bằng cách đăng nhập (trang bảng điều khiển). - Một trang bị hạn chế
Đây là trang quản trị có tổng quan về tất cả hoạt động, tài khoản và phân tích của người dùng (trang quản trị).

Ví dụ, khi người dùng truy cập vào trang chủ, toàn bộ gói
app.min.js
có mã cho các trang khác sẽ được tải và phân tích cú pháp, nghĩa là chỉ một phần của gói đó được sử dụng và hiển thị trên trang. Điều này nghe có vẻ không hiệu quả, phải không? Ngoài ra, tất cả người dùng đang tải một phần bị hạn chế của ứng dụng mà chỉ một số ít người dùng có thể truy cập — trang quản trị. Mặc dù mã được làm tối nghĩa một phần như một phần của quá trình thu nhỏ, chúng tôi có nguy cơ làm lộ các điểm cuối API hoặc dữ liệu khác dành riêng cho người dùng quản trị.Làm thế nào chúng tôi có thể đảm bảo rằng người dùng tải JavaScript tối thiểu cần thiết để hiển thị trang mà họ đang truy cập? Ngoài ra, chúng tôi cũng cần đảm bảo rằng các gói cho các phần bị hạn chế của trang chỉ được tải bởi những người dùng được ủy quyền. Câu trả lời nằm ở phân tách mã.
Trước khi đi sâu vào chi tiết về phân tách mã, chúng ta hãy nhanh chóng nhắc lại những gì khiến JavaScript có tác động lớn đến hiệu suất tổng thể.
Chi phí hiệu suất
Ảnh hưởng của JavaScript đến hiệu suất bao gồm chi phí tải xuống, phân tích cú pháp và thực thi.Giống như bất kỳ tệp nào được tham chiếu và sử dụng trên trang web, trước tiên tệp đó cần được tải xuống từ máy chủ. Tốc độ tải xuống của tệp phụ thuộc vào tốc độ kết nối và kích thước của chính tệp đó. Người dùng có thể duyệt Internet bằng các mạng chậm và không đáng tin cậy, do đó, việc thu nhỏ, tối ưu hóa và phân tách mã của các tệp JavaScript đảm bảo rằng người dùng tải xuống tệp nhỏ nhất có thể.

Không giống như tệp hình ảnh, ví dụ, chỉ cần được hiển thị sau khi tệp đã được tải xuống, các tệp JavaScript cần được phân tích cú pháp, biên dịch và thực thi. Đây là một hoạt động sử dụng nhiều CPU, chặn luồng chính khiến trang không phản hồi trong thời gian đó. Người dùng không thể tương tác với trang trong giai đoạn đó mặc dù nội dung có thể được hiển thị và dường như đã tải xong. Nếu tập lệnh mất quá nhiều thời gian để phân tích cú pháp và thực thi, người dùng sẽ có ấn tượng rằng trang web bị hỏng và rời đi. Đây là lý do tại sao Lighthouse và Core Web Vitals chỉ định các số liệu Độ trễ đầu vào đầu tiên (FID) và Tổng thời gian chặn (TBT) để đo lường tính tương tác của trang web và khả năng phản hồi đầu vào.
JavaScript cũng là một tài nguyên chặn hiển thị, nghĩa là nếu trình duyệt gặp phải một tập lệnh trong tài liệu HTML không bị trì hoãn, trình duyệt sẽ không hiển thị trang cho đến khi tải và thực thi tập lệnh đó. Các thuộc tính HTML
async
và defer
báo hiệu cho trình duyệt không chặn quá trình xử lý trang, tuy nhiên, luồng CPU vẫn bị chặn và tập lệnh cần được thực thi trước khi trang phản hồi với dữ liệu nhập của người dùng.Hiệu suất của trang web không nhất quán trên các thiết bị. Có rất nhiều thiết bị trên thị trường với thông số kỹ thuật CPU và bộ nhớ khác nhau, vì vậy không có gì ngạc nhiên khi sự khác biệt về thời gian thực thi JavaScript giữa các thiết bị cao cấp và thiết bị trung bình là rất lớn.

Để đáp ứng nhiều thông số kỹ thuật thiết bị và loại mạng khác nhau, chúng tôi nên chỉ cung cấp mã quan trọng. Đối với các ứng dụng web dựa trên JavaScript, điều này có nghĩa là chỉ nên tải mã được sử dụng trên trang cụ thể đó, vì việc tải toàn bộ gói ứng dụng cùng một lúc có thể dẫn đến thời gian thực thi lâu hơn và đối với người dùng, thời gian chờ lâu hơn cho đến khi trang có thể sử dụng được và phản hồi với dữ liệu đầu vào.
Chia mã
Với việc chia mã, mục tiêu của chúng tôi là trì hoãn việc tải, phân tích cú pháp và thực thi mã JavaScript không cần thiết cho trang hoặc trạng thái hiện tại. Đối với ví dụ của chúng tôi, điều đó có nghĩa là các trang riêng lẻ sẽ được chia thành các gói tương ứng của chúng —homepage.min.js
, login.min.js
, dashboard.min.js
, v.v.Khi người dùng lần đầu truy cập trang chủ, gói nhà cung cấp chính chứa khung và các phụ thuộc được chia sẻ khác sẽ được tải cùng với gói cho trang chủ. Người dùng nhấp vào nút bật tắt chế độ tạo tài khoản. Khi người dùng tương tác với các đầu vào, thư viện kiểm tra độ mạnh mật khẩu đắt tiền được tải động. Khi người dùng tạo tài khoản và đăng nhập thành công, họ sẽ được chuyển hướng đến bảng điều khiển và chỉ khi đó, gói bảng điều khiển mới được tải. Điều quan trọng cần lưu ý là người dùng cụ thể này không có vai trò quản trị viên trên ứng dụng web, vì vậy gói quản trị viên không được tải.

Nhập động và phân tách mã trong React
Phân tách mã có sẵn cho Create React App và các nền tảng khác sử dụng Webpack như Gatsby và Next.js. Nếu bạn đã thiết lập dự án React theo cách thủ công hoặc nếu bạn đang sử dụng một khuôn khổ không có chức năng phân tách mã được cấu hình sẵn, bạn sẽ phải tham khảo tài liệu Webpack hoặc tài liệu cho công cụ xây dựng mà bạn đang sử dụng.Hàm
Trước khi đi sâu vào việc phân chia mã thành phần React, chúng ta cũng cần đề cập rằng chúng ta cũng có thể mã hóa các hàm phân chia trong React bằng cách nhập động chúng. Nhập động là JavaScript thuần túy, vì vậy cách tiếp cận này sẽ hoạt động với tất cả các khung. Tuy nhiên, hãy nhớ rằng cú pháp này không được hỗ trợ bởi các trình duyệt cũ như Internet Explorer và Opera Mini.
Mã:
import("path/to/myFunction.js").then((myFunction) => { /* ... */});

Biểu mẫu này sử dụng thư viện
zxcvbn
có kích thước khá lớn là 800kB để kiểm tra độ mạnh của mật khẩu, điều này có thể gây ra vấn đề về hiệu suất, do đó, đây là ứng cử viên phù hợp cho việc phân tách mã. Đây chính xác là tình huống mà tôi đã xử lý vào năm ngoái và chúng tôi đã đạt được hiệu suất tăng đáng kể bằng cách chia mã thư viện này thành một gói riêng biệt và tải nó một cách động.
Hãy cùng xem thành phần
Comments.jsx
trông như thế nào.
Mã:
import React, { useState } from "react";import zxcvbn from "zxcvbn"; /* Chúng ta đang nhập trực tiếp lib */export const Comments = () => { const [password, setPassword] = useState(""); const [passwordStrength, setPasswordStrength] = useState(0); const onPasswordChange = (event) => { const { value } = event.target; const { score } = zxcvbn(value) setPassword(value); setPasswordStrength(score); }; return ( {/* ... */} Độ mạnh mật khẩu: {passwordStrength} {/* ... */} );};
zxcvbn
và kết quả là nó được đưa vào gói chính. Gói thu nhỏ kết quả cho thành phần bài đăng blog nhỏ của chúng tôi là một gói nén 442kB khổng lồ! Thư viện React và trang bài đăng blog này hầu như không đạt đến 45kB khi nén, vì vậy chúng tôi đã làm chậm đáng kể quá trình tải ban đầu của trang này bằng cách tải ngay thư viện kiểm tra mật khẩu này.
Chúng ta có thể đi đến kết luận tương tự khi xem đầu ra của Webpack Bundle Analyzer cho ứng dụng. Hình chữ nhật hẹp ở phía bên phải là thành phần bài đăng trên blog của chúng tôi.

Kiểm tra mật khẩu không phải là chức năng quan trọng đối với việc hiển thị trang. Chức năng của nó chỉ được yêu cầu khi người dùng tương tác với dữ liệu nhập mật khẩu. Vì vậy, hãy chia nhỏ
zxcvbn
thành một gói riêng biệt, nhập động và chỉ tải khi giá trị nhập mật khẩu thay đổi, tức là khi người dùng bắt đầu nhập mật khẩu. Chúng ta cần xóa câu lệnh import
và thêm câu lệnh import động vào hàm xử lý sự kiện onChange
mật khẩu.
Mã:
import React, { useState } from "react";export const Comments = () => { /* ... */ const onPasswordChange = (event) => { const { value } = event.target; setPassword(value); /* Nhập động - đổi tên nhập mặc định thành tên lib để rõ ràng hơn */ import("zxcvbn").then(({default: zxcvbn}) => { const { score } = zxcvbn(value); setPasswordStrength(score); }); }; /* ... */}
[*]
Như chúng ta có thể thấy từ video, tải trang ban đầu là khoảng 45kB chỉ bao gồm các phụ thuộc của khung và các thành phần trang bài đăng trên blog. Đây là trường hợp lý tưởng vì người dùng có thể nhận được nội dung nhanh hơn nhiều, đặc biệt là những người sử dụng kết nối mạng chậm hơn.
Khi người dùng bắt đầu nhập mật khẩu, chúng ta có thể thấy gói cho thư viện
zxcvbn
xuất hiện trong tab mạng và kết quả của hàm đang chạy được hiển thị bên dưới đầu vào. Mặc dù quy trình này lặp lại ở mỗi lần nhấn phím, tệp chỉ được yêu cầu một lần và chạy ngay lập tức khi có sẵn.Chúng tôi cũng có thể xác nhận rằng thư viện đã được chia mã thành một gói riêng biệt bằng cách kiểm tra đầu ra của Webpack Bundle Analyzer.

Các thành phần React của bên thứ ba
Phân chia mã Các thành phần React rất đơn giản trong hầu hết các trường hợp và bao gồm bốn bước sau:- sử dụng lệnh xuất mặc định cho thành phần mà chúng ta muốn phân chia mã;
- nhập thành phần với
React.lazy
; - hiển thị thành phần như một thành phần con của
React.Suspense
; - cung cấp một thành phần dự phòng cho
React.Suspense
.
react-calendar
làm thư viện mà chúng tôi sẽ sử dụng.
Chúng ta hãy xem thành phần
DatePicker
. Chúng ta có thể thấy rằng thành phần Calendar
từ gói react-calendar
đang được hiển thị có điều kiện khi người dùng tập trung vào phần tử đầu vào ngày.
Mã:
import React, { useState } from "react";nhập Lịch từ "react-calendar";export const DatePicker = () => { const [showModal, setShowModal] = useState(false); const handleDateChange = (ngày) => { setShowModal(false); }; const handleFocus = () => setShowModal(true); return ( Ngày sinh {showModal && } );};

Giống như trong ví dụ trước, toàn bộ ứng dụng được tải trong một gói JavaScript duy nhất và
react-calendar
chiếm một phần đáng kể trong đó. Hãy xem liệu chúng ta có thể chia nhỏ mã hay không.Điều đầu tiên chúng ta cần lưu ý là cửa sổ bật lên
Calendar
được tải có điều kiện, chỉ khi trạng thái showModal
được đặt. Điều này khiến thành phần Calendar
trở thành ứng cử viên sáng giá cho việc tách mã.Tiếp theo, chúng ta cần kiểm tra xem
Calendar
có phải là xuất mặc định hay không. Trong trường hợp của chúng ta, thì là xuất.
Mã:
import Calendar from "react-calendar"; /* Nhập chuẩn */
DatePicker
để tải chậm thành phần Calendar
.
Mã:
import React, { useState, lazy, Suspense } from "react";const Calendar = lazy(() => import("react-calendar")); /* Nhập động */export const DateOfBirth = () => { const [showModal, setShowModal] = useState(false); const handleDateChange = (date) => { setShowModal(false); }; const handleFocus = () = > setShowModal(true); return ( {showModal && ( )} );};
import
và thay thế bằng câu lệnh lazy
import. Tiếp theo, chúng ta cần gói thành phần được tải chậm trong thành phần Suspense
và cung cấp fallback
được hiển thị cho đến khi thành phần được tải chậm khả dụng.Điều quan trọng cần lưu ý là
fallback
là thuộc tính bắt buộc của thành phần Suspense
. Chúng ta có thể cung cấp bất kỳ nút React hợp lệ nào làm phương án dự phòng:-
null
Nếu chúng ta không muốn bất kỳ thứ gì hiển thị trong quá trình tải. -
string
Nếu chúng ta chỉ muốn hiển thị một văn bản. - Thành phần React
Các phần tử tải Skeleton chẳng hạn.
react-calendar
đã được tách mã thành công khỏi gói chính.
Thành phần dự án
Chúng tôi không giới hạn ở các thành phần của bên thứ ba hoặc các gói NPM. Chúng tôi có thể chia tách mã hầu như bất kỳ thành phần nào trong dự án của mình. Ví dụ, hãy lấy các tuyến đường trang web và chia tách mã các thành phần trang riêng lẻ thành các gói riêng biệt. Theo cách đó, chúng ta sẽ luôn chỉ tải gói chính (được chia sẻ) và một gói thành phần cần thiết cho trang mà chúng ta đang ở.App.jsx
chính của chúng ta bao gồm một bộ định tuyến React và ba thành phần được tải tùy thuộc vào vị trí hiện tại (URL).
Mã:
import { Navigation } from "./Navigation";import { Routes, Route } from "react-router-dom";import React from "react";import Dashboard from "./pages/Dashboard";import Home from "./pages/Home";import About from "./pages/About";function App() { return ( );}export default App;
Mã:
import React from "react";const Home = () => { return (/* Component */);};export default Home;

Hãy cấu trúc lại các câu lệnh
import
của chúng ta như trong ví dụ trước và sử dụng lazy
import cho các thành phần trang code-split. Chúng ta cũng cần lồng các thành phần này vào một thành phần Suspense
duy nhất. Nếu chúng ta phải cung cấp một phần tử dự phòng khác cho các thành phần này, chúng ta sẽ lồng từng thành phần vào một thành phần Suspense
riêng biệt. Các thành phần có một export mặc định, vì vậy chúng ta không cần phải thay đổi chúng.
Mã:
import { Routes, Route } from "react-router-dom";import React, { lazy, Suspense } from "react";const Dashboard = lazy(() => import("./pages/Dashboard"));const Home = lazy(() => import("./pages/Home"));const About = lazy(() => import("./pages/About"));function App() { return ( );}export default App;

[*]
Chúng ta nên phân tách mã nào?
Điều quan trọng là hiểu, những hàm và thành phần nào nên được chia mã thành các gói riêng biệt ngay từ đầu. Theo cách đó, chúng ta có thể chia mã chủ động và sớm trong quá trình phát triển và tránh những cạm bẫy đóng gói đã đề cập ở trên và phải gỡ rối mọi thứ.Bạn có thể đã có một số ý tưởng về cách chọn đúng thành phần để chia mã từ các ví dụ mà chúng tôi đã đề cập. Sau đây là một tiêu chí cơ bản tốt cần tuân theo khi chọn ứng viên tiềm năng để chia mã:
- các thành phần trang cho các tuyến đường (các trang riêng lẻ),
- các thành phần được tải có điều kiện tốn kém hoặc có kích thước lớn (modals, dropdowns, menu, v.v.),
- các hàm và thành phần của bên thứ ba tốn kém hoặc có kích thước lớn.
Kiểm toán và tái cấu trúc các bundle JavaScript
Một số dự án sẽ yêu cầu tối ưu hóa sau trong chu kỳ phát triển hoặc thậm chí một thời điểm nào đó sau khi dự án đi vào hoạt động. Nhược điểm chính của việc chia tách mã sau trong chu kỳ phát triển là bạn sẽ phải xử lý các thành phần và thay đổi ở quy mô rộng hơn. Nếu một số thành phần được sử dụng rộng rãi trở thành ứng cử viên tốt cho việc chia tách mã và nó được sử dụng trên 50 thành phần khác, phạm vi của yêu cầu kéo và các thay đổi sẽ lớn và khó kiểm tra nếu không có thử nghiệm tự động nào tồn tại.
Lúc đầu, việc được giao nhiệm vụ tối ưu hóa hiệu suất của toàn bộ ứng dụng web có thể hơi khó khăn. Một nơi tốt để bắt đầu là kiểm tra ứng dụng bằng Webpack Bundle Analyzer hoặc Source Map Explorer và xác định các gói cần được chia nhỏ mã và phù hợp với các tiêu chí đã đề cập ở trên. Một cách khác để xác định các gói đó là chạy kiểm tra hiệu suất trong trình duyệt hoặc sử dụng WebPageTest và kiểm tra xem gói nào chặn luồng chính của CPU lâu nhất.
Sau khi xác định các ứng viên phân chia mã, chúng ta cần kiểm tra phạm vi thay đổi cần thiết để phân chia mã thành phần này khỏi gói chính. Tại thời điểm này, chúng ta cần đánh giá xem lợi ích của việc phân chia mã có lớn hơn phạm vi thay đổi cần thiết và thời gian đầu tư cho phát triển và thử nghiệm hay không. Rủi ro này là tối thiểu hoặc không có trong giai đoạn đầu của chu kỳ phát triển.
Cuối cùng, chúng ta cần xác minh rằng thành phần đã được phân chia mã đúng cách và kích thước gói chính đã giảm. Chúng ta cũng cần xây dựng và thử nghiệm thành phần để tránh đưa ra các vấn đề tiềm ẩn.
Có rất nhiều bước để phân chia mã cho một thành phần hiện có, vì vậy chúng ta hãy tóm tắt các bước trong danh sách kiểm tra nhanh:
- Kiểm tra trang web bằng trình phân tích gói và trình tạo hồ sơ hiệu suất trình duyệt và xác định các thành phần và gói lớn hơn mất nhiều thời gian thực thi nhất.
- Kiểm tra xem lợi ích của việc phân chia mã có lớn hơn thời gian phát triển và thử nghiệm cần thiết hay không.
- Nếu thành phần có lệnh xuất được đặt tên, hãy chuyển đổi thành lệnh xuất mặc định.
- Nếu thành phần là một phần của thùng xuất, hãy xóa thành phần đó khỏi tệp barrel.
- Cấu trúc lại các câu lệnh
import
để sử dụng các câu lệnhlazy
. - Gói các thành phần code-split trong Thành phần
Suspense
và cung cấp phương án dự phòng. - Đánh giá gói kết quả (kích thước tệp và mức tăng hiệu suất). Nếu gói không làm giảm đáng kể kích thước tệp gói hoặc cải thiện hiệu suất, hãy hoàn tác việc chia tách mã.
- Kiểm tra xem dự án có xây dựng thành công không và có hoạt động mà không có bất kỳ sự cố nào không.
Ngân sách hiệu suất
Chúng ta có thể định cấu hình các công cụ xây dựng và công cụ tích hợp liên tục (CI) của mình để phát hiện các sự cố về kích thước gói ngay từ đầu quá trình phát triển bằng cách đặt ngân sách hiệu suất có thể đóng vai trò là đường cơ sở hiệu suất hoặc giới hạn kích thước tài sản chung. Các công cụ xây dựng như Webpack, công cụ CI và công cụ kiểm tra hiệu suất như Lighthouse có thể sử dụng ngân sách hiệu suất đã xác định và đưa ra cảnh báo nếu một số gói hoặc tài nguyên vượt quá giới hạn ngân sách. Sau đó, chúng ta có thể chạy việc chia tách mã cho các gói bị trình giám sát ngân sách hiệu suất phát hiện. Đây là thông tin đặc biệt hữu ích cho việc đánh giá yêu cầu kéo, vì chúng tôi kiểm tra cách các tính năng bổ sung ảnh hưởng đến kích thước gói tổng thể.
Chúng ta có thể tinh chỉnh ngân sách hiệu suất để phù hợp với các tình huống người dùng tệ nhất có thể xảy ra và sử dụng điều đó làm cơ sở để tối ưu hóa hiệu suất. Ví dụ: nếu chúng ta sử dụng kịch bản người dùng duyệt trang web trên kết nối không đáng tin cậy và chậm trên điện thoại trung bình có CPU chậm hơn làm cơ sở, chúng ta có thể cung cấp trải nghiệm người dùng tối ưu cho nhiều loại thiết bị và loại mạng của người dùng hơn nhiều.
Alex Russell đã đề cập đến chủ đề này rất chi tiết trong bài viết của ông về chủ đề ngân sách hiệu suất web trong thế giới thực và phát hiện ra rằng quy mô ngân sách tối ưu cho những tình huống xấu nhất nằm ở đâu đó giữa 130kB và 170kB.
“Ngân sách hiệu suất là một phần thiết yếu nhưng ít được đánh giá cao của thành công sản phẩm và sức khỏe của nhóm. Hầu hết các đối tác mà chúng tôi làm việc cùng đều không nhận thức được môi trường hoạt động trong thế giới thực và do đó đưa ra những lựa chọn công nghệ không phù hợp. Chúng tôi đặt ngân sách trong thời gian là Thời gian tương tác = 5 giây cho lần tải đầu tiên và
— Alex Russell
React Suspense và Kết xuất phía máy chủ (SSR)
Một lưu ý quan trọng mà chúng ta phải lưu ý là thành phần ReactSuspense
chỉ dành cho mục đích sử dụng phía máy khách, nghĩa là kết xuất phía máy chủ (SSR) sẽ báo lỗi nếu nó cố gắng kết xuất thành phần Suspense
bất kể thành phần dự phòng nào. Vấn đề này sẽ được giải quyết trong React phiên bản 18 sắp tới. Tuy nhiên, nếu bạn đang làm việc trên một dự án đang chạy trên phiên bản cũ hơn của React, bạn sẽ cần giải quyết vấn đề này.Một cách để giải quyết là kiểm tra xem mã có đang chạy trên trình duyệt hay không, đây là giải pháp đơn giản, nếu không muốn nói là hơi phức tạp.
Mã:
const isBrowser = typeof window !== "undefined"return ( {isBrowser && componentLoadCondition && ( )} )
Cho đến khi React phiên bản 18 được phát hành, nhóm React khuyên bạn nên sử dụng thư viện các thành phần Loadable Components cho trường hợp chính xác này. Plugin này mở rộng các thành phần
lazy
import và Suspense
của React và thêm hỗ trợ hiển thị phía máy chủ, nhập động với các thuộc tính động, thời gian chờ tùy chỉnh, v.v. Thư viện Loadable Components là giải pháp tuyệt vời cho các ứng dụng React lớn và phức tạp hơn, và tính năng chia tách mã React cơ bản hoàn hảo cho các ứng dụng nhỏ và vừa.Lợi ích và lưu ý khi chia tách mã
Chúng ta đã thấy hiệu suất trang và thời gian tải có thể được cải thiện như thế nào bằng cách tải động các gói JavaScript đắt tiền, không quan trọng. Là một lợi ích bổ sung của việc chia tách mã, mỗi gói JavaScript có hàm băm duy nhất, nghĩa là khi ứng dụng được cập nhật, trình duyệt của người dùng sẽ chỉ tải xuống các gói đã cập nhật có hàm băm khác nhau.Tuy nhiên, chia tách mã có thể dễ dàng bị lạm dụng và các nhà phát triển có thể trở nên quá nhiệt tình và tạo quá nhiều gói nhỏ gây hại cho khả năng sử dụng và hiệu suất. Tải động quá nhiều thành phần nhỏ hơn và không liên quan có thể khiến giao diện người dùng có cảm giác không phản hồi và bị chậm trễ, gây hại cho trải nghiệm người dùng nói chung. Việc chia tách mã quá mức thậm chí có thể gây hại cho hiệu suất trong trường hợp các gói được phục vụ qua HTTP 1.1 không có đa kênh.
"Sử dụng ngân sách hiệu suất, trình phân tích gói, công cụ giám sát hiệu suất để xác định và đánh giá từng ứng viên tiềm năng cho việc chia tách mã. Chỉ sử dụng việc chia tách mã một cách hợp lý và có chừng mực, nếu nó dẫn đến việc giảm đáng kể kích thước gói hoặc cải thiện hiệu suất đáng kể."
Tài liệu tham khảo
- Chia mã, tài liệu React
- “Tối ưu hóa khởi động JavaScript”, Addy Osmani
- “Bạn có đủ khả năng chi trả không?: Ngân sách hiệu suất web trong thế giới thực”, Alex Russell
- “Kết hợp ngân sách hiệu suất vào quy trình xây dựng của bạn”, Milica Mihajlija
- “When JavaScript Bytes”, Tim Kadlec
Đọc thêm
- Cách tăng cường quy trình thiết kế của bạn với Setapp
- Bài ca ngợi thời gian làm dự án phụ
- Tạo biểu mẫu nhiều bước hiệu quả để có trải nghiệm người dùng tốt hơn
- Chế độ chặt chẽ: Tại sao trình duyệt tạo ra kết quả hiệu suất khác nhau