Tree Shaking: อัลกอริทึมที่เข้าใจผิดในเชิงพืชสวน (2023)
(wingolog.org)- WebAssembly ประสบความสำเร็จในการนำ โปรแกรม C++ ขนาดใหญ่ อย่าง Photoshop มาไว้บนเว็บ แต่ในแอปที่เน้น DOM การแพร่หลายยังถูกจำกัดเพราะมีโมเดลการเขียนโปรแกรมต่างจาก JavaScript
- การรองรับ Wasm GC และ reference types ในเบราว์เซอร์เปิดโอกาสให้ภาษาที่มีหน่วยความจำแบบจัดการ เช่น Python และ Scheme แต่บนเว็บ ขนาดที่ต้องส่งผ่านเครือข่ายกลายเป็นอุปสรรคต่อการนำไปใช้
- โปรแกรม Wasm แบบง่ายของ Go มีขนาด 2MB และเมื่อเพิ่ม import อาจเกิน 10MB ส่วน Pyodide REPL ต้องดาวน์โหลดประมาณ 20MB จึงเป็นภาระสำหรับเว็บแอปทั่วไป
- คอมไพเลอร์ Hoot Scheme ตั้งเป้าไปที่ Wasm ที่รองรับ GC และลดหน่วยคอมไพล์ “main” ขั้นต่ำลงเหลือประมาณ 70KB ขณะที่หน่วยคอมไพล์เสริมอาจต่ำกว่า 1KB ได้
- Tree shaking ที่มีประสิทธิภาพไม่ใช่แค่การลบฟังก์ชันที่ไม่ถูกอ้างอิง แต่เป็นปัญหาระดับคอมไพเลอร์ที่ต้องผสาน flow analysis เข้ากับการออกแบบไลบรารีมาตรฐาน
พื้นที่ที่ WebAssembly ทำผลงานได้บนเว็บ
- WebAssembly ไม่ได้แพร่หลายกว้างขวางเท่าความคาดหวังช่วงแรกบนเว็บ แต่ก็ประสบ ความสำเร็จแบบจำกัด ในบางพื้นที่
- ตัวอย่างเด่นคือกรณีที่นำโปรแกรม C++ ขนาดใหญ่มาไว้บนเว็บ เช่น Photoshop
- Figma ก็ถูกกล่าวถึงในฐานะกรณีตัวอย่าง Wasm เมื่อ 5 ปีก่อน แต่ปัจจุบันไม่ได้ชู Wasm มากนัก
- ไลบรารี NPM ขนาดเล็กที่คอมไพล์จาก C++ หรือ Rust มักใช้ Wasm ภายใน
- Blazor อาจถูกใช้ในแอปองค์กรภายในบางส่วน แต่ก็อาจมีการตลาดที่พูดเกินจริงอยู่บ้าง
- เดโม 3D FPS ของ Unreal Engine เป็นการทดลองที่อิงจากรีลีสหลักก่อนหน้าเมื่อกว่า 5 ปีก่อน และปัจจุบัน Unreal 5 ไม่รองรับเป้าหมาย WebAssembly
เหตุผลที่ Wasm ติดขัดในแอปที่เน้น DOM
- WebAssembly ประสบความสำเร็จนอกสภาพแวดล้อมเว็บ และอาจมีความสำคัญมากขึ้นบนแพลตฟอร์มเว็บ แต่บนเว็บอาจมองได้ว่ากำลังเพิ่งพ้นช่วงหุบเหวแห่งความผิดหวัง
- Wasm มีจุดแข็งในงานที่ JavaScript ทำได้ไม่ดี หรือในงานที่ต้องการ การใช้งานร่วมกันของ implementation ระหว่างไคลเอนต์กับเซิร์ฟเวอร์
- Wasm ไม่ประสบความสำเร็จในแอปที่เน้น DOM
- ไม่มีใครพูดถึงการเขียนฟรอนต์เอนด์ของ wordpress.com ใหม่ด้วย Wasm
- โมเดลการเขียนโปรแกรมหลักของเว็บคือ JavaScript ซึ่งมี dynamic typing และ managed memory
- WebAssembly 1.0 ถูกออกแบบโดยมี static typing และ linear memory เป็นแกนหลัก
- การเข้าถึง DOM จาก Wasm ยุ่งยากในระดับที่มีแต่ผู้สนับสนุน Wasm อย่างกระตือรือร้นเท่านั้นที่รับมือได้
- ภาษาอย่าง C# ต้องแจกจ่าย garbage collector ไปด้วย และจุดนี้กลายเป็นอุปสรรคต่อการนำ Wasm ไปใช้สำหรับภาษาที่ไม่ใช่ C/Rust
ปัญหาขนาดการส่งผ่านที่ยังเหลืออยู่หลัง Wasm GC
- เบราว์เซอร์จะให้การรองรับ reference types และ garbage collection ภายในไม่กี่เดือนข้างหน้า
- Chrome และ Firefox มี Wasm GC แล้ว
- Safari ก็น่าจะตามมาในไม่ไกลนักด้วยผลงานของ Asumu Takikawa
- Wasm GC เป็นความเปลี่ยนแปลงที่ทำให้ภาษาเพิ่มขึ้นต้องอัปเดต toolchain เพื่อรองรับ WebAssembly
- หากเว็บ Wasm จะประสบความสำเร็จ คอมไพเลอร์ต้องสร้าง โค้ดขนาดเล็ก ได้
- หาก toolchain ของภาษาสามารถสร้างไฟล์ Wasm ที่มีประโยชน์ในระดับไม่กี่ KB ตามเกณฑ์การส่งผ่านได้ ก็จะได้เปรียบ
- มิฉะนั้นจะต้องพึ่งความคาดหวังที่เกินจริงหรือฐานผู้ใช้ที่ถูกผูกไว้ และอาจอยู่ในสมดุลที่ไม่มั่นคงจนกว่าจะพบทางออกถัดไป
- ใน ecosystem ของ JavaScript มีอุตสาหกรรมเครื่องมือขนาดใหญ่สำหรับลดขนาดที่ส่งมอบและลดความบวมของโค้ดอยู่แล้ว
- bundler อย่าง esbuild รวมโมดูล JS หลายตัวเป็นไฟล์เดียว
- พยายามรวมเฉพาะฟังก์ชันและชนิดข้อมูลที่ถูกใช้
- ใช้กลยุทธ์ลดขนาดอย่าง minification เช่น การย่อชื่อด้วย
กับดักของชื่อ Tree Shaking
- Tree shaking มี อุปมาเชิงภาพ ว่าเหลือไว้เฉพาะโค้ดที่จำเป็นสำหรับหน้าหนึ่ง ๆ แล้วสลัดส่วนที่เหลือทิ้ง
- ในอุปมานี้ โมดูลถูกจินตนาการเป็นกิ่งไม้ และนิยามเป็นใบไม้ แต่ต้นไม้จริง ๆ ไม่ได้บอกเราว่ากิ่งไหนจำเป็นหรือไม่จำเป็นเพียงเพราะเราจับลำต้นแล้วเขย่า
- ชื่อนี้ทำให้คิดไปในทางลบโค้ดที่ไม่จำเป็น แต่ในเชิงอัลกอริทึม การหาจุดตรึงที่คงไว้เฉพาะโค้ดที่จำเป็นน่าจะเหมาะสมกว่า
- ถึงอย่างนั้น tree shaking ก็เป็นชื่อที่ติดหู จึงยังถูกใช้ต่อไปแม้จะไม่แม่นยำทั้งในเชิงพืชสวนและเชิงอัลกอริทึม
กำแพงขนาดที่ runtime หนาสร้างขึ้น
- ในภาษาที่มี runtime หนา การทำ tree shaking ให้ได้สูงสุดไม่เคยเป็นลำดับความสำคัญใหญ่
- ในการรองรับ WebAssembly ของ Go แม้แต่โปรแกรมที่ง่ายที่สุดก็มีขนาด 2MB ตาม golang wiki
- หากเพิ่ม import ก็อาจเกิน 10MB
- ตัวอย่าง REPL ของ Pyodide ซึ่งเป็นพอร์ต Python ไป WebAssembly ดาวน์โหลดข้อมูลประมาณ 20MB
- ขนาดเช่นนี้โอเคสำหรับเดโมเทคโนโลยีหรือแอปพลิเคชันที่สมบูรณ์มาก ๆ แต่ยากจะเป็น ตัวเลือกการพัฒนาเว็บ ทั่วไป
Toolchain ทางเลือกและ implementation ที่ปรับให้เหมาะกับแพลตฟอร์ม
- การรองรับ Wasm ในตัวของ Go และ Pyodide สืบทอดมาจาก upstream toolchain และบนเซิร์ฟเวอร์ ขนาดไบนารีอาจไม่ได้สำคัญมากนัก
- เมื่อกำหนดเป้าหมายเป็นอุปกรณ์ขนาดเล็ก มักมี implementation แยกต่างหากเกิดขึ้น
- แบ็กเอนด์ Wasm ของ TinyGo ดูเหมือนจะลดลงไปได้ต่ำกว่า 1KB
- Toolchain ทางเลือกเหล่านี้มักมาพร้อม ข้อจำกัดหรือความแตกต่างเฉพาะ
- โปรแกรม Python ที่คอมไพล์เป็น Wasm เพื่อรันในสภาพแวดล้อม DOM ย่อมแตกต่างจากโปรแกรม Python แบบ “native”
- ผู้เขียน toolchain พยายามให้ภาษาเดียวกัน แต่ implementation ของไลบรารีมาตรฐานอาจต่างกันได้
- นักพัฒนา ClojureScript ก็คงอยากลบ เอกสารความแตกต่างกับ Clojure ออกหากเป็นไปได้ และหาก Wasm กลายเป็นเป้าหมายที่ใช้งานได้จริงของ ClojureScript ความเป็นไปได้นั้นก็จะเกิดขึ้น
วิธีทำ Tree Shaking ของ Hoot Scheme
- หลังมีการรองรับ GC แล้ว Wasm ทำให้สามารถคิดถึงการเขียนโปรแกรม DOM ด้วยภาษาอย่าง Python ได้ แต่การใช้งานในวงกว้างต้องการโมดูลขนาดเล็ก
- Hoot Scheme compiler ตั้งเป้าไปที่ Wasm ที่มี GC
- ปัจจุบันหน่วยคอมไพล์ “main” ขั้นต่ำมีขนาดประมาณ 70KB
- ตั้งเป้าให้มีขนาดต่ำกว่านี้
- หน่วยคอมไพล์เสริมที่ import ฟีเจอร์ runtime เช่น exception handler จากโมดูล main อาจต่ำกว่า 1KB ได้
- คอมไพเลอร์ Hoot ใส่ prelude ไว้หน้าสุดของโค้ดผู้ใช้
- Tree shaking เกิดขึ้นในหลายขั้นตอน
- partial evaluation สามารถประเมินเฉพาะผลข้างเคียงของ binding ที่ไม่ถูกใช้ แล้วลบทิ้งได้
- fixing letrec ก็ทำงานคล้ายกัน
- CPS เดินผ่านโปรแกรมบ่อยครั้ง และตามเฉพาะฟังก์ชัน ค่า และเส้นควบคุมการไหลที่ถูกอ้างอิง
- pass dead-code elimination แบบชัดเจนลบการกำหนดค่าที่ไม่ถูกใช้และไม่มีผลข้างเคียง ซึ่งอาจเกิดขึ้นหลังการปรับแต่งอื่น ๆ
- นิยามใน ไลบรารีมาตรฐาน ที่เขียนด้วย WebAssembly ใกล้เคียง raw จะถูกใส่ในไบนารีผลลัพธ์เฉพาะเมื่อจำเป็น
สิ่งที่ลบง่ายและสิ่งที่ลบยาก
- นิยาม procedure เช่น ฟังก์ชันหรือ closure ค่อนข้างจัดการง่าย
- เพียงรวมเฉพาะฟังก์ชันที่โค้ดอ้างถึงก็พอ
- ในภาษาอย่าง Scheme แค่นี้ก็ให้ผลมากแล้ว
- ความยากที่เห็นได้ทันทีมีสามอย่าง
-
โมเดลการประเมินของ letrec*
- scope ของนิยามใน prelude เป็นแบบ recursive แต่มีลำดับ
- ค่า binding สามารถเรียกหรืออ้างอิงค่าที่นิยามก่อนหน้า และอาจ capture ค่าที่จะนิยามภายหลังได้ด้วย
- หากการประเมินค่า binding จำเป็นต้องอ้างอิงค่าที่จะนิยามทีหลัง จึงเป็นข้อผิดพลาด
- โดยมากแล้วไม่เป็นปัญหากับ procedure แต่สำหรับนิยามที่ไม่ใช่ procedure คอมไพเลอร์อาจพิสูจน์คุณสมบัติว่า “อ้างอิงเฉพาะ binding ก่อนหน้า” ไม่ได้
- ในกรณีนี้ อัลกอริทึม fixing letrec reloaded อาจเหลือ binding ที่ถูก set! ไว้ และต้องใช้ pass DCE ที่ละเอียดอ่อนจึงจะลบออกได้
-
vtable ของชนิด record
- นิยามที่ไม่ใช่ procedure บางส่วนคือ ชนิด record
- ชนิด record มี vtable ที่บรรจุวิธีแสดงผล record หรือวิธีตรวจสอบ instance เป็นต้น
- callback ใน vtable อาจทำให้โค้ดจำนวนมากยังมีชีวิตอยู่ แม้ไม่ได้ถูกใช้จริง
-
ฟังก์ชันแสดงผลแบบ polymorphic
- ฟังก์ชัน polymorphic อย่าง
displayขยายขอบเขตโค้ดที่จำเป็นอย่างมาก - หากเรียก
displayเพื่อพิมพ์สตริง จะดึง facility สำหรับ buffered I/O ทั้งหมดเข้ามา - เพราะ
displayสามารถพิมพ์อะไรก็ได้ โค้ดสำหรับกรณีต่าง ๆ เช่น bitvector และ pair ก็อาจถูกดึงมาด้วย - หากเรียก
write-stringที่ใช้เฉพาะสตริง ก็หลีกเลี่ยงโค้ดแสดงผลข้อมูลทั่วไปได้ แต่ยังรวม facility สำหรับ buffered I/O ทั่วไปอย่าง port อยู่ดี
- ฟังก์ชัน polymorphic อย่าง
Tree Shaking ที่เหมาะสมที่สุดคือปัญหา Flow Analysis
- Tree shaking ที่เหมาะสมที่สุด ท้ายที่สุดแล้วคือปัญหา flow analysis
- หากโปรแกรมไม่มี bitvector เลย โค้ดที่จัดการ bitvector ภายใน
displayก็อาจกลายเป็น dead code ได้ - หากต้องการรู้เรื่องนี้ ต้องรู้ว่า
displayถูกเรียกด้วยอาร์กิวเมนต์ชนิดใด และจำเป็นต้องใช้ high-level flow analysis - ใน Python ปัญหานี้ยิ่งยากขึ้น
- object-oriented dispatch เป็นการเขียนโปรแกรมแบบ higher-order ดังนั้น
foo.barหมายถึงอะไรจึงขึ้นกับว่าfooคืออะไร - การ lookup ของ Python เป็นแบบ dynamic มากกว่า Scheme และเมธอดอย่าง
__getattr__ก็อาจถูกใช้ทั่วไป - ในทางปฏิบัติ flow analysis อาจสามารถตัดการ lookup แบบ dynamic เหล่านี้ออกได้
- เป้าหมายของ tree shaking ใน Python ไม่ใช่ term ขนาดใหญ่ที่มี lexical binding แต่เป็นชุดโมดูลที่ซับซ้อน
- เรื่องนี้คล้ายกับ JavaScript แต่ Python ยังไม่มี ecosystem ของ bundler สำหรับ tree shaking ที่เป็นที่ยอมรับ
- object-oriented dispatch เป็นการเขียนโปรแกรมแบบ higher-order ดังนั้น
เงื่อนไขของ toolchain ภาษา Wasm บนเว็บ
- Wasm GC อาจทำให้การเขียนโปรแกรม DOM ด้วยภาษาที่ไม่ใช่ JavaScript เป็นไปได้
- หากจะนำไปสู่การใช้งานในวงกว้าง โมดูล Wasm ผลลัพธ์ต้องมีขนาดเล็ก
- ต้องมีการลงทุนอย่างมากใน toolchain ของแต่ละภาษา
- การลงทุนเช่นนี้มักปรากฏในรูปของ toolchain ทางเลือก ที่รวมอัลกอริทึม tree shaking เชิงทดลอง
- ไลบรารีมาตรฐานทางเลือกจำเป็นต้องถูกออกแบบให้ tree shaker ทำงานได้ดีขึ้น
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
openEtG ยังคงรักษา Wasm blob (เอนจินเกมการ์ด) ให้มีขนาด ต่ำกว่า 400KB พร้อมกับย้ายลอจิกจำนวนมาก เช่น การสร้างข้อความบนการ์ด ไปไว้ใน Wasm และเขียนด้วย Rust
เพื่อลดขนาด จึงต้องจัดการหลายอย่าง เช่น ใช้เลขคณิตแบบ fixed-point แทน floating-point, ย้ายจาก hash map ไปใช้ vector, หลีกเลี่ยง string, ใช้ allocator ขนาดเล็กอย่าง
talc, และลด dependenciesตอนนี้ใช้แค่
randกับfxhashและดูเหมือนว่าrandก็อาจถอดออกได้ ส่วนfxhashใช้เพียงเพื่อตรวจสอบการ desync ด้วยการทำแฮชสถานะของเกมยังลดจำนวนชนิดของ generic instantiation ด้วย เช่น มี
Vecอยู่แล้วก็พยายามไม่ดึงชนิดอย่างBox<[i16]>เข้ามาเพิ่ม และการตัด floating-point กับ hash map ออกก็ช่วยลดความหลากหลายของ type ได้เช่นกันอัลกอริทึมเองก็ออกแบบโดยคำนึงถึงขนาดด้วย ตัวอย่างเช่นใช้ตารางค้นหาแบบ bit-packed เพื่อเข้ารหัสกลไก adrenaline ที่ทำให้สิ่งมีชีวิตพลังโจมตีต่ำโจมตีได้บ่อยกว่า
มีการเปรียบเทียบต้นทุนระหว่างการเก็บค่าที่ไม่บีบอัดกับต้นทุนของลอจิกถอดรหัส และสำหรับการประเมิน AI นั้น 64 ถูกเข้ารหัสใน WebAssembly ได้มีประสิทธิภาพกว่า 128 จึงใช้ ความแม่นยำคงที่ 6 บิต
กลไกการกำหนดเป้าหมายเดิมเคยเป็นรูปแบบ AST โดยให้แต่ละ predicate เป็น enum และเก็บ AND/OR เป็น expression slice แต่ตอนนี้เปลี่ยนมาเข้ารหัส expression แบบ Polish notation ลงในจำนวนเต็ม 32 บิต โดย AND/OR ใช้ 2 บิต และ predicate ใช้ 6 บิต
ในกรณีนี้ Polish notation ดีกว่า reverse Polish notation เพราะสามารถทำ short-circuit evaluation ของ AND/OR ได้
กำลังคิดอยู่ว่าในงานที่รู้ข้อกำหนดความละเอียดสูงสุดชัดเจน เช่น กรณีที่ไม่ต้องการความแม่นยำตำแหน่งต่ำกว่ามิลลิเมตร fixed-point จะช่วยได้หรือไม่ เลยอยากฟังประเด็นที่เกี่ยวข้องเพิ่มเติม
wasm-optของ binaryenดูเหมือนว่าจะลดขนาด Wasm ได้อย่างสม่ำเสมอราว 20–30%: https://github.com/WebAssembly/binaryen
เวลาเสิร์ฟ Wasm bundle ให้เบราว์เซอร์ ก็ควรใช้การบีบอัด Brotli และตั้งค่าให้เว็บเซิร์ฟเวอร์ใช้ไฟล์ที่บีบอัดด้วย Brotli ด้วย
ถ้าเป็น nginx แก้แค่บรรทัดเดียวก็ทำได้ และ Brotli ช่วยลดขนาด Wasm bundle ได้ประมาณ 3 เท่า ซึ่งดีกว่า gzip มาก
ขนาดนี้รวมทั้ง deep learning forward/backward pass, อัลกอริทึม reinforcement learning และการจำลองพลวัตไว้แล้ว
ตอนนี้ก็ยังไม่ได้หนักหนาอะไร แต่ก็สงสัยว่าน่าจะทำให้เล็กลงได้อีกแค่ไหน เลยอยากลองลดดูเร็ว ๆ นี้
อยากเข้าใจว่าคำพูดที่ว่าหลีกเลี่ยง floating-point แล้วใช้เลขคณิตแบบ fixed-point เพื่อประหยัดพื้นที่นั้นเป็นไปได้อย่างไร
โดยเฉพาะข้อ 6 อย่าง แนวทางใช้
VecแบบเดียวกับBoxดูไม่น่าจะทำให้ประหยัดได้มากนักชื่อ tree shaking ดูจะเป็นชื่อที่เรียกผิดอยู่พอสมควร
คอมไพเลอร์ Virgil เรียกสิ่งนี้ว่า “การวิเคราะห์ความสามารถในการเข้าถึง” และฝังไว้ในโมเดลการคอมไพล์
คอมไพเลอร์จะพาร์สโปรแกรมและโค้ดไลบรารี ตรวจสอบชนิด และรันโค้ดเริ่มต้น แต่หลังจากนั้นจะไล่สำรวจจากจุดเข้าใช้งานหลัก วิเคราะห์เฉพาะโค้ดที่เข้าถึงได้ และใส่เฉพาะส่วนนั้นลงในไบนารีสุดท้าย
มันยังสร้างโปรแกรมที่มีเพียงฟังก์ชัน
mainเดี่ยว ๆ โดยไม่มีระบบรันไทม์ได้ดี และระบบรันไทม์จำเป็นแค่สำหรับการติดตามสแตกกับการเก็บกวาดขยะเท่านั้น จึงละออกได้หากต้องการกรณีแรกที่พบคือเครื่องมือ Treeshaker ใน Lucid Common Lisp 4.1 เมื่อปี 1992 ซึ่งเป็นอิมพลีเมนเทชัน Common Lisp เชิงพาณิชย์สำหรับ UNIX
Lucid CL มีแนวคิดเรื่องอิมเมจ ซึ่งเป็นเมมโมรีดัมป์ที่บันทึกไว้ของ Lisp heap ระหว่างการทำงาน และแอปพลิเคชันประกอบด้วยอิมเมจกับรันไทม์
โดยปกติอิมเมจจะมีโค้ดและข้อมูลในหน่วยความจำเกือบทั้งหมด จึงมีความต้องการสร้างอิมเมจที่เล็กลงสำหรับการแจกจ่าย และ Treeshaker จะลบโค้ดและข้อมูลที่ตัดสินว่า “ไม่ได้ใช้” ก่อนบันทึกอิมเมจ
วิธีคือเล็มการเชื่อมต่อออกจากกราฟของข้อมูลและโค้ด Lisp ที่เข้าถึงได้ จากนั้นให้ GC หรือโค้ดพิเศษเก็บกวาดขยะเพื่อลดหน่วยความจำ แล้วค่อยดัมป์ออกมาเป็นอิมเมจที่เล็กลง
ดังนั้น Treeshaker จึงไม่ใช่เครื่องมือของคอมไพเลอร์ แต่เป็นเครื่องมือที่ลบโค้ดและข้อมูลที่ไม่ได้ใช้จาก Lisp heap
อิมเมจ Lisp แบบพื้นฐานมีแม้กระทั่งตัวคอมไพเลอร์ อินเทอร์พรีเตอร์ และอิมพลีเมนเทชัน REPL รวมอยู่ด้วย ดังนั้นถ้าขัดจังหวะโปรแกรมที่กำลังรันแล้วเข้า REPL ก็ยังสามารถใช้โค้ดทั้งหมดใน heap ที่กู้คืนจากอิมเมจได้
เพราะฉะนั้นการเอาแม้แต่คอมไพเลอร์หรือ REPL ออกไปจึงมีความหมาย
การวิเคราะห์ความสามารถในการเข้าถึงมักถูกใช้เพื่อตัดสินว่าส่วนใดของโค้ดลบได้ แต่ตัวการวิเคราะห์เองไม่ได้ลบโค้ด การลบเป็นขั้นตอนถัดไป
โดยปกติ “tree shaking” มักสื่อถึงการลบในระดับฟังก์ชัน ขณะที่ dead-code elimination ทำได้ละเอียดกว่านั้นมาก เช่น การลบกิ่งของนิพจน์เงื่อนไข และอาจอาศัยการวิเคราะห์แบบสแตติกได้หลากหลายชนิด
ตอนเห็นครั้งแรกก็เข้าใจความหมายได้ทันทีโดยไม่ต้องค้นเพิ่ม
มันคือการเขย่าต้นไม้ให้สิ่งที่เกาะหลวม ๆ หล่นลงมา และในที่นี้ก็ชัดเจนว่าแพ็กเกจที่ไม่ได้ใช้จะถูก “เขย่าจนหล่น” ออกจากต้นไม้นั้น
ถ้าจินตนาการไดอะแกรมซอร์สโค้ดเป็นวัตถุทางกายภาพ เมื่อเขย่ามัน สิ่งที่ไปไม่ถึงจากรากก็จะหลุดร่วงออกมา
มันแทบไม่ต่างจากการวิเคราะห์ความสามารถในการเข้าถึง เพียงแต่สำนวนหนึ่งชวนให้นึกภาพเชิงพื้นที่มากกว่า
โค้ดบางส่วนไม่ได้เชื่อมกับลำต้นที่เป็นจุดเข้าใช้งาน
tree shaking จะลบส่วนที่ไม่เชื่อมต่อเหล่านั้น นั่นคือใบที่ติดหลวม ๆ และกิ่งที่ตายแล้ว
ถ้าทูลเชนคอมไพเลอร์ของภาษาสามารถสร้าง Wasm ที่ใช้งานได้จริงขนาดเพียงไม่กี่ KB ในแง่การส่งผ่านเครือข่าย ก็จะเปิดความเป็นไปได้ใหม่ ๆ
ไบนารีที่เล็กมากจะเปิดกรณีใช้งานใหม่ของ Wasm และ WasmGC ก็น่าจะช่วยได้แน่นอน
Java และ Kotlin ก็ทำได้ดีพอสมควรแล้วในวันนี้ที่ราว 2~3KB: https://developer.chrome.com/blog/wasmgc, https://twitter.com/bashorov/status/1661377260274720770
แต่ต้องระวัง เพราะขึ้นอยู่กับว่าใช้ API อะไร อาจมีโค้ดก้อนใหญ่ถูกดึงติดมาด้วย
ถึงอย่างนั้น ภาษาเหล่านี้ก็ได้เปรียบ C++ และ Rust เรื่องขนาดโค้ดไปมากแล้ว เพราะด้วย WasmGC จึงไม่จำเป็นต้องใส่โค้ดจัดการหน่วยความจำหลาย KB ลงในบันเดิล
มันอาจช่วยได้เวลาจัดการอ็อบเจ็กต์ JavaScript และอาจใช้เป็นตัวจัดสรรหน่วยความจำทางเลือกที่มีประสิทธิภาพด้อยกว่าได้ด้วย
ถึงอย่างนั้น ตัวจัดสรรของ Rust แบบปกติบน Wasm ก็น่าจะใช้ได้ดีในกรณีส่วนใหญ่
ถ้าเริ่มปรับแต่งเพื่อให้ขนาดเล็กและใช้
wasm-optแล้วบีบอัดด้วย Brotli ก็สามารถยัดโค้ดจำนวนมหาศาลให้มีขนาดดาวน์โหลดต่ำกว่า 100KB ได้การเอาต้นทุนของ Wasm 100KB ไปเทียบตรง ๆ กับ JavaScript แบบบันเดิล 100KB เป็นความผิดพลาด เพราะ JavaScript ใช้เวลาพาร์สและเริ่มต้นช้ากว่าหลายเท่า
เวลาดาวน์โหลดเป็นต้นทุนจริงก็จริง แต่ในแง่เวลาแสดงผลหน้าจอแรก 100KB ของ Wasm ดีกว่า 100KB ของ JavaScript มาก
ถึงอย่างไร เล็กกว่าไว้ก่อนย่อมดีกว่า และก็น่าตื่นเต้นที่ Java, Kotlin, C#, Python และ Go อาจกลายเป็นภาษาที่ใช้งานได้จริงสำหรับเว็บแอปพลิเคชัน
อีกเรื่องที่น่าสนใจคือขนาดของแอปพลิเคชันจริงจะออกมาเป็นอย่างไร
ความต่างที่ใหญ่ที่สุดน่าจะมาจากการออกแบบเฟรมเวิร์ก และการ diff แบบ virtual DOM ก็ย่อมซับซ้อนและช้ากว่าไลบรารีคอมโพเนนต์เชิง reactive อย่าง Svelte, SolidJS และ Leptos ของ Rust อยู่เสมอ
ถ้า WasmGC ได้รับการรองรับทุกที่ ในแง่ประสิทธิภาพ การเลือกเว็บเฟรมเวิร์ก น่าจะมีผลมากกว่าภาษาอย่างมาก
สงสัยว่าคำพูดที่ว่า “Wasm ทำให้การเขียนโปรแกรม DOM ด้วยภาษาที่ไม่ใช่ JavaScript เป็นสิ่งที่นึกภาพได้จริง” นั้นจริงแค่ไหน
เท่าที่เข้าใจ ถ้าจะจัดการ DOM จากภาษาอย่าง Rust สุดท้ายก็ยังต้องมี binding ที่ serialize การเรียกไปฝั่ง JavaScript เพื่อให้ไปรันอยู่ดี
ในรูปแบบปัจจุบันของ Wasm มันยังคง ผูกติดอยู่กับ JavaScript ไม่ใช่หรือ
ในทางทฤษฎี ตอนนี้สามารถดึงฟังก์ชัน DOM จากรันไทม์ แล้วเรียกด้วยการอ้างอิงอ็อบเจ็กต์ DOM เพื่อข้าม JavaScript และเรียกรันไทม์โดยตรงได้
ยังไม่แน่ใจว่าในทางปฏิบัติทำได้จริงแค่ไหน แต่ GC อย่างน้อยก็ให้กลไกพื้นฐานที่จำเป็นสำหรับการไปถึงจุดนั้น
โดยพื้นฐานแล้วคล้ายกับ SolidJS
#[component] fn App() -> impl IntoView { let (count, set_count) = create_signal(0); view! { "Click me: "{move || count()} } }มี overhead ด้านขนาด Wasm เมื่อเทียบกับ JavaScript อยู่บ้าง แต่ไม่ถึงกับร้ายแรง
หลังใช้
wasm-optและบีบอัดด้วย Brotli แล้ว Wasm bundle ของแอปตัวนับนี้มีขนาด 37KB อยู่ในช่วงใกล้เคียงกับ React แต่หลังเริ่มรันแล้วเร็วกว่าเยอะยังไม่ได้ลองจัดการ DOM โดยตรง แต่สำหรับคอมโพเนนต์ทั่วไปก็ดูน่าใช้มาก
เช่น DOM ที่เขียนด้วย Rust สามารถถูกใช้จากโมดูล Wasm ที่เขียนด้วย Rust ได้โดยไม่ต้องมีการรัน JavaScript
แต่เนื่องจากบางส่วนของ DOM API ถูกกำหนดมาให้สอดคล้องกับ semantics ของ JavaScript เลยค่อนข้างยุ่งยาก และจึงดูเหมือนว่าจะเริ่มจากสิ่งที่มีมรดกจาก JavaScript น้อยกว่าอย่าง HTTP request, TCP socket และการเข้าถึงไฟล์ระบบก่อน
มีคำว่า “dead-code elimination” มาตั้งนานแล้ว เลยสงสัยว่าทำไมถึงเกิดคำว่า tree shaking ขึ้นมา
ส่วน “tree shaking” หมายถึง การวิเคราะห์ทั้งโปรแกรม เพื่อทิ้งทั้งโมดูลและฟังก์ชันที่ไม่ถูกเรียกใช้
ในเชิงแนวคิดมันคล้ายกัน แต่สำหรับคนเขียนคอมไพเลอร์มักต้องแยกกันทำ ดังนั้นการมีสองชื่อก็ช่วยได้
tree shaking เป็นคำที่นึกภาพตามได้ง่าย และพูดง่าย เข้าถึงง่ายกว่า “dead-code elimination” เลยดูจะกลายเป็นคำที่นิยมกว่า
ลองค้นดูว่าคำไหนมาก่อน พบว่าการใช้ “dead-code elimination” ที่เก่าสุดอยู่ในงานวิจัยปี 1973: https://research-repository.st-andrews.ac.uk/bitstream/handle/10023/22636/NicholasAlexandrakisMScThesis1973_original_C.pdf?sequence=1
ใน Google Scholar กลับหาไม่เจอการใช้ “tree shaking” หรือ “tree shaker” ในบริบทคอมพิวติ้ง ส่วนใหญ่เป็นเรื่องต้นไม้จริง ๆ อย่างพวกส้ม
การพูดถึงที่เก่าที่สุดที่ดูเหมือนจะมีคือโพสต์ใน comp.lang.lisp: https://groups.google.com/forum/#!topic/comp.lang.lisp/pspFr1XByZk
อุปมาเรื่อง tree shaking น่าจะมาจากวิธีเก็บเกี่ยวของต้นไม้ผลบางชนิดหรือเปล่า
เขย่าต้นไม้แล้วผลสุกก็ร่วงลงมา
แต่ในการเก็บเกี่ยวผลไม้ เราต้องการของที่ร่วงลงมา ขณะที่เวลาบันทึกภาพที่ serialize แล้ว เรากลับทิ้งสิ่งที่ร่วงลงมา ดังนั้นมันจึงไม่ใช่อุปมาที่ดีมากนัก
ผลไม้บอบบางกว่า ถ้าตกไกลเกินไปมักช้ำ เลยโดยมากเก็บด้วยมือ
กระบวนการเขย่าค่อนข้างรุนแรง แต่ไม่ได้ทำอันตรายกับต้นไม้
ปรัชญาของผมคือพยายามปรับขนาดและประสิทธิภาพใน JavaScript อย่างหนักด้วยการทำอัลกอริทึมและการออกแบบให้เรียบง่าย เพื่อเหลือขนาดบันเดิลและ CPU ไว้ใช้กับโค้ดที่ต้องคำนวณแบบหนัก ๆ
ในโปรเจกต์ ClubCompy มีการใช้ Wasm เพื่อทำ FAT file system บน local storage ซึ่งพบว่ามีต้นทุนการคำนวณสูงมาก
ช่วงหลังของปีนี้ก็วางแผนจะใช้ Wasm อีกครั้งเมื่อต้องใส่ระบบตรวจจับการชนของสไปรต์แบบแม่นยำระดับพิกเซลกลับเข้าไป
การทำครั้งแรกใช้ JavaScript ล้วน และเมื่อสไปรต์ 256 ตัวบนหน้าจอชนกันเอง เฟรมเรตตกลงไปต่ำกว่า 1fps
คิดว่าน่าจะย้ายไปประมวลผลบน worker thread ได้แทบจะฟรี ทำให้ไม่มีผลกระทบด้านประสิทธิภาพ
ที่เกม 2D ส่วนใหญ่ใช้กล่องชนแบบสี่เหลี่ยมก็มีเหตุผล เพราะผู้เล่นคาดเดาได้ง่ายว่าชนหรือไม่ชน
การชนระดับพิกเซลอาจทำให้การเคลื่อนที่แบบเดียวกัน บางครั้งชน บางครั้งไม่ชน ขึ้นอยู่กับเฟสของรอบแอนิเมชัน
ถ้าการเคลื่อนไหวที่ก่อนหน้านี้ทำได้เสมออยู่ ๆ ล้มเหลวเพราะจังหวะแอนิเมชันไม่ดี ก็จะรู้สึกแย่
ยิ่งไปกว่านั้น ระดับพิกเซลก็ไม่ได้สมจริงเสมอไป เพราะรายละเอียดเล็ก ๆ ของสไปรต์อาจเป็นองค์ประกอบอย่างผ้าหรือเส้นผม ซึ่งในความเป็นจริงไม่ได้ทำให้เกิดการชนแบบแข็งทื่อ
และบ่อยครั้งสไปรต์ก็ขยับทีละหลายพิกเซลในแต่ละเฟรม ทำให้การชนรายพิกเซลเพิ่มโอกาสที่จะทะลุผ่านกัน
ระบบตรวจจับการชนที่เรียบง่ายและคาดเดาได้มักเป็นทางเลือกที่ดีที่สุด
1fps ควรจะไปโผล่แถว ๆ ตอนมีการชนสัก 20,000 ครั้ง
ถ้าเป็นระดับพิกเซล และมีหน่วยความจำเหลือพอ ก็ใช้วิธีง่าย ๆ ได้โดยเรนเดอร์ offscreen canvas ของสนามทั้งหมด แล้ววาดแต่ละสไปรต์เป็น stencil คนละสี จากนั้นค่อยตรวจสอบมัน
เป็นเวลาเชิงเส้นและไม่ต้องแบ่งส่วนเพิ่มเติม
แต่ต้องระวังว่ามาตรการ anti-fingerprinting อาจเปลี่ยนบิตล่างของข้อมูลแคนวาสให้เป็น noise ดังนั้นอาจต้องใช้บิตบนแทน
บทความนี้พูดได้ถูกต้อง มีปัญหาเรื่องขนาดโค้ดใน Wasm
ในเบราว์เซอร์นี่เป็นปัญหาเพราะต้องดาวน์โหลดโค้ดทั้งหมดก่อนที่เว็บไซต์จะเริ่มทำงาน และในสถาปัตยกรรม serverless ก็เป็นปัญหาเช่นกัน เพราะระหว่างที่ไคลเอนต์รอ โค้ดจะถูกโหลดจาก cold storage ไปยังเซิร์ฟเวอร์ที่ต้องใช้
tree shaking อาจช่วยได้ แต่ดูเหมือนจะเป็นเพียงการปรับปรุงแบบค่อยเป็นค่อยไป
โดยพื้นฐานแล้ว เหตุผลที่โปรแกรม Wasm มักพองใหญ่ขึ้นคือมันต้องพก language runtime และ standard library ของแต่ละภาษามาทั้งชุด
ตรงกันข้าม JavaScript ได้รับ implementation และไลบรารีพื้นฐานจากเบราว์เซอร์
จึงอาจคิดไปได้ว่าเบราว์เซอร์คงไม่สามารถบรรจุ language runtime ของทุกภาษาไว้ล่วงหน้าได้ แต่ก็ควรพิจารณาแนวทางอื่นด้วย: shared libraries และ dynamic linking
WebAssembly รองรับ dynamic linking และสามารถโหลด Wasm หลายโมดูลพร้อมกันแล้วเรียกใช้ข้ามกันได้
แต่ toolchain ของ Wasm จำนวนมากกลับไม่พยายามรองรับสิ่งนี้ และถูกออกแบบให้ static link ทั้งโปรแกรมรวมถึง language runtime เป็นโมดูลขนาดใหญ่ก้อนเดียว
Pyodide (CPython on Wasm) เป็นตัวอย่างที่สวนทาง เพราะปัจจุบันถูกออกแบบโดยคำนึงถึง dynamic linking
นี่เองคือเหตุผลที่ Cloudflare Workers สามารถเพิ่มการรองรับ Python ระดับ first-class ได้เมื่อไม่นานมานี้: https://blog.cloudflare.com/python-workers
หากมองจากมุมของผู้รับผิดชอบทางเทคนิคของแพลตฟอร์ม Workers ทั้งหมด Workers ทุกตัวที่รันอยู่บนเครื่องเดียวกันสามารถแชร์ Pyodide runtime ที่คอมไพล์ไว้ชุดเดียวได้ จึงไม่จำเป็นต้องโหลดแยกสำหรับแต่ละ Worker
หาก dynamic linking ได้รับการรองรับอย่างแพร่หลายมากขึ้น ก็อาจจินตนาการถึงโครงสร้างที่เบราว์เซอร์โหลด language runtime ยอดนิยมไว้ล่วงหน้า หรือแม้แต่ไลบรารียอดนิยม แล้วทุกเว็บเพจที่ต้องใช้ runtime นั้นก็แชร์สำเนาโค้ดแบบอ่านอย่างเดียวชุดเดียวกันได้
runtime เหล่านี้ยังคงทำงานอยู่ภายใน sandbox ดังนั้นเบราว์เซอร์ไม่จำเป็นต้องเชื่อถือ เพียงแค่จัดเตรียมให้ก็พอ
วิธีนี้อาจทำให้สร้างเบราว์เซอร์ที่รองรับภาษาอื่นนอกเหนือจาก JavaScript แบบ “built-in” ได้ โดยที่ผู้ดูแลเบราว์เซอร์ไม่จำเป็นต้องตรวจสอบหรือทำความเข้าใจ implementation ของภาษาเหล่านั้นทั้งหมด
ผมไม่ได้รู้ลึกมาก แต่เข้าใจว่าวิธีนั้นไม่ได้ผลดีนัก
มีเวอร์ชันของไลบรารีมากเกินไปจนแต่ละเวอร์ชันไม่ได้ถูกใช้อย่างแพร่หลายจริง และต่อมาก็มีความกังวลด้านความเป็นส่วนตัวทำให้เบราว์เซอร์หันไปแยกแคชตามไซต์หรือแหล่งที่มา
แม้อาจไม่ได้ตั้งใจให้พึ่งแคช แต่เรื่องนี้ดูเป็นปัญหาเชิงสังคมที่ยุ่งยากมากกว่าปัญหาเชิงเทคนิค
หากเพิ่ม language runtime อีกจำนวนมากให้เป็นส่วนหนึ่งของการรองรับพื้นฐานของเบราว์เซอร์ ก็จะยิ่งเพิ่มอุปสรรคในการสร้างเบราว์เซอร์ใหม่ และก็ไม่สามารถรองรับไลบรารีกับ runtime ทุกอย่างที่ผู้คนต้องการได้อยู่ดี
ถ้าปล่อยให้แต่ละฝ่ายพกมาเองแล้วหวังพึ่งแคช ก็ยังต้องตอบให้ได้ว่าจะหลีกเลี่ยงปัญหาแบบเดียวกับที่เคยเจอในการแคช JavaScript libraries มาก่อนได้อย่างไร
ตัวอย่างเช่น อาจใส่ runtime และส่วนแกนของ standard library ที่หลายโปรแกรมใช้ไว้ใน Go shared library ได้
แม้จะไม่มีการรองรับ dynamic library ภายในแอป ก็ยังลดขนาดของทุกโปรแกรม Go ได้ และ language runtime ก็ไม่จำเป็นต้องหมกมุ่นกับการประหยัดพื้นที่มากนัก
เพราะเมื่อมันถูกโหลดไว้แล้ว และมีโปรแกรมใดโปรแกรมหนึ่งใช้ฟังก์ชันแม้เพียงตัวเดียว พื้นที่นั้นก็ไม่ถือว่าสูญเปล่า
สิ่งนี้จะเปลี่ยน cost model ของการปรับขนาดโปรแกรมสำหรับภาษานั้น
ฟังก์ชัน standard library ที่ถูกรวมไว้จะกลายเป็นของฟรีในทางปฏิบัติทันทีที่คุณเลือกใช้ภาษานั้น จึงใช้ได้เลย
อย่างไรก็ตาม ปัญหาเดิมก็จะย้อนกลับมาในกรณีของไลบรารีและ framework ที่ใช้กันทั่วไป
ถ้ารันบน Cloudflare ก็คงอยากให้ Cloudflare standard library สำหรับ Go ถูกแชร์ด้วย
ปัญหาคือภาษาไม่ได้วิวัฒนาการด้วยความเร็วเดียวกับ runtime
อาจต้องจำกัดการรองรับหลายเวอร์ชันของภาษา หรือไม่ก็ shared libraries จะค่อย ๆ สะสมตามเวลา จนผลของการแชร์ระหว่างแอปลดลง
JavaScript มีโมเดลเวอร์ชันแบบ “ไม่มีทางเลือก” พร้อม backward compatibility ที่แข็งแรง และบางครั้งต้องพึ่ง polyfill
สำหรับภาษาอื่น โมเดลนี้อาจเหมาะน้อยกว่า
ถ้า runtime ต้องการลดพื้นที่จริง ๆ ก็อาจทำได้ด้วยการจำกัดความหลากหลายของปลั๊กอิน
แม้จะมีเสียงบ่น แต่แนวทางแบบ “คุณต้องใช้ JavaScript” ก็ทำงานได้ค่อนข้างดีบนเบราว์เซอร์
ภาษาแบบ WebAssembly อาจไม่จำเป็นต้องหลากหลายมากเกินไป และความหลากหลายก็มีต้นทุนแบบสถานการณ์หอบาเบลตามมาด้วย
ถ้าทำ tree shaking จากข้อมูลการใช้งานจริง ก็น่าจะตัด dead code หรือโค้ดที่แทบไม่ได้ใช้ออกได้มาก โดยไม่ต้องไปสร้างอัลกอริทึม static analysis ที่ซับซ้อนมาก
โดยเฉพาะในบริบทอย่าง Hoot สิ่งอย่าง
appendChildคือฟังก์ชันภายนอกที่ถูกเรียกจากภายใน Scheme: https://spritely.institute/news/building-interactive-web-pages-with-guile-hoot.htmlในทางทฤษฎี เราสามารถใช้ส่วนใหญ่ของ JavaScript standard library ในสภาพแวดล้อม Wasm ใด ๆ ด้วยวิธีนี้ได้
Zig เหมาะกับงานลักษณะนี้อย่างสมบูรณ์แบบ
โดยส่วนตัวผมมองว่าถ้าขนาดไฟล์ Wasm ต่ำกว่า 100KB ก็ไม่ใช่ปัจจัยสำคัญ แต่ถ้าเกินระดับ MB เมื่อไรจึงค่อยสำคัญ
GC แบบ built-in สำคัญกับบางแอป แต่ไม่ใช่ทั้งหมด และสำหรับเว็บแอป ทางที่ดีที่สุดคือสร้างโดยไม่พึ่ง GC
ปัจจัยที่สำคัญที่สุดต่อความสำเร็จของแอปที่ใช้ Wasm ก็ยังคงเป็นข้อได้เปรียบด้านประสิทธิภาพ
กำลังรัน แอป Blazor บน Cloudflare Pages ซึ่งดาวน์โหลดได้เร็วและประสิทธิภาพก็ดี แต่เวลาโหลดแย่มาก
เลยคิดว่าแก้ด้วย .NET ไม่ได้ และดูเหมือนปัญหาหลักคือภาษาเชิงวัตถุถูกออกแบบมาให้ทุกอย่างผูกโยงกันไปหมด
อีกทั้งยังยากที่จะไปแข่งกับขนาดเงินทุนและการแข่งขันที่ทุ่มลงใน JavaScript และ JavaScript ก็มีการคัดลอก/วางที่แทบเหมือนเป็นฟีเจอร์ของภาษา จนเหมือนเป็นโกงเกม
อย่างที่สาม แม้แต่ใน Blazor ก็ยังต้องใช้ JavaScript และทักษะฝั่งนั้นอยู่ดี และผมคิดว่านี่แหละคือปัญหาหลัก
ปัญหาคือภาษายุคนั้นพึ่งพา reflection ในหลายกรณีการใช้งานมากเกินไป
ตอนนี้มีคนกำลังทำงานอย่างหนักเพื่อตัดกรณีการใช้งานเหล่านี้ออกจากส่วนใหญ่ของ .NET และทำเครื่องหมายว่าโค้ดส่วนไหนปลอดภัยต่อการลบโค้ดที่ไม่ได้ใช้งาน
ถ้ามีความเป็นไปได้ว่าจะมีการเรียกเมธอดผ่าน reflection ก็ยากที่จะรู้ว่าควรลบอะไรได้อย่างปลอดภัย
ต่อให้ดูเหมือนไม่มีที่ไหนเรียก
Foo.Bar()เลย แล้วจะทำอย่างไรถ้ามีใครทำReflection.getClass(someClass).runMethod(someVar)และตัวแปรนั้นถูกตั้งเป็น"Foo"กับ"Bar"ตัวอย่างเช่น Dart ไม่อนุญาต reflection ในแอปที่คอมไพล์ล่วงหน้า ซึ่งทำให้สามารถลบโค้ดที่ไม่ได้ใช้งานได้อย่างปลอดภัย: https://docs.flutter.dev/resources/faq#does-flutter-come-with-a-reflection-mirrors-system
Dart เป็นภาษาเชิงวัตถุ แต่หลีกเลี่ยงการสร้างโค้ดตอนรันไทม์และ reflection ตอนรันไทม์ แล้วเลือกใช้การสร้างโค้ดตอนคอมไพล์แทน
.NET ก็กำลังไปในทิศทางนี้ แต่ไม่ใช่สิ่งที่จะทำเสร็จได้ข้ามคืน
แต่อย่างที่คนอื่นพูดไว้ ภาษาอื่นที่ไม่ใช่ JavaScript ยังมีปัญหาที่ต้องแบกส่วนของฟังก์ชัน standard library ซึ่งปกติรวมอยู่ใน JavaScript runtime ของเบราว์เซอร์ไปด้วย
โมเดลการแพ็กเกจในปัจจุบันยังดึงศักยภาพของ .NET trimming ออกมาใช้ได้ไม่เต็มที่ เพราะติดข้อจำกัดจากการที่ Wasm ถูกแพ็กอยู่บน Mono
ถ้าอยากเห็นว่าลดขนาดได้ดีแค่ไหน จริง ๆ ควรลอง build แอปทั่วไปแบบ AOT แล้วจะได้ไบนารีขนาดเล็ก
การรองรับแบบทดลองของเป้าหมาย NativeAOT-LLVM Wasm ใน
dotnet/runtimelabให้ทั้งขนาด bundle ที่เล็กกว่ามากและประสิทธิภาพที่ดีกว่ามาก แต่ตอนนี้มันยังอยู่ใต้dotnet/runtimelabไม่ใช่dotnet/runtimeจึงไม่รู้ว่าจะพร้อมให้ใช้เมื่อไร