- ในระบบนิเวศ npm ปัญหาใหญ่ที่ถูกชี้ให้เห็นคือ การพองตัวของ dependency tree ซึ่งเกิดจากการรองรับรันไทม์เก่า โครงสร้างแพ็กเกจแบบอะตอม และการใช้ ponyfill ที่ล้าสมัย
- แพ็กเกจยูทิลิตีขนาดเล็กที่ยังคงอยู่ด้วยเหตุผลด้านความเข้ากันได้กับเอนจินรุ่นเก่าและ ความปลอดภัยข้าม realm ยังคงหลงเหลือโดยไม่จำเป็นแม้ในสภาพแวดล้อมสมัยใหม่
- สถาปัตยกรรมแบบอะตอม ถูกออกแบบมาเพื่อเพิ่มการนำกลับมาใช้ซ้ำ แต่ในทางปฏิบัติกลับกลายเป็นโครงสร้างที่ไม่มีประสิทธิภาพซึ่งเพิ่มต้นทุนด้านความซ้ำซ้อน ความปลอดภัย และการบำรุงรักษา
- แพ็กเกจ ponyfill ที่ล้าสมัย สำหรับฟีเจอร์ที่ทุกเอนจินรองรับอยู่แล้วไม่ได้ถูกลบออก ส่งผลให้เกิดภาระการดาวน์โหลดและการจัดการโดยไม่จำเป็น
- ชุมชนกำลังผลักดัน การจัดระเบียบ dependency ที่ไม่จำเป็นและการเปลี่ยนไปใช้ความสามารถแบบเนทีฟ ผ่านเครื่องมืออย่าง e18e, knip และ module-replacements
สามแกนของภาวะพองตัวของ dependency ใน JavaScript
- พร้อมกับการเติบโตของ ชุมชน e18e การมีส่วนร่วมที่เน้นประสิทธิภาพก็เพิ่มขึ้น และมีการทำ cleanup เพื่อลบแพ็กเกจที่ไม่จำเป็นหรือไม่มีการดูแลแล้ว
- ในระบบนิเวศ npm ปัญหาใหญ่ที่ถูกชี้ให้เห็นคือ ภาวะพองตัวของ dependency tree (dependency bloat) โดยมีสาเหตุหลักจากการรองรับรันไทม์เก่า โครงสร้างแพ็กเกจแบบอะตอม และการใช้ ponyfill ที่ล้าสมัย
1. การรองรับรันไทม์เก่า (รวมถึงความปลอดภัยและ realm)
- ใน npm tree มี แพ็กเกจยูทิลิตีขนาดเล็ก จำนวนมาก เช่น
is-string, hasown ซึ่งยังคงอยู่ด้วยเหตุผลสามประการต่อไปนี้
- รองรับเอนจินที่เก่ามาก (เช่น ES3, IE6/7, Node.js ยุคแรก)
-
การป้องกันการดัดแปลง global namespace
-
การรองรับเอนจินเก่า
- ในสภาพแวดล้อม ES3 ไม่มี ฟีเจอร์ของ ES5 เช่น
Array.prototype.forEach, Object.keys, Object.defineProperty
- ในสภาพแวดล้อมเหล่านี้ต้องเขียนเองโดยตรงหรือใช้ polyfill
- ทางออกที่ดีที่สุดคือ อัปเกรด แต่ผู้ใช้บางส่วนยังคงใช้เวอร์ชันเก่าอยู่
-
การป้องกันการดัดแปลง global namespace
- ภายใน Node ใช้แนวคิด primordials โดยห่ออ็อบเจ็กต์ global ตั้งแต่ช่วงเริ่มต้นเพื่อป้องกันการดัดแปลง
- ตัวอย่างเช่น หากมีการ override
Map ตัว Node เองอาจทำงานพังได้ ดังนั้น Node จึงเก็บ reference ต้นฉบับไว้
- ผู้ดูแลแพ็กเกจบางรายนำแนวทางนี้มาใช้กับแพ็กเกจทั่วไปด้วย โดยใช้ แพ็กเกจที่เน้นความปลอดภัย อย่าง
math-intrinsics
-
ค่าข้าม realm
- เมื่อส่งอ็อบเจ็กต์ข้าม iframe จะเกิดปัญหาที่การตรวจ
instanceof ล้มเหลว
- ตัวอย่าง:
window.RegExp !== iframeWindow.RegExp
- เฟรมเวิร์กทดสอบอย่าง
chai ใช้วิธี Object.prototype.toString.call(val) เพื่อตรวจชนิดข้าม realm
- แพ็กเกจอย่าง
is-string จึงมีอยู่เพื่อรองรับ ความเข้ากันได้ข้าม realm
-
ปัญหา
- นักพัฒนาส่วนใหญ่ใช้ Node รุ่นใหม่หรือเบราว์เซอร์ evergreen อยู่แล้ว ทำให้ความเข้ากันได้แบบนี้ไม่จำเป็น
- แต่แพ็กเกจเหล่านี้กลับอยู่ใน “hot path” ของ dependency tree ทั่วไป ทำให้ ทุกคนต้องจ่ายต้นทุน
2. สถาปัตยกรรมแบบอะตอม (Atomic)
- นักพัฒนาบางส่วนเชื่อว่าควรแยกแพ็กเกจออกเป็น หน่วยที่เล็กที่สุดเท่าที่จะทำได้ เพื่อสร้างเป็น building blocks ที่นำกลับมาใช้ซ้ำได้
- ผลลัพธ์คือมีแพ็กเกจที่แยกย่อยอย่างมากจำนวนมาก เช่น
shebang-regex, arrify, slash, path-key, onetime, is-wsl
- ตัวอย่าง:
shebang-regex มีเพียง regex บรรทัดเดียว (/^#!(.*)/)
-
ปัญหา
- แพ็กเกจแบบอะตอมส่วนใหญ่ ไม่ได้ถูกนำกลับมาใช้ซ้ำ หรือมีผู้ใช้เพียงรายเดียว
- ตัวอย่าง:
shebang-regex → ใช้โดย shebang-command เท่านั้น
cli-boxes → ใช้โดย boxen, ink เท่านั้น
onetime → ใช้โดย restore-cursor เท่านั้น
- ในกรณีเหล่านี้มันแทบไม่ต่างจากการเขียนโค้ด inline แต่กลับมีต้นทุนเพิ่มจาก คำขอ npm, การแตกไฟล์, แบนด์วิดท์ เป็นต้น
-
ปัญหาความซ้ำซ้อน
- ตัวอย่าง: ใน dependency tree ของ
nuxt@4.4.2 มี is-docker, is-stream, is-wsl, path-key ซ้ำกันอย่างละ 2 เวอร์ชัน
- หากแทนที่ด้วยโค้ด inline ก็จะไม่มีทั้งปัญหาความขัดแย้งของเวอร์ชันและต้นทุนการ resolve ทำให้ ต้นทุนจากความซ้ำซ้อนแทบหายไป
-
ความเสี่ยงของซัพพลายเชนที่เพิ่มขึ้น
- ยิ่งมีจำนวนแพ็กเกจมากเท่าไร ความเสี่ยงด้านความปลอดภัยและการบำรุงรักษา ก็ยิ่งเพิ่มขึ้น
- เคยมีกรณีจริงที่ผู้ดูแลคนหนึ่งดูแลแพ็กเกจขนาดเล็กจำนวนมาก แล้วบัญชีถูกแฮ็กจนทำให้ แพ็กเกจนับร้อยเสียหายพร้อมกัน
- โค้ดง่าย ๆ อย่าง
Array.isArray(val) ? val : [val] สามารถเขียน inline ได้โดยไม่จำเป็นต้องแยกเป็นแพ็กเกจ
-
บทสรุป
- สถาปัตยกรรมแบบอะตอมจึงกลายสภาพเป็นโครงสร้างที่ ไม่มีประสิทธิภาพและเสี่ยง ตรงข้ามกับเจตนาเดิม
- โดยแทบไม่ให้ประโยชน์จริงกับผู้ใช้ส่วนใหญ่ แต่ทำให้ ทั้งระบบนิเวศต้องแบกรับต้นทุน
3. Ponyfill ที่ล้าสมัย
- Polyfill คือโค้ดที่เพิ่มความสามารถให้กับสภาพแวดล้อมเมื่อเอนจินยังไม่รองรับฟีเจอร์นั้น
ส่วน Ponyfill คืออิมพลีเมนเทชันทดแทนที่ import มาใช้โดยตรง โดยไม่แก้ไขสภาพแวดล้อม
- ตัวอย่าง:
@fastly/performance-observer-polyfill มีทั้ง polyfill และ ponyfill
-
ปัญหา
- Ponyfill เคยมีประโยชน์ในอดีต แต่ แม้ฟีเจอร์เป้าหมายจะถูกรองรับโดยทุกเอนจินแล้วก็ยังไม่ถูกลบออก
- ตัวอย่าง:
globalthis (รองรับตั้งแต่ปี 2019, ดาวน์โหลด 49 ล้านครั้งต่อสัปดาห์)
indexof (รองรับตั้งแต่ปี 2010, ดาวน์โหลด 2.3 ล้านครั้งต่อสัปดาห์)
object.entries (รองรับตั้งแต่ปี 2017, ดาวน์โหลด 35 ล้านครั้งต่อสัปดาห์)
- แพ็กเกจเหล่านี้ส่วนใหญ่ยังคงอยู่ เพียงเพราะไม่เคยถูกลบออก
- เมื่อเอนจิน LTS ทุกตัวรองรับฟีเจอร์นั้นแล้ว ก็ควรถอด ponyfill ออก
แนวทางลดภาวะพองตัว
- ด้วยความที่ dependency tree ซ้อนลึกมาก การจัดระเบียบจึงทำได้ยาก แต่สามารถปรับปรุงได้ผ่านความร่วมมือของชุมชน
- นักพัฒนาแต่ละคนควรถามตัวเองว่า “แพ็กเกจนี้จำเป็นจริงหรือไม่?” และหากไม่จำเป็นก็ควร เปิด issue หรือมองหาแพ็กเกจทดแทน
- โปรเจกต์ module-replacements ให้ รายชื่อแพ็กเกจที่สามารถแทนด้วยความสามารถแบบเนทีฟได้
-
การใช้ knip
- knip เป็น เครื่องมือสำหรับตรวจจับ dependency ที่ไม่ได้ใช้และโค้ดที่ตายแล้ว
- แม้จะไม่ใช่คำตอบโดยตรง แต่ก็มีประโยชน์ในฐานะ จุดเริ่มต้นของการ cleanup
-
การใช้ e18e CLI
- สามารถใช้คำสั่ง
@e18e/cli analyze เพื่อ ตรวจหา dependency ที่สามารถแทนที่ได้
- ตัวอย่าง: ย้ายจาก
chalk ไปเป็น picocolors แบบอัตโนมัติ
- ในอนาคตมีแผนจะแนะนำ ความสามารถแบบเนทีฟอย่าง
styleText ของ Node ตามสภาพแวดล้อมด้วย
-
การใช้ npmgraph
- npmgraph.js.org เป็น เครื่องมือแสดงภาพ dependency tree
- ตัวอย่าง: ใน tree ของ
eslint@10.1.0 จะเห็นว่าแขนง find-up ถูกแยกโดดเดี่ยวอยู่
- ฟังก์ชันค้นหาไฟล์แบบง่าย ๆ ไม่ควรต้องใช้ถึง 6 แพ็กเกจ ดังนั้นจึงอาจใช้ทางเลือกที่เล็กกว่าอย่าง
empathic ได้
-
โปรเจกต์ module replacements
- ชุมชนช่วยกันดูแล ชุดข้อมูลการแมประหว่างแพ็กเกจที่แทนที่ได้กับความสามารถแบบเนทีฟ
- และยังรองรับ การย้ายแบบอัตโนมัติ ผ่าน โปรเจกต์ codemods
บทสรุป
- ภาวะพองตัวในปัจจุบันคือโครงสร้างที่ทำให้ทุกคนต้องจ่ายต้นทุน เพราะมีผู้ใช้ส่วนน้อยที่ยังต้องการ ความเข้ากันได้กับของเก่าและโครงสร้างเฉพาะทาง
- แม้ในอดีตจะหลีกเลี่ยงไม่ได้ แต่ เมื่อเอนจินและ API สมัยใหม่พัฒนามากพอแล้ว ภาระนี้ก็ไม่จำเป็นอีกต่อไป
- ต่อจากนี้ ผู้ใช้ส่วนน้อยเหล่านั้นอาจต้องดูแลสแตกแยกต่างหาก ส่วนคนส่วนใหญ่ควรเปลี่ยนไปสู่ ฐานโค้ดที่เบาและทันสมัย
- โปรเจกต์อย่าง e18e และ npmx กำลังสนับสนุนเรื่องนี้ผ่านการจัดทำเอกสารและเครื่องมือ
และนักพัฒนาแต่ละคนก็ควรตรวจสอบ dependency ของตนเอง พร้อมถามว่า “จำเป็นเพราะอะไร?”
- ทุกคนช่วยกันจัดระเบียบได้
2 ความคิดเห็น
ตอนที่ผมทำไลบรารีเองก็ยังคงมีการแจกจ่าย cjs build อยู่เหมือนกัน
แต่ก็อยากให้ไลบรารีที่แม้แต่ในปี 2026 ก็ยังไม่มีตัวอย่าง esm และทั้งหมดอิงกับ
requireล้วนอัปเดตกันสักหน่อยความคิดเห็นจาก Hacker News
ช่วงนี้ผมคิดว่าทิศทางที่ดีที่สุดคือการพัฒนาด้วย JavaScript ที่ไม่มี dependency
ทั้งไลบรารีมาตรฐานของ JS/CSS, การวิเคราะห์แบบสถิต (การตรวจ JSDoc ของ TypeScript), ES modules, เว็บคอมโพเนนต์ ก็ทรงพลังเพียงพอแล้ว
คนมักบอกว่าวิธีนี้ไม่เหมาะกับการขยายระบบหรือการดูแลรักษา แต่จากประสบการณ์ของผมกลับทำให้รักษาโครงสร้างที่เรียบง่ายและแก้ไขเปลี่ยนแปลงได้ง่ายกว่า
สิ่งที่เฟรมเวิร์กหรือ build tool ทำอยู่ส่วนใหญ่สามารถแทนที่ได้ด้วยความสามารถที่มีอยู่ในเบราว์เซอร์และ vanilla pattern
แต่ปัญหาคือแนวทางนี้ยังเป็นพื้นที่ที่ไม่ค่อยคุ้นเคย ทำให้ระบบนิเวศของบทเรียนส่วนใหญ่ยังหมุนรอบเฟรมเวิร์กขนาดใหญ่
จริง ๆ แล้วต่อให้ย้ายโค้ด React มาเป็น vanilla ล้วน ๆ ก็ยังรักษาความเป็นโมดูลาร์ไว้ได้ และโค้ดยาวขึ้นแค่ราว 1.5 เท่า แต่เพราะไม่มี dependency ประสิทธิภาพกลับดีกว่า
แน่นอนว่าไม่ได้หมายความว่า dependency เป็นสิ่งไม่ดี แค่มีนักพัฒนาจำนวนมากติดอยู่กับ กรอบความคิดตายตัว ว่า “จำเป็นต้องใช้”
ตัวอย่างเช่นผมทำเว็บที่มีความสามารถด้านแผนที่เยอะ จึงต้องใช้ไลบรารีอย่าง mapbox/maplibre/openlayers ที่แทบไม่มีตัวเลือกทดแทน
ลูกค้าก็ไม่ต้องจ่ายค่าการย้ายระบบแม้แต่บาทเดียว
ผมเลยสงสัยว่าเขาจัดการการอัปเดตโมเดลอย่างไร ตามที่พูดไว้ในบทความนี้
กลับกลายเป็นว่าการดูแล codebase ขนาดใหญ่ ด้วยทีมเล็กทำได้สะดวกกว่า
ด้วยเครื่องมือสมัยนี้ การทำเองง่ายกว่าเมื่อก่อนมาก และยังเข้ากับ agentic engineering ได้ดีด้วย
บทความนี้เขียนดี และอธิบายปัญหาได้ชัดโดยไม่ใช้อารมณ์
ผมคิดว่าส่วนหนึ่งของสาเหตุคือ JS ยังไม่มี “standard library” ที่สมบูรณ์จริง ๆ
เป็นบทความที่ดี แต่ผมคิดว่ารากของปัญหาจริง ๆ คือ การเพิ่มสิ่งที่ไม่จำเป็น (=bloat)
อยากยกคำพูดของแซงเต็กซูเปรีที่ว่า “ความสมบูรณ์แบบไม่ได้เกิดขึ้นเมื่อไม่มีอะไรให้เพิ่มอีก แต่เกิดขึ้นเมื่อไม่มีอะไรให้ตัดออกอีก”
ซอฟต์แวร์ส่วนใหญ่ถูกเขียนโดยตั้งคำถามว่า “จะเพิ่มอะไรได้ง่ายขึ้นอย่างไร?” มากกว่า “จะทำให้มันงดงามขึ้นอย่างไร?”
และคำตอบก็มักเป็น
npm i more-stuffเสมอเหมือนกับการเปรียบเทียบระหว่าง เดโมสเทเนสกับซิเซโร โค้ดที่ดีคือโค้ดที่ตัดอะไรออกไม่ได้อีกแล้ว
JS ต้องคำนึงถึงทั้งความเข้ากันได้กับเบราว์เซอร์เก่าและอนาคต อีกทั้งเป็นภาษาที่เน้น UI จึงทำให้ ตัวระบบพองใหญ่ขึ้น จากเรื่อง accessibility, internationalization, การรองรับมือถือ ฯลฯ
หลายกรณีดูเหมือนนี่คือปัญหา หนี้ทางเทคนิค ที่ซ่อนอยู่
สาเหตุมาจากการไม่อัปเดต compile target ไปเป็น ESx ที่ใหม่ขึ้น และไม่อัปเดตแพ็กเกจหรือ implementation
ES5 รองรับในทุกเบราว์เซอร์มาแล้ว 13 ปี (caniuse.com/es5)
ทั้งสองกลุ่มมองว่าสิ่งที่ตัวเองทำคือฟีเจอร์ และยังดูแลแพ็กเกจยอดนิยมจำนวนมาก
เลยทำให้เปลี่ยนแปลงได้ยาก แม้บางครั้งชุมชนจะวิจารณ์ แต่พวกเขาก็มีเหตุผลในแบบของตัวเอง
ถ้า transpile ลงไปเป็นเวอร์ชันเก่าด้วย Babel โค้ดจะ ใหญ่และช้าลง และสุดท้ายก็ยังใช้ไม่ได้บนเบราว์เซอร์เก่าเพราะข้อจำกัดของ CSS หรือฟีเจอร์ JS อยู่ดี
แถมเคยมีกรณีที่ polyfill ก่อปัญหาด้วยซ้ำ (polyfill ของ exponent operator ที่จัดการ BigInt ไม่ได้)
มีทั้งคอนโซล, ทีวี, Android รุ่นเก่า, iPod touch, เบราว์เซอร์ฝังใน Facebook และสภาพแวดล้อมอีกหลากหลาย
เพราะงั้นผมเลยมี external module แค่ตัวเดียว แล้วปล่อยให้ที่เหลือจัดการด้วยการตั้งค่า transpiler
เมื่อก่อนต้อง override
setTimeoutอะไรทำนองนั้นเพื่อติดตาม async แต่ตอนนี้จัดการได้ง่ายกว่ามากด้วย signalsผมคิดว่าผู้เขียนแพ็กเกจบางคน จงใจแยก dependency tree ออกเป็นชิ้นเล็ก ๆ เพื่อเพิ่มยอดดาวน์โหลด
การมีแพ็กเกจยาว 7 บรรทัดมันไร้สาระมาก ขนาด metadata ใน lockfile ยังใหญ่กว่าโค้ดเสียอีก
เมื่อก่อน 5% ของ dependency ใน create-react-app มาจากมินิแพ็กเกจของผู้เขียนคนเดียว
มีตัวอย่างอย่าง has-symbols, is-string, ljharb
อย่างเช่น Anthropic ให้ Claude ฟรี แก่นักดูแลโอเพนซอร์สที่มียอดดาวน์โหลด npm สูง
การแข่งขันเรื่องยอดดาวน์โหลดยิ่งทำให้ความเสี่ยงสูงขึ้น
แต่ในวัฒนธรรมอื่นกลับมองว่านั่นเป็นเรื่องที่ดีกว่าเสียอีก
ก่อนจะวิจารณ์ ecosystem ของ JS ผมว่าไปอ่าน 30 years of br tags ก่อนก็ดี
จะช่วยให้เข้าใจ กระบวนการวิวัฒนาการ ของ JS และเครื่องมือรอบตัวมัน
การพูดแค่ว่า “ปัญหาคือพวกนักพัฒนา JS” เป็นการขาดวิธีคิดแบบวิศวกรรม
เราควรคิดถึงทฤษฎีและแนวปฏิบัติที่ดีกว่าอยู่เสมอ
โลกของซอฟต์แวร์เปลี่ยนเร็ว จึงควรจัด “พิธีศพปลอม” ให้ตัวเองเป็นระยะเพื่อทิ้งแนวปฏิบัติเก่า ๆ
ผมกำลังดูแล codebase Node.js ที่มีอายุ 9 ปี ซึ่งมี dependency แค่ 8 ตัว และทั้งหมด ไม่มี dependency ย่อย
ผมใช้ความสามารถที่มีมาใน Node ก่อน แล้วค่อยเขียนเองเฉพาะส่วนที่จำเป็น
มันเสถียรกว่าเดิมมากและเครียดน้อยกว่ามาก
standard library ของ Deno ก็ยอดเยี่ยมเช่นกัน ถ้าใช้ร่วมกับความสามารถพื้นฐานของ runtime ก็สร้างแอปได้ด้วยแพ็กเกจไม่กี่ตัว
JS เป็น ภาษาที่ดีพอสมควรถ้าใช้อย่างระมัดระวัง
ผมเข้าใจข้ออ้างเรื่อง ความปลอดภัยข้าม realm ของแพ็กเกจอย่าง
is-stringแต่ในความเป็นจริงสถานการณ์แบบนั้นพบไม่บ่อยปัญหาคือ npm เปิดให้เผยแพร่ได้ง่ายเกินไป จนปรัชญาแบบ “แยกโมดูลแล้วเผยแพร่” ถูก ขยายเกินขอบเขต
ผู้ใช้ไม่ค่อย audit dependency tree และมักติดตั้งเลยทันที ทำให้ต้นทุนที่ควรเป็นทางเลือกกลายเป็นต้นทุนพื้นฐานไป
ปัญหา ponyfill น่าจะแก้ได้ด้วยระบบอัตโนมัติ
เช่น อาจมี บอตสไตล์ Renovate ที่ตรวจจับอัตโนมัติและลบฟีเจอร์ที่มีรองรับอยู่แล้วใน Node LTS
หลักการของ PWA ภายในบริษัทเรามีแค่อย่างเดียว:
“อัปเกรดเป็น Chrome เวอร์ชันล่าสุด ถ้ายังมีปัญหาค่อยว่ากัน”
ผมเข้าใจว่า Safari ใช้หน่วยความจำน้อยกว่า แต่การกำหนดให้เป็นมาตรฐานเดียวกันในเชิงนโยบายมีประสิทธิภาพกว่า
คำพูดที่ว่า “ต้องรองรับถึง ES3 (ระดับ IE6/7)” นี่เข้าใจยากจริง ๆ
ในเชิงความปลอดภัย แม้แต่เว็บธนาคารก็ควรบล็อกเบราว์เซอร์โบราณแบบนั้น
การอัปเกรด Webpack, Babel, polyfill stack เป็นงานใหญ่ เลยปล่อยค้างไว้อย่างนั้น
เป็นวัฒนธรรมแบบ “ถ้ายังไม่พังก็อย่าไปแตะมัน”