Ba trụ cột của sự phình to của JavaScript
The Three Pillars of JavaScript Bloat
Việc các dự án JavaScript bị phình to do quá nhiều dependency chủ yếu xuất phát từ ba nguyên nhân chính: 1. **Hỗ trợ các môi trường chạy (runtimes) cũ:** Nhiều thư viện vẫn cần hỗ trợ các phiên bản JavaScript rất cũ, thiếu các tính năng native ES5 trở lên. Điều này buộc chúng phải bao gồm các polyfill hoặc đoạn mã tương thích, làm tăng kích thước. 2. **Bảo vệ khỏi sự thay đổi global namespace ("primordials"):** Để tránh các đoạn mã độc hại hoặc thư viện khác làm hỏng các biến toàn cục quan trọng (như `Array`, `Object`), các dependency thường cần các cơ chế "primordials" để đảm bảo chúng hoạt động với phiên bản gốc của các constructor này. 3. **Xử lý giá trị cross-realm:** Khi làm việc với các môi trường độc lập nhau (ví dụ, trong `iframe` hoặc web worker), các constructor của cùng một kiểu dữ liệu có thể khác nhau giữa các realm. Các dependency cần có cách để xử lý sự khác biệt này. Các developer nên lưu ý rằng có rất nhiều gói utility nhỏ lẻ được tạo ra để giải quyết những vấn đề này, thường là cho các tình huống khá đặc thù. Trước khi đưa một dependency vào dự án, hãy cân nhắc kỹ: liệu việc nâng cấp môi trường chạy lên phiên bản mới hơn có thể loại bỏ được nhu cầu này không, hoặc liệu có thể áp dụng các chiến lược cô lập (isolation strategies) khác để giảm thiểu dấu chân dependency của dự án hay không.
Trong vài năm qua, chúng tôi đã chứng kiến sự phát triển đáng kể của cộng đồng e18e và nhờ đó mà số lượng đóng góp tập trung vào hiệu suất cũng tăng lên. Phần lớn trong số này là sáng kiến “dọn dẹp”, trong đó...
Trong vài năm qua, chúng tôi đã chứng kiến sự phát triển đáng kể của cộng đồng e18e và nhờ đó mà số lượng đóng góp tập trung vào hiệu suất cũng tăng lên. Phần lớn trong số này là sáng kiến “dọn dẹp”, trong đó cộng đồng đã loại bỏ các gói dư thừa, lỗi thời hoặc không được bảo trì.
Một trong những chủ đề phổ biến nhất xuất hiện trong chủ đề này là "sự phình to của phần phụ thuộc" - ý tưởng rằng các cây phụ thuộc npm ngày càng lớn hơn theo thời gian, thường có mã dự phòng từ lâu mà nền tảng hiện cung cấp nguyên bản.
Trong bài đăng này, tôi muốn xem xét ngắn gọn ba loại lỗi chính trong cây phụ thuộc của chúng ta, lý do chúng tồn tại và cách chúng ta có thể bắt đầu giải quyết chúng.
1. Hỗ trợ thời gian chạy cũ hơn (với sự an toàn và các lĩnh vực)

Biểu đồ trên là hình ảnh thường thấy trong nhiều cây phụ thuộc npm - một hàm tiện ích nhỏ dành cho một thứ có vẻ như vốn đã có sẵn, theo sau là nhiều phần phụ thuộc sâu nhỏ tương tự.
Vậy tại sao lại như vậy? Tại sao chúng ta cần is-string thay vì kiểm tra typeof? Tại sao chúng ta cần hasown thay vì Object.hasOwn (hoặc Object.prototype.hasOwnProperty)? Ba điều:
- Hỗ trợ cho các công cụ rất cũ
- Bảo vệ chống lại đột biến không gian tên toàn cầu
- Giá trị đa lĩnh vực
Hỗ trợ cho các động cơ rất cũ
Ở một nơi nào đó trên thế giới, dường như có một số người cần hỗ trợ ES3 - hãy nghĩ đến IE6/7 hoặc các phiên bản cực kỳ đầu tiên của Node.js.1
Đối với những người này, phần lớn những gì chúng ta coi là đương nhiên ngày nay đều không tồn tại. Ví dụ: họ không có bất kỳ điều nào sau đây:
Array.prototype.forEachArray.prototype.reduceObject.keysObject.defineProperty
Đây đều là những tính năng của ES5, nghĩa là chúng không tồn tại trong các công cụ ES3.
Đối với những linh hồn bất hạnh vẫn đang chạy động cơ cũ này, họ cần phải tự mình thực hiện lại mọi thứ hoặc được cung cấp các polyfill.
Ngoài ra, điều thực sự tuyệt vời là nếu họ nâng cấp.
Bảo vệ chống lại đột biến không gian tên toàn cầu
Lý do thứ hai cho một số gói này là “sự an toàn”.
Về cơ bản, bên trong Node có một khái niệm về “nguyên thủy”. Về cơ bản, đây chỉ là các đối tượng toàn cầu được bao bọc khi khởi động và được Node nhập từ đó trở đi, để tránh việc Node bị phá vỡ do ai đó làm thay đổi không gian tên chung.
Ví dụ: nếu bản thân Node sử dụng Map và chúng tôi xác định lại Map là gì - chúng tôi có thể phá vỡ Node. Để tránh điều này, Node giữ một tham chiếu đến Bản đồ ban đầu mà nó nhập thay vì truy cập vào toàn cục.
Bạn có thể đọc thêm về điều này tại đây trong kho lưu trữ Node.
Điều này rất có ý nghĩa đối với một động cơ , vì nó thực sự sẽ không bị đổ nếu một tập lệnh làm xáo trộn không gian tên chung.
Một số nhà bảo trì cũng tin rằng đây cũng là cách chính xác để xây dựng gói. Đây là lý do tại sao chúng tôi có các phần phụ thuộc như math-intrinsics trong biểu đồ trên, về cơ bản sẽ tái xuất các hàm Math.* khác nhau để tránh đột biến.
Cross-realm values
Cuối cùng, chúng ta có các giá trị liên vùng. Về cơ bản, đây là những giá trị bạn đã chuyển từ lĩnh vực này sang lĩnh vực khác - ví dụ: từ trang web sang con hoặc ngược lại.
Trong trường hợp này, một regExp(pattern) mới trong iframe không cùng một lớp RegExp như lớp trong trang mẹ. Điều này có nghĩa là window.RegExp !== iframeWindow.RegExp, tất nhiên có nghĩa là val instanceof RegExp sẽ là false nếu nó đến từ iframe (một lĩnh vực khác).
Ví dụ: tôi là người bảo trì chai và chúng tôi gặp phải vấn đề chính xác này. Chúng tôi cần hỗ trợ các xác nhận xảy ra trên nhiều lĩnh vực (vì người chạy thử nghiệm có thể chạy thử nghiệm trong VM hoặc iframe), nên chúng tôi không thể dựa vào các kiểm tra instanceof. Vì lý do đó, chúng tôi sử dụng Object.prototype.toString.call(val) === '[object RegExp]' để kiểm tra xem thứ gì đó có phải là biểu thức chính quy hay không, hoạt động trên nhiều lĩnh vực vì nó không dựa vào hàm tạo.
Trong biểu đồ trên, is-string về cơ bản đang thực hiện công việc tương tự trong trường hợp chúng ta chuyển một new String(val) từ vùng này sang vùng khác.
Tại sao đây là vấn đề
Tất cả điều này có ý nghĩa đối với một nhóm rất nhỏ người. Nếu bạn đang hỗ trợ các công cụ rất cũ, chuyển các giá trị qua các lĩnh vực hoặc muốn được bảo vệ khỏi ai đó làm biến đổi môi trường - thì những gói này chính là thứ bạn cần.
Vấn đề là đại đa số chúng ta không cần bất kỳ thứ gì trong số này. Chúng tôi đang chạy phiên bản Node từ 10 năm qua hoặc sử dụng trình duyệt thường xanh. Chúng tôi không cần hỗ trợ các môi trường trước ES5, chúng tôi không chuyển giá trị qua các khung và chúng tôi gỡ cài đặt các gói phá vỡ môi trường.2
Các lớp tương thích thích hợp này bằng cách nào đó đã lọt vào “đường dẫn nóng” của các gói hàng ngày. Nhóm nhỏ những người thực sự cần thứ này sẽ là những người tìm kiếm các gói đặc biệt cho nó. Thay vào đó, nó bị đảo ngược và tất cả chúng ta đều phải trả giá.
2. Kiến trúc nguyên tử
Một số người tin rằng các gói nên được chia nhỏ đến mức gần như nguyên tử, tạo ra một tập hợp các khối xây dựng nhỏ mà sau này có thể được sử dụng lại để xây dựng những thứ khác ở cấp độ cao hơn.
Loại kiến trúc này có nghĩa là chúng tôi kết thúc với các biểu đồ như thế này:

Như bạn có thể thấy, những đoạn mã chi tiết nhất đều có các gói riêng. Ví dụ: shebang-regex là như sau tại thời điểm viết bài này:
const shebangRegex = /^#!(.*)/;
xuất mặc định shebangRegex;
Bằng cách phân tách mã đến cấp độ nguyên tử này, về mặt lý thuyết là chúng ta có thể tạo các gói cấp độ cao hơn chỉ bằng cách nối các dấu chấm.
Một số ví dụ về các gói nguyên tử này để giúp bạn hình dung về mức độ chi tiết:
arrify- Chuyển đổi một giá trị thành một mảng (Array.isArray(val) ? val : [val])dấu gạch chéo- Thay thế dấu gạch chéo ngược trong đường dẫn hệ thống tệp bằng/cli-boxes - Tệp JSON chứa các cạnh của hộp path-key- Lấy khóa biến môi trườngPATHcho nền tảng hiện tại (PATHtrên Unix,Pathtrên Windows)onetime- Đảm bảo hàm chỉ được gọi một lầnis-wsl- Kiểm tra xemprocess.platformcó phải làlinuxvàos.release()có chứakhông microsoftis-windows- Kiểm tra xemprocess.platformcó phải làwin32
Ví dụ: nếu muốn xây dựng một CLI mới, chúng tôi có thể đưa một vài CLI này vào mà không cần lo lắng về việc triển khai. Chúng tôi không cần phải làm env['PATH'] || env['Path'], chúng tôi có thể lấy một gói cho điều đó.
Tại sao đây lại là vấn đề
Trên thực tế, hầu hết hoặc tất cả các gói này không trở thành những khối xây dựng có thể tái sử dụng như mong muốn. Chúng được sao chép phần lớn trên nhiều phiên bản khác nhau trong một cây rộng hơn hoặc là các gói sử dụng một lần mà chỉ một gói khác sử dụng.
Gói sử dụng một lần
Hãy cùng xem một số gói chi tiết nhất:
shebang-regexhầu như chỉ đượcshebang-commandsử dụng bởi cùng một nhà bảo trìcli-boxeshầu như chỉ đượcsử dụng boxenvàinkbởi cùng một nhà bảo trìonetimehầu như chỉ đượcrestore-cursorsử dụng bởi cùng một nhà bảo trì
Mỗi thứ trong số này chỉ có một người tiêu dùng có nghĩa là chúng tương đương với mã nội tuyến nhưng khiến chúng tôi tốn nhiều tiền hơn để có được (yêu cầu npm, trích xuất tar, băng thông, v.v.).
Sao chép
Khi xem xét cây phụ thuộc của nuxt, chúng ta có thể thấy một vài khối xây dựng này được sao chép:
is-docker(2 phiên bản)is-stream(2 phiên bản)is-wsl(2 phiên bản)isexe(2 phiên bản)npm-run-path(2 phiên bản)path-key(2 phiên bản)path-scurry(2 phiên bản)
Nội tuyến chúng không có nghĩa là chúng tôi không còn sao chép mã nữa, nhưng điều đó có nghĩa là chúng tôi không phải trả các chi phí như giải quyết phiên bản, xung đột, chi phí mua lại, v.v.
Nội tuyến khiến việc sao chép gần như miễn phí, trong khi việc đóng gói khiến việc này trở nên đắt đỏ.
Diện tích bề mặt chuỗi cung ứng lớn hơn
Chúng tôi càng có nhiều gói hàng thì diện tích bề mặt chuỗi cung ứng của chúng tôi càng lớn. Mỗi gói đều là một điểm có khả năng xảy ra lỗi trong quá trình bảo trì, bảo mật, v.v.
Ví dụ: người bảo trì nhiều gói trong số này đã bị xâm phạm vào năm ngoái. Điều này có nghĩa là hàng trăm khối xây dựng nhỏ đã bị xâm phạm, đồng nghĩa với việc các gói cấp cao hơn mà chúng tôi thực sự cài đặt cũng bị xâm phạm.
Logic đơn giản như Array.isArray(val) ? val : [val] có thể không cần gói, bảo mật, bảo trì riêng, v.v. Nó chỉ có thể được nội tuyến và chúng ta có thể tránh được nguy cơ nó bị xâm phạm.
Tương tự như trụ cột đầu tiên, triết lý này đã đi vào “con đường nóng” và có lẽ không nên có. Một lần nữa, tất cả chúng ta đều phải trả giá mà không thu được lợi ích thực sự nào.
3. “Ponyfills” đã quá lời chào đón
Nếu đang xây dựng một ứng dụng, bạn có thể muốn sử dụng một số tính năng "trong tương lai" mà công cụ bạn chọn chưa hỗ trợ. Trong trường hợp này, polyfill có thể hữu ích - nó cung cấp cách triển khai dự phòng cho tính năng cần có, vì vậy bạn có thể sử dụng nó như thể nó được hỗ trợ nguyên bản.
Ví dụ: temporal-polyfill điền đầy đủ API tạm thời mới để chúng tôi có thể sử dụng Tạm thời bất kể công cụ có hỗ trợ hay không.
Bây giờ, nếu thay vào đó bạn đang xây dựng một thư viện thì bạn nên làm gì?
Nói chung, không thư viện nào nên tải polyfill vì đó là mối quan tâm của người tiêu dùng và thư viện không nên làm thay đổi môi trường xung quanh nó. Thay vào đó, một số nhà bảo trì chọn sử dụng thứ được gọi là ponyfill (bám sát chủ đề kỳ lân, lấp lánh và cầu vồng).
Về cơ bản, Ponyfill là một polyfill bạn nhập chứ không phải là một polyfill làm thay đổi môi trường.
Tính năng này hoạt động vì nó có nghĩa là thư viện có thể sử dụng công nghệ trong tương lai bằng cách nhập bản triển khai của nó để chuyển sang bản gốc nếu nó tồn tại và sử dụng dự phòng nếu không. Không điều nào trong số này làm thay đổi môi trường nên nó an toàn cho các thư viện sử dụng.
Ví dụ: nhanh chóng cung cấp @fastly/performance-observer-polyfill, chứa cả polyfill và điền thông tin cho PerformanceObserver.
Tại sao đây lại là vấn đề
Những polyfill này đã thực hiện công việc của mình vào thời điểm đó - chúng cho phép tác giả thư viện sử dụng công nghệ trong tương lai mà không làm thay đổi môi trường và không buộc người tiêu dùng phải biết nên cài đặt polyfill nào.
Vấn đề xảy ra khi những chú ngựa con này không được chào đón. Khi tính năng mà họ điền vào hiện đã được hỗ trợ bởi tất cả các công cụ mà chúng tôi quan tâm, thì nên loại bỏ phần bổ sung này. Tuy nhiên, điều này thường không xảy ra và dây buộc tóc vẫn giữ nguyên sau khi cần.
Chúng tôi hiện còn lại rất nhiều gói dựa trên các tính năng mà chúng tôi đã có trong một thập kỷ nay.
Ví dụ:
globalthis- điền thông tin choglobalThis(được hỗ trợ rộng rãi vào năm 2019, 49 triệu lượt tải xuống mỗi tuần)indexof- điền thông tin choArray.prototype.indexOf(được hỗ trợ rộng rãi vào năm 2010, 2,3 triệu lượt tải xuống mỗi tuần)object.entries- điền thông tin choObject.entries(được hỗ trợ rộng rãi vào năm 2017, 35 triệu lượt tải xuống mỗi tuần)
Trừ khi những gói này vẫn còn tồn tại nhờ Trụ cột 1, chúng thường vẫn được sử dụng chỉ vì không ai nghĩ đến việc loại bỏ chúng.
Khi tất cả các phiên bản hỗ trợ dài hạn của công cụ đều có tính năng này thì bạn nên xóa phần Ponyfill.4
Chúng ta có thể làm gì với điều đó?
Phần lớn sự phình to này ngày nay được lồng sâu trong các cây phụ thuộc nên việc làm sáng tỏ tất cả và đến được vị trí phù hợp là một nhiệm vụ khá khó khăn. Sẽ mất thời gian và tốn rất nhiều công sức từ người bảo trì và người tiêu dùng.
Tuy nhiên, tôi nghĩ chúng ta có thể đạt được tiến bộ đáng kể trên mặt trận này nếu tất cả chúng ta cùng nhau hợp tác.
Bắt đầu tự hỏi bản thân, “tại sao tôi lại có gói này?” và “tôi có thực sự cần nó không?”.
Nếu bạn tìm thấy thứ gì đó có vẻ dư thừa, hãy nêu vấn đề với người bảo trì để hỏi xem liệu nó có thể bị xóa hay không.
Nếu bạn gặp phải sự phụ thuộc trực tiếp có nhiều vấn đề như vậy, hãy tìm giải pháp thay thế không có. Một khởi đầu tốt cho việc đó là dự án module-replacements.
Sử dụng knip để xóa các phần phụ thuộc không sử dụng
knip là một dự án tuyệt vời có thể giúp bạn tìm và loại bỏ các phần phụ thuộc không được sử dụng, mã chết, v.v. Trong trường hợp này, đây có thể là một công cụ tuyệt vời giúp bạn tìm và xóa các phần phụ thuộc mà bạn không còn sử dụng nữa.
Việc này không nhất thiết giải quyết được các vấn đề nêu trên nhưng là điểm khởi đầu tuyệt vời để giúp dọn dẹp cây phụ thuộc trước khi thực hiện các công việc liên quan khác.
Bạn có thể đọc thêm về cách knip xử lý các phần phụ thuộc không được sử dụng trong tài liệu của họ.
Sử dụng CLI e18e để phát hiện các phần phụ thuộc có thể thay thế được
e18e CLI có một phân tích cực kỳ hữu ích để xác định những phần phụ thuộc nào không còn cần thiết nữa hoặc có những phần thay thế được cộng đồng đề xuất.
Ví dụ: nếu bạn nhận được thông tin như thế này:
$ npx @e18e/cli phân tích
...
│ Cảnh báo:
│ • Mô-đun "phấn" có thể được thay thế bằng chức năng gốc. Bạn có thể đọc thêm tại
│ https://nodejs.org/docs/latest/api/util.html#utilstyletextformat-text-options. Xem thêm tại
│ https://github.com/es-tooling/module-replacements/blob/main/docs/modules/chalk.md.
...
Bằng cách sử dụng tính năng này, chúng tôi có thể nhanh chóng xác định những phần phụ thuộc trực tiếp nào có thể được loại bỏ. Sau đó, chúng tôi cũng có thể sử dụng lệnh di chuyển để tự động di chuyển một số phần phụ thuộc sau:
$ npx @e18e/cli di chuyển --all
e18e (cli v0.0.1)
┌ Di chuyển các gói...
│
│ Mục tiêu: phấn
│
◆ /code/main.js (1 đã di chuyển)
│
└ Quá trình di chuyển hoàn thành - 1 tệp đã được di chuyển.
Trong trường hợp này, nó sẽ di chuyển từ phấn sang picocolors, một gói nhỏ hơn nhiều nhưng cung cấp chức năng tương tự.
Trong tương lai, CLI này thậm chí sẽ đề xuất dựa trên môi trường của bạn - ví dụ: nó có thể đề xuất styleText gốc thay vì thư viện màu nếu bạn đang chạy một Node đủ mới.
Sử dụng npmgraph để điều tra cây phụ thuộc của bạn
npmgraph là một công cụ tuyệt vời để trực quan hóa cây phụ thuộc của bạn và điều tra xem sự phình to đến từ đâu.
Ví dụ: chúng ta hãy xem nửa dưới của Biểu đồ phụ thuộc của ESLint kể từ khi viết bài này:

Chúng ta có thể thấy trong biểu đồ này rằng nhánh tìm kiếm bị cô lập, trong đó không có nhánh nào khác sử dụng các phần phụ thuộc sâu của nó. Đối với một việc đơn giản như truyền tải hệ thống tệp trở lên, có thể chúng ta không cần 6 gói. Sau đó, chúng tôi có thể tìm kiếm giải pháp thay thế, chẳng hạn như empathic có biểu đồ phụ thuộc nhỏ hơn nhiều và đạt được kết quả tương tự.
Thay thế mô-đun
thay thế mô-đun Dự án đang được sử dụng làm tập dữ liệu trung tâm để cộng đồng rộng lớn hơn ghi lại những gói nào có thể được thay thế bằng chức năng gốc hoặc các lựa chọn thay thế hiệu quả hơn.
Nếu bạn cần một giải pháp thay thế hoặc chỉ muốn kiểm tra các phần phụ thuộc của mình thì tập dữ liệu này rất phù hợp cho việc đó.
Tương tự, nếu bạn gặp các gói trong cây của mình bị dư thừa do chức năng gốc hoặc chỉ có các lựa chọn thay thế đã được thử nghiệm thực tế tốt hơn, thì dự án này chắc chắn là một nơi tuyệt vời để đóng góp điều đó để những người khác có thể hưởng lợi từ nó.
Kết hợp với dữ liệu còn có dự án codemods cung cấp các mod mã hóa để tự động di chuyển một số gói này sang các gói thay thế được đề xuất.
Suy nghĩ kết thúc
Tất cả chúng ta đều phải trả giá để một nhóm người cực kỳ nhỏ có được kiến trúc đặc biệt mà họ thích hoặc mức độ tương thích ngược mà họ cần.
Đây không hẳn là lỗi của những người tạo ra những gói này vì mỗi người có thể xây dựng theo cách họ muốn. Nhiều người trong số họ là thế hệ nhà phát triển JavaScript có ảnh hưởng lớn tuổi hơn - xây dựng các gói trong thời kỳ đen tối hơn, nơi nhiều API hay và khả năng tương thích chéo mà chúng ta có ngày nay không tồn tại. Họ xây dựng theo cách họ đã làm vì đó có thể là cách tốt nhất vào thời điểm đó.
Vấn đề là chúng tôi chưa bao giờ tiếp tục từ đó. Ngày nay, chúng tôi vẫn tải xuống tất cả những thứ cồng kềnh này mặc dù chúng tôi đã có những tính năng này trong vài năm.
Tôi nghĩ chúng ta có thể giải quyết vấn đề này bằng cách đảo ngược mọi thứ. Nhóm nhỏ này phải trả chi phí - họ phải có ngăn xếp đặc biệt của riêng mình hầu như chỉ họ sử dụng. Những người khác đều nhận được mã hiện đại, gọn nhẹ và được hỗ trợ rộng rãi.
Hy vọng rằng những thứ như e18e và npmx có thể giúp giải quyết vấn đề đó thông qua tài liệu, công cụ, v.v. Bạn cũng có thể trợ giúp bằng cách xem xét kỹ hơn các phần phụ thuộc của mình và hỏi "tại sao?". Hãy nêu vấn đề với các phần phụ thuộc của bạn, hỏi họ xem liệu họ có cần những gói này nữa không và tại sao họ lại cần những gói này nữa.
Chúng ta có thể sửa nó.
Chú thích cuối trang
-
Tôi tin rằng có những người cần những công cụ cũ như vậy nhưng rất muốn xem một số ví dụ ↩
-
Hầu hết sự phình to này là từ thời điểm mà nó có lẽ là cần thiết vì nền tảng này rõ ràng không có nhiều tính năng vào thời điểm đó. Tôi nghĩ đó có lẽ là quyết định/kiến trúc đúng đắn vào thời điểm đó. ↩
-
Số năm hỗ trợ được đề cập nhiều nhất là từ MDN hoặc nếu nó có trước MDN thì từ dữ liệu tương thích ↩
-
Thực sự thì nội dung về “Ponyfill” thực sự là một chủ đề chưa được giải quyết. Tôi nghĩ chúng ta nên bỏ chúng sau khi đạt được LTS, nhưng những người khác không đồng ý và muốn chúng “mãi mãi”. ↩
Tác giả: onlyspaceghost