- Sự phình to của cây phụ thuộc trong hệ sinh thái npm được chỉ ra là một vấn đề lớn, bắt nguồn từ việc hỗ trợ runtime cũ, cấu trúc package nguyên tử và việc dùng các ponyfill đã lỗi thời
- Những package tiện ích nhỏ vẫn được duy trì vì lý do tương thích với engine cũ và độ an toàn cross-realm, nên vẫn tồn tại không cần thiết ngay cả trong môi trường hiện đại
- Kiến trúc nguyên tử ban đầu nhằm tăng khả năng tái sử dụng, nhưng trên thực tế lại trở thành một cấu trúc kém hiệu quả làm tăng chi phí trùng lặp, bảo mật và bảo trì
- Các package ponyfill cũ dành cho những tính năng mà mọi engine hiện đã hỗ trợ vẫn chưa bị loại bỏ, gây ra tải xuống không cần thiết và gánh nặng quản lý
- Cộng đồng đang thúc đẩy dọn dẹp phụ thuộc không cần thiết và chuyển sang tính năng native thông qua các công cụ như e18e, knip và module-replacements
Ba trụ cột của sự phình to phụ thuộc trong JavaScript
- Cùng với sự phát triển của cộng đồng e18e, số lượng đóng góp tập trung vào hiệu năng ngày càng tăng, và các hoạt động cleanup để dọn dẹp những package không cần thiết hoặc không còn được duy trì đang được tiến hành
- Sự phình to của cây phụ thuộc (dependency bloat) trong hệ sinh thái npm được xem là một vấn đề lớn, với các nguyên nhân chính là hỗ trợ runtime cũ, cấu trúc package nguyên tử và việc sử dụng ponyfill đã lỗi thời
1. Hỗ trợ runtime cũ (bao gồm an toàn và realm)
- Trong cây npm có rất nhiều package tiện ích nhỏ như
is-string, hasown, và chúng được duy trì vì ba lý do sau
- Hỗ trợ các engine rất cũ (ví dụ: ES3, IE6/7, Node.js đời đầu)
-
Ngăn chặn việc làm biến đổi global namespace
- Xử lý giá trị cross-realm
-
Hỗ trợ engine cũ
- Trong môi trường ES3, các tính năng ES5 không tồn tại như
Array.prototype.forEach, Object.keys, Object.defineProperty
- Trong những môi trường như vậy, phải tự cài đặt hoặc dùng polyfill
- Giải pháp tốt nhất là nâng cấp, nhưng một số người dùng vẫn duy trì phiên bản cũ
-
Ngăn chặn việc làm biến đổi global namespace
- Node nội bộ sử dụng khái niệm primordials để bọc các đối tượng global ở thời điểm khởi tạo nhằm bảo vệ chúng khỏi bị sửa đổi
- Ví dụ, nếu ghi đè
Map thì bản thân Node có thể bị hỏng, vì vậy Node giữ lại tham chiếu gốc
- Một số maintainer áp dụng cách này cho cả package thông thường và dùng các package thiên về an toàn như
math-intrinsics
-
Giá trị cross-realm
- Khi truyền đối tượng giữa các iframe, có thể phát sinh vấn đề khiến kiểm tra
instanceof thất bại
- Ví dụ:
window.RegExp !== iframeWindow.RegExp
- Các framework kiểm thử như
chai thực hiện kiểm tra kiểu giữa các realm bằng cách Object.prototype.toString.call(val)
- Những package như
is-string tồn tại để phục vụ khả năng tương thích cross-realm này
-
Vấn đề
- Phần lớn lập trình viên hiện dùng Node hiện đại hoặc trình duyệt evergreen, nên những lớp tương thích này là không cần thiết
- Tuy vậy, các package này vẫn nằm trên “hot path” của cây phụ thuộc chung nên mọi người đều phải trả chi phí
2. Kiến trúc nguyên tử (Atomic)
- Một số lập trình viên cho rằng package nên được tách thành các đơn vị nhỏ nhất có thể để cấu thành các khối xây dựng có thể tái sử dụng
- Kết quả là xuất hiện rất nhiều package bị chia nhỏ cực độ như
shebang-regex, arrify, slash, path-key, onetime, is-wsl
- Ví dụ:
shebang-regex chỉ chứa đúng một dòng regex (/^#!(.*)/)
-
Vấn đề
- Phần lớn package nguyên tử không được tái sử dụng hoặc chỉ có một bên tiêu thụ duy nhất
- Ví dụ:
shebang-regex → chỉ được shebang-command dùng
cli-boxes → chỉ được boxen, ink dùng
onetime → chỉ được restore-cursor dùng
- Trong những trường hợp này, nó tương đương với code inline nhưng lại phát sinh thêm chi phí như request npm, giải nén, băng thông
-
Vấn đề trùng lặp
- Ví dụ: trong cây phụ thuộc của
nuxt@4.4.2, các package như is-docker, is-stream, is-wsl, path-key đều bị trùng 2 phiên bản
- Nếu thay bằng code inline thì xung đột phiên bản hay chi phí resolve sẽ biến mất, nên chi phí trùng lặp gần như bằng không
-
Mở rộng rủi ro chuỗi cung ứng
- Càng nhiều package thì rủi ro bảo mật và bảo trì càng tăng
- Thực tế đã có trường hợp một maintainer quản lý nhiều package nhỏ bị hack tài khoản, khiến hàng trăm package bị xâm phạm cùng lúc
- Với đoạn code đơn giản như
Array.isArray(val) ? val : [val], không cần phải tách thành package riêng mà có thể xử lý inline
-
Kết luận
- Kiến trúc nguyên tử, trái với ý định ban đầu, đã biến thành một cấu trúc kém hiệu quả và nhiều rủi ro
- Toàn bộ hệ sinh thái đang phải gánh chi phí mà đa số người dùng không nhận được lợi ích thực chất
3. Ponyfill lỗi thời
- Polyfill là đoạn code bổ sung vào môi trường những tính năng mà engine chưa hỗ trợ,
còn Ponyfill là cách triển khai thay thế được dùng bằng cách import trực tiếp mà không sửa đổi môi trường
- Ví dụ:
@fastly/performance-observer-polyfill cung cấp cả polyfill lẫn ponyfill
-
Vấn đề
- Ponyfill từng hữu ích trong quá khứ, nhưng dù tính năng đích đã được mọi engine hỗ trợ, chúng vẫn không bị loại bỏ
- Ví dụ:
globalthis (được hỗ trợ từ năm 2019, 49 triệu lượt tải mỗi tuần)
indexof (được hỗ trợ từ năm 2010, 2,3 triệu lượt tải mỗi tuần)
object.entries (được hỗ trợ từ năm 2017, 35 triệu lượt tải mỗi tuần)
- Những package này chủ yếu còn tồn tại vì đơn giản là chưa bị gỡ bỏ
- Khi mọi engine LTS đều hỗ trợ tính năng, ponyfill nên được loại bỏ
Cách giảm sự phình to
- Do cây phụ thuộc lồng sâu, việc dọn dẹp là khó nhưng vẫn có thể cải thiện nhờ hợp tác cộng đồng
- Mỗi lập trình viên nên tự hỏi “package này có thực sự cần thiết không?”, và nếu không thì cần mở issue hoặc tìm package thay thế
- Dự án module-replacements cung cấp danh sách các package có thể thay bằng tính năng native
-
Sử dụng knip
- knip là công cụ phát hiện phụ thuộc không dùng tới và dead code
- Đây không phải giải pháp trực tiếp, nhưng hữu ích như điểm khởi đầu cho việc dọn dẹp
-
Tận dụng e18e CLI
- Có thể dùng lệnh
@e18e/cli analyze để phát hiện các phụ thuộc có thể thay thế
- Ví dụ: tự động migrate từ
chalk sang picocolors
- Trong tương lai, công cụ này cũng dự kiến đề xuất các tính năng native như
styleText của Node tùy theo môi trường
-
Tận dụng npmgraph
- npmgraph.js.org là công cụ trực quan hóa cây phụ thuộc
- Ví dụ: trong cây
eslint@10.1.0, nhánh find-up bị cô lập
- Một chức năng tìm file đơn giản không cần tới 6 package, nên có thể dùng giải pháp thay thế nhỏ gọn hơn như
empathic
-
Dự án module replacements
- Cộng đồng đang duy trì bộ dữ liệu ánh xạ giữa package có thể thay thế và tính năng native
- Dự án codemods cũng hỗ trợ tự động migrate
Kết luận
- Sự phình to hiện nay là một cấu trúc trong đó toàn bộ hệ sinh thái phải trả giá chỉ vì một nhóm nhỏ người dùng muốn duy trì khả năng tương thích cũ hoặc cấu trúc đặc thù
- Trước đây điều đó có thể là tất yếu, nhưng giờ đây khi các engine và API hiện đại đã đủ trưởng thành, đó là gánh nặng không còn cần thiết
- Trong tương lai, nhóm thiểu số này nên duy trì stack riêng, còn phần còn lại nên chuyển sang nền tảng code nhẹ hơn và hiện đại hơn
- Các dự án như e18e và npmx đang hỗ trợ điều này thông qua tài liệu hóa và công cụ,
và mỗi lập trình viên cũng cần rà soát phụ thuộc của mình và đặt câu hỏi “vì sao nó cần thiết?”
- Tất cả chúng ta đều có thể cùng nhau dọn dẹp
2 bình luận
Khi tự làm thư viện, tôi vẫn còn cung cấp bản build CJS, nhưng
tôi cũng mong các thư viện mà đến năm 2026 còn chẳng có lấy một ví dụ ESM nào, toàn bộ đều dựa trên
require, thì nên được cập nhật một chút.Ý kiến trên Hacker News
Gần đây tôi nghĩ hướng tốt nhất là phát triển bằng JavaScript không phụ thuộc
Thư viện chuẩn JS/CSS cũng rất tốt, và các thứ như phân tích tĩnh (kiểm tra JSDoc của TypeScript), ES module, web component cũng đã đủ mạnh
Nhiều người nói cách này bất lợi cho khả năng mở rộng hay bảo trì, nhưng theo kinh nghiệm của tôi thì ngược lại, có thể giữ được cấu trúc đơn giản và dễ thay đổi hơn
Phần lớn những gì framework hay build tool làm đều có thể thay bằng tính năng tích hợp sẵn của trình duyệt và mẫu vanilla
Nhưng đây vẫn là một vùng còn khá lạ, nên vấn đề là phần lớn hệ sinh thái tutorial vẫn xoay quanh các framework lớn
Thực tế, ngay cả khi chuyển hoàn toàn code React sang vanilla, tính mô-đun vẫn được giữ nguyên và độ dài code chỉ tăng khoảng 1,5 lần, trong khi hiệu năng lại tốt hơn vì không có phụ thuộc
Tất nhiên không phải phụ thuộc là xấu. Chỉ là nhiều lập trình viên bị mắc kẹt trong định kiến rằng “nhất định phải dùng”
Ví dụ tôi làm các trang có nhiều chức năng bản đồ, nên phải dùng những thư viện gần như không có lựa chọn thay thế như mapbox/maplibre/openlayers
Khách hàng cũng không phải trả một xu nào cho chi phí migration
Tôi tò mò bài toán cập nhật mô hình được xử lý thế nào, giống như trong bài viết này
Ngược lại, việc duy trì codebase quy mô lớn với ít người lại trở nên dễ hơn
Nhờ các công cụ ngày nay, việc tự triển khai đã dễ hơn nhiều so với trước, và cũng rất hợp với agentic engineering
Bài viết được viết tốt, không cảm tính mà vẫn giải thích vấn đề rất rõ
Tôi nghĩ một phần nguyên nhân của tình trạng này là vì JS chưa thực sự có một thư viện chuẩn đúng nghĩa
Bài hay, nhưng tôi nghĩ gốc rễ vấn đề là chính phần thêm thắt không cần thiết (=bloat)
Tôi muốn trích lời Saint-Exupéry: “Sự hoàn hảo không đạt được khi không còn gì để thêm, mà khi không còn gì để bớt”
Phần lớn phần mềm được viết theo kiểu hỏi “làm sao thêm cho dễ hơn?” thay vì “làm sao làm nó thanh nhã hơn?”
Và câu trả lời lúc nào cũng là
npm i more-stuffGiống như sự đối lập giữa Demosthenes và Cicero, code tốt là code không thể lược bớt thêm được nữa
JS phải tính đến cả tương thích trình duyệt cũ lẫn mới, lại là ngôn ngữ xoay quanh UI nên phình to vì accessibility, quốc tế hóa, hỗ trợ di động, v.v.
Trong nhiều trường hợp, đây có vẻ là vấn đề nợ kỹ thuật bị che giấu
Nguyên nhân là không nâng compile target lên ESx, cũng không cập nhật package hay implementation
ES5 đã được mọi trình duyệt hỗ trợ suốt 13 năm rồi (caniuse.com/es5)
Cả hai đều xem hành vi của mình là tính năng, và duy trì nhiều package phổ biến
Nên rất khó thay đổi. Đôi khi cộng đồng có chỉ trích, nhưng họ cũng có logic riêng
Nếu dùng Babel để transpile xuống bản cũ thì code sẽ cồng kềnh và chậm hơn, mà trên trình duyệt xưa cũng thường không chạy nổi vì giới hạn CSS hay JS
Thậm chí polyfill còn từng gây ra vấn đề (polyfill toán tử lũy thừa không xử lý được BigInt)
Có đủ loại môi trường như console, TV, Android đời cũ, iPod touch, trình duyệt nhúng trong Facebook, v.v.
Vì vậy tôi chỉ giữ một external module, còn lại xử lý bằng cấu hình transpiler
Trước đây họ override
setTimeoutv.v. để theo dõi bất đồng bộ, còn giờ có thể xử lý đơn giản hơn nhiều bằng signalsTôi nghĩ một số tác giả package cố tình chia nhỏ dependency tree để tăng số lượt tải
Sự tồn tại của package 7 dòng là vô lý. Metadata trong lockfile còn lớn hơn cả code
Trước đây từng có lúc 5% dependency của create-react-app là mini package của một tác giả
Có thể kể đến các ví dụ như has-symbols, is-string, ljharb
Ví dụ Anthropic cung cấp Claude miễn phí cho các maintainer mã nguồn mở có nhiều lượt tải npm
Cuộc đua lượt tải chỉ càng làm rủi ro tăng lên
Nhưng ở nhiều nền văn hóa khác thì đó lại được xem là điều tốt
Trước khi chỉ trích hệ sinh thái JS, nên đọc 30 years of br tags
Nó giúp hiểu được quá trình tiến hóa của JS và công cụ
Chỉ đơn giản nói rằng “lập trình viên JS là vấn đề” là thiếu tư duy kỹ thuật
Chúng ta luôn phải suy nghĩ về lý thuyết và thực hành tốt hơn
Thế giới phần mềm thay đổi rất nhanh, nên đôi khi cần tự làm một “đám tang giả” để từ bỏ các thói quen cũ
Tôi đang quản lý một codebase Node.js đã 9 năm tuổi, chỉ có 8 dependency và tất cả đều không có phụ thuộc con
Tôi ưu tiên tận dụng tính năng tích hợp sẵn của Node, và chỉ tự triển khai phần thật sự cần thiết
Nó ổn định hơn nhiều và bớt căng thẳng hơn trước
Thư viện chuẩn của Deno cũng rất tốt, nên nếu kết hợp với các tính năng mặc định của runtime thì chỉ vài package cũng đủ để xây ứng dụng
JS là một ngôn ngữ khá ổn nếu tiếp cận cẩn trọng
Tôi hiểu lập luận về độ an toàn cross-realm của các package như
is-string, nhưng trên thực tế tình huống đó hiếm gặpVấn đề là npm cho phép phát hành quá dễ, khiến triết lý “chia nhỏ rồi phát hành module” bị mở rộng quá đà
Người dùng không audit dependency tree mà cứ thế cài, nên chi phí vốn dĩ là tùy chọn lại biến thành chi phí mặc định
Vấn đề ponyfill có thể giải quyết bằng tự động hóa
Ví dụ một bot kiểu Renovate có thể hữu ích để tự động phát hiện và loại bỏ các tính năng đã được hỗ trợ sẵn trong các phiên bản Node LTS
Nguyên tắc của PWA nội bộ trong công ty chỉ có một:
“Hãy nâng cấp lên Chrome bản mới nhất. Nếu vẫn có vấn đề thì lúc đó tính tiếp”
Tôi hiểu Safari tốn ít bộ nhớ hơn, nhưng thống nhất theo chính sách vẫn hiệu quả hơn
Câu kiểu “phải hỗ trợ đến ES3 (cỡ IE6/7)” thực sự rất khó hiểu
Về mặt bảo mật, ngay cả trang ngân hàng cũng nên chặn những trình duyệt cổ như vậy
Nâng cấp stack Webpack, Babel, polyfill là việc lớn nên họ cứ để nguyên
Văn hóa kiểu “chưa hỏng thì đừng sửa”