ระบบปลั๊กอินคือภาษีด้านประสิทธิภาพ
คุณติดตั้ง VS Code มันเร็ว คุณเพิ่มส่วนขยาย 15 ตัว ตอนนี้ใช้เวลา 4 วินาทีในการเริ่มต้นและ Extension Host กิน RAM 800 MB เกิดอะไรขึ้น?
รูปแบบนี้ซ้ำแล้วซ้ำเล่าทุกที่: WordPress, Eclipse, Chrome, Figma, Slack แอปส่งมาเร็ว ปลั๊กอินทำให้มันช้า ไม่มีใครแปลกใจอีกแล้ว — เรายอมรับว่ามันเป็นต้นทุนของความสามารถในการขยาย
แต่ระบบปลั๊กอินไม่ใช่แค่ปัญหาประสิทธิภาพ มันเป็นปัญหาปรัชญาการออกแบบ อุตสาหกรรมสับสนระหว่าง "ความสามารถในการขยาย" กับ "ไดนามิซึมในรันไทม์" ในขณะที่คำตอบที่ดีกว่ามักเป็นการผสมรวมในเวลาคอมไพล์ ปลั๊กอินเดียวที่มีประสิทธิภาพคือปลั๊กอินที่หยุดเป็นปลั๊กอินในเวลาคอมไพล์
สเปกตรัมประสิทธิภาพของความสามารถในการขยาย
ไม่ใช่ทุกความสามารถในการขยายที่มีต้นทุนเท่ากัน มีสเปกตรัมจากศูนย์ต้นทุนถึงต้นทุนสูงสุด และอุตสาหกรรมส่วนใหญ่ตั้งอยู่ที่ปลายที่แพง:
- การลิงก์แบบสแตติก / โมดูลในเวลาคอมไพล์ — ไม่มีโอเวอร์เฮด ไลบรารี C, Rust crates, แพ็กเกจ Go ขอบเขตโมดูลหายไปทั้งหมดในไบนารีสุดท้าย
- ไลบรารีที่แชร์โหลดเมื่อเริ่มต้น — แทบไม่มี โมดูล nginx, โมดูลเคอร์เนล Linux ต้นทุนครั้งเดียวเมื่อโหลด จากนั้นเป็นการเรียกฟังก์ชันโดยตรง
- การ dispatch แบบไดนามิกผ่านอินเทอร์เฟซ / vtables — โอเวอร์เฮดน้อย ปลั๊กอินเอนจินเกมใน C++ การ indirection ของตัวชี้หนึ่งครั้งต่อการเรียก
- ปลั๊กอินที่ตีความในโปรเซสเดียวกัน — ปานกลาง ปลั๊กอิน PHP ของ WordPress, OSGi bundles ของ Eclipse ทุกการเรียกปลั๊กอินผ่านตัวตีความ
- ปลั๊กอินในโปรเซสแยกผ่าน IPC — มาก ส่วนขยาย VS Code, ส่วนขยาย Chrome ทุกการโต้ตอบข้ามขอบเขตโปรเซสและซีเรียลไลซ์ข้อมูล
- ปลั๊กอินใน sandbox ผ่าน IPC ที่ซีเรียลไลซ์ — หนัก ปลั๊กอิน Figma, content scripts ของส่วนขยายเบราว์เซอร์ การซีเรียลไลซ์ การดีซีเรียลไลซ์ และการบังคับใช้ sandbox ในทุกการเรียก
ข้อมูลเชิงลึกสำคัญ: ปลั๊กอินเดียวที่มีประสิทธิภาพคือปลั๊กอินที่หยุดเป็นปลั๊กอินในเวลาคอมไพล์ ระดับ 1 และ 2 เร็วเพราะ "ปลั๊กอิน" กลายเป็นสิ่งที่แยกแยะไม่ออกจากโค้ดโฮสต์ในอาร์ติแฟกต์สุดท้าย
ความเสียหายในโลกจริง
WordPress
ทุกปลั๊กอินเชื่อมต่อกับวงจรชีวิตของคำขอ 30 ปลั๊กอินหมายถึง 30 ชั้นของการเรียกฟังก์ชันต่อการโหลดหน้า ผลลัพธ์: ปลั๊กอิน caching มีอยู่เพียงเพื่อบรรเทาความเสียหายของปลั๊กอินอื่น ปลั๊กอินประสิทธิภาพเพื่อแก้ปัญหาประสิทธิภาพที่ปลั๊กอินสร้างขึ้น ความย้อนแย้งเขียนตัวเอง
VS Code
ส่วนขยายแชร์ event loop Node.js เดียวในโปรเซสแยก ส่วนขยายที่ทำงานผิดหนึ่งตัวบล็อกทั้งหมด Extension Host มักปรากฏเป็นตัวใช้ CPU อันดับต้นๆ บนเครื่องของนักพัฒนา Microsoft ได้สร้างเครื่องมือ profiling คำสั่ง bisect และระบบเหตุการณ์การเปิดใช้งาน — โครงสร้างพื้นฐานทั้งหมดเพื่อจัดการปัญหาที่ส่วนขยายสร้างขึ้น
Eclipse
เรื่องเตือนใจ การแก้ไข bundle OSGi โอเวอร์เฮดการโหลดคลาส กราฟ dependency ขนาดใหญ่ ครั้งหนึ่งเคยเป็น IDE ที่ได้รับความนิยมมากที่สุด ตอนนี้ถูกทิ้งโดยนักพัฒนากระแสหลักเป็นส่วนใหญ่ สถาปัตยกรรมปลั๊กอินที่ควรจะเป็นจุดแข็งที่ยิ่งใหญ่ที่สุดกลับกลายเป็นจุดอ่อนที่กำหนดตัวตน
ตัว Electron เอง
ปัญหาปลั๊กอินในระดับแพลตฟอร์ม ทุกแอป Electron ส่งรันไทม์ Chromium + Node.js เต็มรูปแบบ VS Code คือ Electron Slack คือ Electron Discord คือ Electron แต่ละตัวใช้ RAM 300–500 MB อย่างอิสระเพื่อเรนเดอร์สิ่งที่เป็นหน้าต่างแชทหรือเท็กซ์เอดิเตอร์โดยพื้นฐาน "ปลั๊กอิน" ที่นี่คือแพลตฟอร์มเว็บทั้งหมด บันเดิลใหม่สำหรับทุกแอปพลิเคชัน
ทำไมอุตสาหกรรมยังคงเลือกปลั๊กอิน
ถ้าปลั๊กอินแพงขนาดนั้น ทำไมทุกคนยังคงสร้างพวกมัน? เหตุผลส่วนใหญ่เป็นเรื่ององค์กร ไม่ใช่เทคนิค:
- ประสบการณ์นักพัฒนา — ปลั๊กอินเขียนง่ายเมื่อคุณไม่สนใจเรื่องประสิทธิภาพ ส่งไฟล์ JS เชื่อมต่อกับอีเวนต์บางอย่าง เสร็จ
- การเติบโตของระบบนิเวศ — ปลั๊กอินสร้างผลกระทบเครือข่ายและการมีส่วนร่วมของชุมชน marketplace ที่มี 30,000 ส่วนขยายเป็นคูน้ำที่ทรงพลัง
- ความสะดวกขององค์กร — ปลั๊กอินให้ทีมเลื่อนการตัดสินใจออกแบบ "มีคนจะเขียนปลั๊กอินสำหรับสิ่งนั้น" เป็นสถาปัตยกรรมที่เทียบเท่ากับ "เราจะแก้ไขในขั้นตอนหลังการผลิต"
- โมเดลธุรกิจ — marketplace ปลั๊กอินสร้างรายได้และ lock-in แพลตฟอร์มจับมูลค่าจากระบบนิเวศ
ความจริงที่ไม่สบายใจ: ปลั๊กอินมักเป็นวิธีหลีกเลี่ยงการตัดสินใจสถาปัตยกรรมที่ยากเกี่ยวกับสิ่งที่ควรอยู่ในแกนกลาง พวกมันให้คุณส่งบางสิ่งที่ไม่สมบูรณ์และเรียกมันว่า "ขยายได้"
ทางเลือก: การผสมรวมในเวลาคอมไพล์
จะเป็นอย่างไรถ้าความสามารถในการขยายเกิดขึ้นในเวลา build แทนที่จะเป็นรันไทม์?
นี่ไม่ใช่สมมติฐาน มีตัวอย่างที่พิสูจน์แล้วในภาษาระบบ:
- Rust proc macros — โค้ดที่รันในเวลาคอมไพล์และสร้างโค้ดเนทีฟที่ไม่มีโอเวอร์เฮด การซีเรียลไลซ์ Serde, การตั้งค่ารันไทม์ Tokio, การเราต์ Axum — ทั้งหมดแก้ไขก่อนที่โปรแกรมของคุณจะเริ่ม
- Zig comptime — การรันในเวลาคอมไพล์ที่กำจัดการแยกสาขาในรันไทม์ทั้งหมด โครงสร้างข้อมูลเจเนอริกถูกมอโนมอร์ฟไอซ์ การกำหนดค่าถูกแก้ไข โค้ดที่ตายถูกกำจัด สิ่งที่เหลือคือสิ่งที่รันจริง
- C++ templates / constexpr — โพลีมอร์ฟิซึมในเวลาคอมไพล์ที่ไม่มีต้นทุนในรันไทม์ STL บรรลุประสิทธิภาพที่ยอดเยี่ยมเพราะทุกอัลกอริทึมเจเนอริกเฉพาะทางในเวลาคอมไพล์
- Tree-shaking ใน bundlers — เวอร์ชันบางส่วนที่ไม่สมบูรณ์แบบของแนวคิดนี้ที่นำไปใช้กับ JavaScript Webpack และ Rollup กำจัด exports ที่ไม่ได้ใช้ในเวลา build ข้อจำกัดคือมันทำได้แค่ลบโค้ด ไม่ใช่เฉพาะทาง
รูปแบบสอดคล้องกัน: ย้ายการตัดสินใจจากรันไทม์ไปเวลา build สิ่งที่คุณไม่รวมไม่มีต้นทุน สิ่งที่คุณรวมคอมไพล์เป็นโค้ดเนทีฟโดยไม่มีการ indirection ขอบเขตโมดูลกลายเป็นเครื่องมือจัดระเบียบระดับซอร์สโค้ด ไม่ใช่ขอบเขตประสิทธิภาพในรันไทม์
สิ่งนี้หมายความว่าอย่างไรสำหรับ TypeScript
TypeScript เป็นภาษาที่ได้รับความนิยมมากที่สุดสำหรับสร้างเครื่องมือที่ขยายได้ — และแย่ที่สุดด้านประสิทธิภาพรันไทม์ ระบบนิเวศ TypeScript ทั้งหมดรันบน Node.js ซึ่งรันบน V8 ซึ่ง JIT-คอมไพล์ JavaScript ทุกเลเยอร์เพิ่มโอเวอร์เฮด: เวลาอุ่น JIT, การหยุดเก็บขยะ, dynamic dispatch สำหรับทุกการเข้าถึงคุณสมบัติ, ขอบเขต IPC ระหว่างโปรเซส
นี่คือจุดที่ Perry เข้ามา Perry คอมไพล์ TypeScript โดยตรงเป็นไบนารีเนทีฟ ไม่มี V8 ไม่มีการอุ่น JIT ไม่มีการหยุดเก็บขยะ ไม่มีขอบเขต IPC
เมื่อโมดูลของคุณคอมไพล์เป็นโค้ดเนทีฟ "ปลั๊กอิน" กลายเป็นแค่... โมดูล มันผสมรวมในเวลา build ไบนารีสุดท้ายมีโอเวอร์เฮดปลั๊กอินเป็นศูนย์เพราะไม่มีปลั๊กอิน — แค่โค้ดเนทีฟ ตัวจัดการเส้นทาง Express, ฟังก์ชัน middleware, ไลบรารียูทิลิตี้ — ทั้งหมดคอมไพล์เป็นการเรียกฟังก์ชันโดยตรงในไบนารีเดียวกัน ไม่มีการโหลดแบบไดนามิก ไม่มีการซีเรียลไลซ์ ไม่มีขอบเขตโปรเซส
# Your app, your dependencies, your "plugins" — one binary
$ perry compile server.ts -o server
Compiling server.ts + 43 modules...
✓ Built executable: server (1.8 MB, 0.7s)
$ ./server
Listening on port 3000
นี่ไม่ใช่ทฤษฎี Perry คอมไพล์เฟรมเวิร์ก TypeScript จากโลกจริงแล้ว — Hono, tRPC, Strapi — เป็นไบนารีเนทีฟ ARM64 ขนาดต่ำกว่า 2 MB ในเวลาน้อยกว่าหนึ่งวินาที โมดูลที่ประกอบเป็นเฟรมเวิร์กเหล่านั้นถูกคอมไพล์ ลิงก์ และอินไลน์เข้าไปในไฟล์เรียกทำงานเดียว สิ่งที่จะเป็นสถาปัตยกรรมปลั๊กอินที่มีโอเวอร์เฮดรันไทม์ใน Node.js กลายเป็นการผสมรวมที่ไม่มีต้นทุนในไบนารี Perry
ความสามารถในการขยายที่คุณต้องการจริงๆ
การคัดค้านนั้นชัดเจน: "แต่ฉันต้องการความสามารถในการขยายในรันไทม์ ผู้ใช้ต้องติดตั้งปลั๊กอินโดยไม่ต้องคอมไพล์ใหม่"
จริงหรือ? สำหรับแอปพลิเคชันส่วนใหญ่ ชุดส่วนขยายเป็นที่รู้จักในเวลา build คุณเลือก middleware Express, ไดรเวอร์ฐานข้อมูล, ไลบรารี auth, เฟรมเวิร์กการ logging ของคุณ — แล้วก็ deploy "ความสามารถในการขยาย" อยู่ใน package.json ของคุณ แก้ไขเมื่อ npm install ไม่ใช่ในรันไทม์
แอปพลิเคชันที่ต้องการการโหลดปลั๊กอินในรันไทม์จริงๆ — VS Code, WordPress, เบราว์เซอร์ — เป็นข้อยกเว้น ไม่ใช่กฎ และแม้แต่สิ่งเหล่านั้นก็จ่ายราคาสูงสำหรับมัน สำหรับทุกอย่างอื่น การผสมรวมในเวลาคอมไพล์ให้ความยืดหยุ่นเดียวกันโดยไม่มีโอเวอร์เฮด
ความแตกต่างคือความซื่อสัตย์ทางสถาปัตยกรรม แทนที่จะแกล้งทำเป็นว่าทุกแอปพลิเคชันต้องการระบบปลั๊กอิน คุณถาม: ความสามารถในการขยายนี้ต้องเกิดขึ้นในรันไทม์ หรือคอมไพเลอร์สามารถทำงานได้?
เส้นทางข้างหน้า
การติดสถาปัตยกรรมปลั๊กอินของอุตสาหกรรมเป็นอาการของการยอมรับโอเวอร์เฮดรันไทม์ว่าหลีกเลี่ยงไม่ได้ มันไม่ใช่ คอมไพเลอร์สามารถทำงานได้ การผสมรวมในเวลา build ให้ความสามารถในการขยายโดยไม่มีภาษี
เราสร้าง Perry เพราะเราเชื่อว่านักพัฒนา TypeScript สมควรได้รับประสิทธิภาพเนทีฟโดยไม่ต้องสละภาษาที่พวกเขารัก โมดูลของคุณควรผสมรวมในเวลา build คอมไพล์เป็นการเรียกฟังก์ชันโดยตรง และรันโดยไม่มีโอเวอร์เฮดของรันไทม์ที่มีอยู่เพียงเพื่อทำให้ "ความสามารถในการขยาย" เป็นไปได้
ระบบปลั๊กอินที่เร็วที่สุดคือระบบที่ไม่มีอยู่ในรันไทม์