Hệ sinh thái JavaScript gặp khó khăn với các thư viện phụ thuộc cồng kềnh khi các thư viện xử lý trường hợp đặc biệt chiếm ưu thế trong lượt tải xuống

Nhóm Cộng đồng BigGo
Hệ sinh thái JavaScript gặp khó khăn với các thư viện phụ thuộc cồng kềnh khi các thư viện xử lý trường hợp đặc biệt chiếm ưu thế trong lượt tải xuống

Cộng đồng phát triển JavaScript đang phải vật lộn với vấn đề ngày càng gia tăng về sự cồng kềnh của các thư viện phụ thuộc do các thư viện ưu tiên xử lý các trường hợp đặc biệt hơn là các tình huống sử dụng phổ biến. Vấn đề này đã tạo ra các cây phụ thuộc phức tạp không cần thiết, ảnh hưởng đến hàng triệu dự án trên toàn thế giới.

Nguyên nhân gốc rễ: Thiết kế quá mức cho các tình huống hiếm gặp

Vấn đề cốt lõi xuất phát từ các thư viện xử lý những trường hợp đặc biệt mà hầu hết các nhà phát triển sẽ không bao giờ gặp phải. Thay vì tập trung vào trường hợp sử dụng chính, nhiều gói phổ biến triển khai việc xác thực và kiểm tra kiểu dữ liệu mở rộng cho các tình huống chỉ xảy ra trong ít hơn 1% ứng dụng thực tế. Cách tiếp cận này buộc tất cả người dùng phải gánh chịu chi phí hiệu suất của các tính năng họ không cần.

Cuộc thảo luận trong cộng đồng cho thấy vấn đề này một phần tồn tại bởi vì JavaScript trong lịch sử thiếu hệ thống kiểu mạnh. Như một nhà phát triển đã lưu ý, JavaScript yêu cầu kiểm tra runtime mở rộng vì bạn có thể truyền cho một hàm bất cứ thứ gì và việc triển khai phải xử lý nó. Tuy nhiên, ngay cả với việc áp dụng TypeScript , nhiều thư viện vẫn tiếp tục mô hình lập trình phòng thủ này.

Các thư viện phổ biến với hàng triệu lượt tải xuống hàng tuần cho thấy quy mô vấn đề

Một số gói được sử dụng rộng rãi đã chứng minh vấn đề này. Thư viện is-number, được tải xuống 90 triệu lần mỗi tuần, không chỉ kiểm tra xem một thứ gì đó có phải là số hay không - nó còn xác thực cụ thể cho các số dương, hữu hạn và các chuỗi giống số. Hầu hết các nhà phát triển chỉ cần typeof n === 'number', khiến cho sự phức tạp bổ sung trở nên không cần thiết.

Tương tự, is-arrayish nhận được 76 triệu lượt tải xuống hàng tuần nhưng xử lý các tình huống phức tạp như phát hiện mảng cross-realm mà hầu hết ứng dụng không bao giờ gặp phải. Phương thức Array.isArray() tiêu chuẩn sẽ đủ cho phần lớn các trường hợp sử dụng.

Thư viện pascalcase là ví dụ điển hình của việc mở rộng tính năng bằng cách chấp nhận chuỗi, giá trị null, giá trị undefined, mảng, hàm và các đối tượng tùy ý với phương thức toString. Tuy nhiên gần như mọi người dùng đều truyền các chuỗi đơn giản, khiến việc xử lý đầu vào bổ sung trở nên thừa thãi.

Các thư viện phổ biến được thiết kế quá phức tạp theo lượt tải xuống hàng tuần:

  • is-number: 90 triệu lượt tải/tuần - xác thực các số dương, hữu hạn và chuỗi giống số
  • is-arrayish: 76 triệu lượt tải/tuần - kiểm tra các đối tượng giống mảng bao gồm cả các trường hợp cross-realm
  • pascalcase: 9.7 triệu lượt tải/tuần - chấp nhận chuỗi, null, undefined, mảng, hàm và đối tượng
  • is-regexp: 10 triệu lượt tải/tuần - hỗ trợ phát hiện RegExp cross-realm
  • shebang-regex: 86 triệu lượt tải/tuần - 2 dòng code, tương đương với startsWith('!')

Chi phí ẩn của việc xác thực vô hình

Cách tiếp cận này tạo ra gánh nặng ẩn cho các nhà phát triển vô tình kế thừa các quy tắc xác thực hạn chế được chôn sâu trong cây phụ thuộc của họ. Nhiều nhà phát triển không nhận ra rằng các thư viện họ sử dụng gián tiếp có thể từ chối các giá trị số âm hoặc vô cực, dẫn đến hành vi bất ngờ trong ứng dụng sản xuất.

Logic xác thực thường hoạt động một cách vô hình, khiến việc gỡ lỗi trở nên khó khăn hơn khi việc xử lý trường hợp đặc biệt can thiệp vào các trường hợp sử dụng hợp lệ. Điều này chuyển gánh nặng xác thực từ các ranh giới ứng dụng, nơi nó thuộc về, đến sâu trong chuỗi phụ thuộc nơi nó trở nên khó kiểm soát hơn.

Các giải pháp nổi lên từ cộng đồng

Cộng đồng phát triển đang tích cực giải quyết vấn đề này thông qua một số cách tiếp cận. Cộng đồng e18e đóng góp các cải tiến hiệu suất trên toàn hệ sinh thái bằng cách thay thế các phụ thuộc cồng kềnh bằng các lựa chọn thay thế hiện đại, hiệu quả. Họ duy trì danh sách các thay thế được khuyến nghị và cung cấp các plugin ESLint để giúp xác định các phụ thuộc có vấn đề.

Đối với các nhà duy trì thư viện, giải pháp bao gồm việc đưa ra các giả định nghiêm ngặt hơn về kiểu đầu vào và loại bỏ xác thực không cần thiết. Các lựa chọn thay thế hiện đại như scule thể hiện cách tiếp cận này bằng cách chỉ chấp nhận các kiểu dữ liệu mà chúng được thiết kế để xử lý trong khi duy trì không có phụ thuộc.

Các công cụ như npmgraph và node-modules.dev giúp các nhà phát triển hình dung cây phụ thuộc của họ và xác định các cơ hội tối ưu hóa. Những công cụ này giúp dễ dàng phát hiện các gói quá chi tiết không cần thiết và tìm các lựa chọn thay thế hiệu quả hơn.

Các Lựa Chọn Thay Thế Nhẹ Được Khuyến Nghị:

  • scule: 1.8M lượt tải xuống/tuần - chuyển đổi định dạng văn bản không phụ thuộc vào thư viện nào
  • dlv: 14.9M lượt tải xuống/tuần - truy cập thuộc tính sâu với xác thực tối thiểu
  • Array.isArray() gốc - thay thế is-arrayish cho hầu hết các trường hợp sử dụng
  • typeof n === 'number' gốc - thay thế is-number cho việc kiểm tra kiểu dữ liệu cơ bản
  • Inline Math.min(Math.max(value, min), max) - thay thế các hàm clamp phức tạp

Hướng tới các phụ thuộc gọn nhẹ hơn

Con đường phía trước đòi hỏi một sự thay đổi cơ bản trong triết lý thiết kế thư viện. Thay vì xây dựng các thư viện ưu tiên trường hợp đặc biệt xử lý mọi tình huống có thể, cộng đồng nên tập trung vào các thư viện trường hợp sử dụng phổ biến với các phần mở rộng tùy chọn cho các nhu cầu chuyên biệt.

Cách tiếp cận này sẽ cho phép phần lớn người dùng hưởng lợi từ các thư viện nhẹ hơn, nhanh hơn trong khi vẫn cung cấp giải pháp cho các nhà phát triển thực sự cần xử lý trường hợp đặc biệt. Mục tiêu là đảm bảo rằng chỉ những người cần xác thực phức tạp mới phải trả chi phí hiệu suất của nó, thay vì áp đặt nó lên toàn bộ hệ sinh thái.

Lưu ý: Cây phụ thuộc đề cập đến mạng lưới các gói mà một dự án dựa vào, bao gồm cả phụ thuộc trực tiếp (các gói bạn cài đặt một cách rõ ràng) và phụ thuộc gián tiếp (các gói mà các phụ thuộc của bạn cần).

Tham khảo: The bloat of edge-case first libraries