10 điểm bởi GN⁺ 2026-03-23 | 2 bình luận | Chia sẻ qua WhatsApp
  • 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

    • knipcô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.orgcô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ư e18enpmx đ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

 
click 2026-03-23

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.

 
GN⁺ 2026-03-23
Ý 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

    • Tôi cũng đã thử nghiệm cách tiếp cận này nhiều năm rồi, và còn làm cả một trang tutorial tên là plainvanillaweb.com
      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ới các trang marketing đơn giản thì có thể, nhưng nếu là ứng dụng nhiều tính năng thì trong nhiều trường hợp phụ thuộc là bắt buộc
      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
    • Tôi đã làm một dự án theo cách này vào năm 2022, và hoàn toàn không có vấn đề nào liên quan đến CVE hay migration phiên bản
      Khách hàng cũng không phải trả một xu nào cho chi phí migration
    • Render component thì dễ, nhưng cốt lõi của framework là cung cấp cập nhật phản ứng cho mô hình
      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
    • Tôi đã làm việc với JS gần 20 năm, và cuối cùng ổn định ở cách giữ phụ thuộc ở mức tối thiểu rồi tự làm phần còn lại
      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

    • Dạo này thư viện chuẩn của JS đã khá đồ sộ rồi, nên tôi tò mò không biết bạn thấy còn thiếu chức năng nào
    • Còn tôi thì lại thích những bài than phiền gay gắt (rant). Nó giúp hiểu không chỉ cảm xúc của người ta mà cả lý do đằng sau nữ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-stuff

    • Nó làm tôi nhớ đến quy tắc viết của Kurt Vonnegut: “Mỗi câu phải hoặc bộc lộ nhân vật, hoặc đẩy hành động tiến lên”
      Giố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
    • Mọi phần mềm đều có phần thừa, nhưng npm package và web app thì đặc biệt nặng
      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)

    • Thực tế luôn có những người muốn hỗ trợ JS engine cũ, và những người tạo ra vô số mini package
      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
    • Việc cố giữ tương thích dưới ES6 nghe khá kỳ lạ
      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)
    • Không chỉ cần hỗ trợ trình duyệt cũ mà còn phải hỗ trợ cả những trình duyệt kỳ quặc
      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
    • Web có văn hóa “triển khai bây giờ rồi sửa sau”, nên phụ thuộc cũ kỹ tồn tại rất lâu
    • Cũng có trường hợp như quyết định thiết kế của Angular, khi cấu trúc quá khứ gây ra sự kém hiệu quả ở hiện tại
      Trước đây họ override setTimeout v.v. để theo dõi bất đồng bộ, còn giờ có thể xử lý đơn giản hơn nhiều bằng signals
  • Tô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

    • Tôi tò mò không biết đây chỉ là lòng tự ái hay thực sự có lợi ích gì
      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
    • Như bài immich.app/cursed-knowledge, cũng có người thêm 50 package chỉ vì lý do “tương thích ngược”
    • Về mặt bảo mật cũng rất nghiêm trọng. Mỗi micro-package như vậy đều là một bề mặt tấn công
      Cuộc đua lượt tải chỉ càng làm rủi ro tăng lên
    • Trước đây còn có người thâm nhập vào tổ chức rồi thêm package của mình vào dependency để tăng số sao trên hồ sơ xin việc
    • Đây cũng là vấn đề văn hóa. Trong cộng đồng JS, việc tự dán một đoạn code 7 dòng thường bị chê là “phát minh lại bánh xe
      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

    • Thái độ muốn hiểu một thực tế tệ là điều tốt, nhưng chấp nhận quá mức thì lại là chủ nghĩa buông xuôi
      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 thấy bài này không phải đổ lỗi cho lập trình viên, mà là một lời phê bình hợp lý về hiện trạng
  • 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ặp
    Vấ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”

    • Nếu là dùng nội bộ thì đây đúng là cách tiếp cận hợp lý. Khi công ty quyết định trình duyệt được hỗ trợ, việc quản lý sẽ dễ hơn
      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
    • Giữ mọi thứ đơn giản cuối cùng sẽ đem lại lợi ích lớn nhất
  • 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

    • Những team như thế phần lớn là do vẫn chưa thay bộ build tool được thiết lập từ khoảng năm 2015
      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”
    • Thực sự có một nhân vật cụ thể thường xuyên cổ vũ việc hỗ trợ các bản quá cũ, và người đó duy trì rất nhiều package cấp thấp
    • Nhân tiện, tôi còn nghe nói Deutsche Bahn vẫn đang dùng Windows 3.1