Tree shaking, một thuật toán sai lệch về mặt làm vườn (2023)
(wingolog.org)- WebAssembly đã đạt được kết quả trong việc đưa các chương trình C++ lớn như Photoshop lên web, nhưng trong các ứng dụng lấy DOM làm trung tâm, việc phổ biến bị hạn chế do mô hình lập trình khác với JavaScript
- Hỗ trợ Wasm GC và kiểu tham chiếu của trình duyệt mở ra cơ hội cho các ngôn ngữ quản lý bộ nhớ như Python, Scheme, nhưng trên web, kích thước truyền tải chính là rào cản áp dụng
- Một chương trình Wasm đơn giản bằng Go có thể là 2MB, khi thêm import có thể vượt 10MB, còn Pyodide REPL tải xuống khoảng 20MB, gây nặng nề cho ứng dụng web thông thường
- Trình biên dịch Hoot Scheme nhắm tới Wasm có hỗ trợ GC, đã giảm đơn vị biên dịch “main” tối thiểu xuống khoảng 70KB, và các đơn vị biên dịch phụ có thể dưới 1KB
- Tree shaking hiệu quả không chỉ là xóa các hàm không được tham chiếu, mà là vấn đề trình biên dịch nơi phân tích luồng và thiết kế thư viện chuẩn gắn chặt với nhau
Những lĩnh vực WebAssembly đã đạt kết quả trên web
- WebAssembly không lan rộng như kỳ vọng ban đầu trên web, nhưng đã đạt thành công có giới hạn trong một số lĩnh vực nhất định
- Ví dụ tiêu biểu là trường hợp đưa các chương trình C++ lớn như Photoshop lên web
- Figma cũng được nhắc đến như một ví dụ Wasm từ 5 năm trước, nhưng hiện nay không còn nhấn mạnh Wasm nhiều
- Nhiều thư viện NPM nhỏ được biên dịch từ C++ hoặc Rust dùng Wasm ở bên trong
- Blazor có thể được dùng trong một số ứng dụng doanh nghiệp nội bộ, nhưng cũng có khả năng đã bị phóng đại bởi marketing
- Demo 3D FPS của Unreal Engine là một thử nghiệm dựa trên bản phát hành lớn trước đó hơn 5 năm, còn Unreal 5 hiện không hỗ trợ mục tiêu WebAssembly
Vì sao Wasm bị chặn trong các ứng dụng lấy DOM làm trung tâm
- WebAssembly có thành quả trong môi trường ngoài web và có thể ngày càng quan trọng trên nền tảng web, nhưng trên web có thể xem là vừa đi qua “thung lũng vỡ mộng”
- Wasm có thế mạnh ở những việc JavaScript không làm tốt, hoặc các việc cần triển khai dùng chung giữa client và server
- Trong các ứng dụng lấy DOM làm trung tâm, Wasm đã không thành công
- Không có câu chuyện viết lại frontend của wordpress.com bằng Wasm
- Mô hình lập trình chính của web là JavaScript với kiểu động và bộ nhớ được quản lý
- WebAssembly 1.0 được thiết kế xoay quanh kiểu tĩnh và bộ nhớ tuyến tính
- Việc truy cập DOM từ Wasm rườm rà đến mức chỉ những người ủng hộ Wasm nhiệt thành mới chịu nổi
- Các ngôn ngữ như C# phải phân phối kèm garbage collector, và đây là trở ngại trong việc áp dụng Wasm của các ngôn ngữ không phải C/Rust
Vấn đề kích thước truyền tải vẫn còn sau Wasm GC
- Trình duyệt dự kiến sẽ cung cấp hỗ trợ kiểu tham chiếu và garbage collection trong vài tháng tới
- Chrome và Firefox đã cung cấp Wasm GC
- Safari cũng được xem là không còn xa nhờ công việc của Asumu Takikawa
- Wasm GC là thay đổi khiến nhiều ngôn ngữ hơn cập nhật toolchain để hỗ trợ WebAssembly
- Để Wasm trên web thành công, trình biên dịch phải tạo ra mã nhỏ
- Nếu toolchain của ngôn ngữ có thể tạo các tệp Wasm hữu dụng ở mức vài KB tính theo truyền tải thì sẽ có lợi
- Nếu không, chúng phải dựa vào kỳ vọng bị thổi phồng hoặc nhóm người dùng bị khóa chặt, và có thể ở trạng thái cân bằng bất ổn cho đến khi tìm ra giải pháp tiếp theo
- Trong hệ sinh thái JavaScript, đã có một ngành công cụ lớn để giảm kích thước phân phối và tình trạng phình to
- Các bundler như esbuild gộp nhiều module JS thành một tệp
- Cố gắng chỉ bao gồm các hàm và kiểu dữ liệu được sử dụng
- Cũng áp dụng các chiến lược giảm kích thước như minification, chẳng hạn rút ngắn tên
Cái bẫy trong tên gọi tree shaking
- Tree shaking có ẩn dụ trực quan là chỉ giữ lại phần mã cần thiết cho một trang nào đó và làm rơi phần còn lại
- Trong ẩn dụ này, module được tưởng tượng như cành, định nghĩa như lá, nhưng cây thật không cho ta biết cành nào cần và cành nào không cần chỉ bằng cách nắm thân cây mà lắc
- Bản thân tên gọi khiến người ta nghĩ theo hướng loại bỏ mã không cần thiết, nhưng từ góc nhìn thuật toán, phù hợp hơn là tìm điểm cố định chỉ giữ lại mã cần thiết
- Dù vậy, tree shaking là một cái tên ấn tượng nên vẫn tiếp tục được dùng bất chấp sự thiếu chính xác về mặt làm vườn và thuật toán
Rào cản kích thước do runtime dày tạo ra
- Với các ngôn ngữ có runtime dày, tree shaking tối đa chưa từng là ưu tiên lớn
- Trong hỗ trợ WebAssembly của Go, ngay cả chương trình đơn giản nhất cũng là 2MB theo golang wiki
- Nếu thêm import, có thể vượt 10MB
- Ví dụ REPL của Pyodide, bản port Python sang WebAssembly, tải xuống khoảng 20MB dữ liệu
- Kích thước như vậy ổn với demo công nghệ hoặc ứng dụng rất phong phú, nhưng khó trở thành lựa chọn phát triển web thông thường
Toolchain thay thế và triển khai tùy chỉnh theo nền tảng
- Hỗ trợ Wasm tích hợp của Go và Pyodide bắt nguồn từ toolchain upstream, và trên server kích thước binary có thể không quá quan trọng
- Khi nhắm tới thiết bị nhỏ, các triển khai riêng xuất hiện
- Backend Wasm của TinyGo có vẻ có thể giảm xuống dưới 1KB
- Những toolchain thay thế như vậy thường đi kèm hạn chế hoặc điểm đặc thù
- Các chương trình Python nhắm mục tiêu Wasm chạy trong môi trường DOM chắc chắn sẽ khác với chương trình Python “native”
- Tác giả toolchain cố gắng cung cấp cùng một ngôn ngữ, nhưng cách triển khai thư viện chuẩn có thể khác
- Các nhà phát triển ClojureScript có lẽ cũng muốn xóa tài liệu khác biệt với Clojure nếu có thể, và nếu Wasm trở thành mục tiêu thực dụng của ClojureScript thì khả năng đó sẽ xuất hiện
Cách Hoot Scheme thực hiện tree shaking
- Sau khi có hỗ trợ GC, Wasm cho phép nghĩ tới lập trình DOM bằng các ngôn ngữ như Python, nhưng để được dùng phổ biến thì cần các module nhỏ
- Hoot Scheme compiler nhắm tới Wasm có GC
- Đơn vị biên dịch “main” tối thiểu hiện khoảng 70KB
- Mục tiêu là kích thước thấp hơn nữa
- Các đơn vị biên dịch phụ import tính năng runtime như trình xử lý ngoại lệ từ module main có thể dưới 1KB
- Trình biên dịch Hoot gắn prelude trước mã người dùng
- Tree shaking diễn ra ở nhiều giai đoạn
- Đánh giá từng phần có thể đánh giá hiệu ứng của các binding không được dùng rồi loại bỏ chúng
- fixing letrec cũng thực hiện công việc tương tự
- CPS thường xuyên duyệt chương trình và chỉ đi theo các hàm, giá trị và cạnh luồng điều khiển đã được tham chiếu
- Pass dead-code elimination tường minh loại bỏ các phép gán không hiệu ứng và không được dùng có thể xuất hiện sau các tối ưu hóa khác
- Các định nghĩa trong thư viện chuẩn, được viết gần với WebAssembly thô, chỉ được đưa vào binary kết quả khi cần
Loại bỏ dễ và loại bỏ khó
- Định nghĩa thủ tục như hàm hoặc closure tương đối dễ xử lý
- Chỉ cần bao gồm các hàm mà mã tham chiếu tới
- Với các ngôn ngữ như Scheme, chỉ riêng việc này đã có hiệu quả đáng kể
- Các khó khăn thấy ngay gồm ba điểm
-
Mô hình đánh giá letrec*
- Scope của các định nghĩa prelude là đệ quy nhưng có thứ tự
- Giá trị binding có thể gọi hoặc tham chiếu giá trị được định nghĩa trước đó, và cũng có thể capture giá trị sẽ được định nghĩa sau
- Sẽ là lỗi nếu việc đánh giá một giá trị binding cần tham chiếu tới giá trị chỉ được định nghĩa sau đó
- Với thủ tục thì nhìn chung không có vấn đề, nhưng với định nghĩa không phải thủ tục, trình biên dịch có thể không chứng minh được tính chất “chỉ tham chiếu các binding phía trước”
- Trong trường hợp này, thuật toán fixing letrec reloaded có thể giữ lại các binding bị set!, và để loại bỏ chúng cần một pass DCE tinh tế
-
vtable của kiểu record
- Một số định nghĩa không phải thủ tục là kiểu record
- Kiểu record có vtable chứa cách xuất record hoặc cách kiểm tra instance, v.v.
- Callback trong vtable có thể giữ cho nhiều mã còn sống ngay cả khi thực tế không được dùng
-
Hàm xuất đa hình
- Các hàm đa hình như
displaymở rộng đáng kể phạm vi mã cần thiết - Nếu gọi
displayđể xuất chuỗi, toàn bộ cơ sở I/O có bộ đệm sẽ bị kéo vào - Vì
displaycó thể xuất bất cứ thứ gì, mã cho nhiều trường hợp như bitvector, pair, v.v. cũng có thể bị kéo theo - Nếu gọi
write-string, vốn chỉ dùng cho chuỗi, có thể tránh mã xuất dữ liệu tổng quát, nhưng các cơ sở I/O có bộ đệm tổng quát như port vẫn được bao gồm
- Các hàm đa hình như
Tree shaking tối ưu là vấn đề phân tích luồng
- Tree shaking tối ưu rốt cuộc là vấn đề phân tích luồng
- Nếu chương trình tuyệt đối không có bitvector, mã xử lý bitvector bên trong
displaycó thể trở thành mã chết - Để biết điều đó, cần biết
displayđược gọi với loại đối số nào, và để làm vậy cần phân tích luồng cấp cao - Với Python, vấn đề trở nên khó hơn
- Dispatch hướng đối tượng là lập trình bậc cao, nên
foo.barnghĩa là gì phụ thuộc vàofoolà gì - Cơ chế tra cứu của Python động hơn Scheme, và các phương thức như
__getattr__có thể được dùng ở khắp nơi - Trên thực tế, phân tích luồng có thể loại trừ được những tra cứu động như vậy
- Đối tượng tree shaking của Python không phải là một hạng lớn có binding từ vựng, mà là một tập hợp module phức tạp
- Điều này tương tự JavaScript, nhưng Python không có hệ sinh thái bundler tree shaking đã được thiết lập
- Dispatch hướng đối tượng là lập trình bậc cao, nên
Điều kiện cho toolchain ngôn ngữ Wasm trên web
- Wasm GC có thể khiến việc lập trình DOM bằng các ngôn ngữ không phải JavaScript trở nên khả thi
- Để dẫn tới sử dụng đại chúng, các module Wasm kết quả phải nhỏ
- Mỗi toolchain ngôn ngữ cần được đầu tư đáng kể
- Những khoản đầu tư như vậy thường xuất hiện dưới dạng toolchain thay thế, bao gồm các thuật toán tree shaking thử nghiệm
- Thư viện chuẩn thay thế cần được thiết kế để tree shaker hoạt động tốt hơn
1 bình luận
Ý kiến trên Hacker News
openEtG đã giữ blob Wasm (engine game bài) ở mức dưới 400KB trong khi chuyển nhiều logic như tạo văn bản thẻ bài sang Wasm, và được viết bằng Rust
Để giảm kích thước, cần quản lý bằng các cách như dùng số học fixed-point thay cho floating-point, chuyển từ hashmap sang vector, tránh chuỗi, dùng allocator nhỏ như
talc, và giảm phụ thuộcHiện chỉ dùng
randvàfxhash, nhưng có vẻ cũng có thể loại bỏrand;fxhashchỉ được dùng để hash trạng thái game nhằm kiểm tra có bị mất đồng bộ hay khôngCũng đã giảm số loại instance generic để vì đã có
Vecrồi thì không kéo thêm các kiểu nhưBox<[i16]>, và việc loại bỏ floating-point cùng hashmap cũng giúp giảm độ đa dạng kiểuThuật toán cũng được thiết kế có tính đến kích thước; chẳng hạn, cơ chế adrenaline, trong đó sinh vật có sức tấn công thấp sẽ tấn công nhiều lần hơn, được mã hóa bằng bảng tra cứu đóng gói bit
Đã so sánh chi phí lưu các giá trị chưa nén với chi phí logic giải mã, và phần đánh giá AI dùng độ chính xác cố định 6 bit vì trong WebAssembly 64 được mã hóa hiệu quả hơn 128
Cơ chế nhắm mục tiêu trước đây có dạng AST, trong đó mỗi predicate là một enum và AND/OR là các lát cắt biểu thức, nhưng hiện nay biểu thức được mã hóa vào số nguyên 32 bit theo ký pháp Ba Lan, với AND/OR chiếm 2 bit và predicate chiếm 6 bit
Ở đây ký pháp Ba Lan tốt hơn ký pháp Ba Lan ngược vì có thể đánh giá đoản mạch AND/OR
Ở nơi làm việc, tôi đang cân nhắc liệu fixed-point có hữu ích cho các bài toán đã biết yêu cầu độ phân giải tối đa, chẳng hạn khi không cần độ chính xác vị trí dưới milimét, nên muốn nghe thêm về các vấn đề liên quan
wasm-optcủa binaryenCó vẻ nó giảm kích thước Wasm ổn định khoảng 20~30%: https://github.com/WebAssembly/binaryen
Khi phục vụ bundle Wasm cho trình duyệt, cũng nên dùng nén Brotli và cấu hình web server sử dụng tệp nén Brotli
Với nginx, chỉ cần đổi một dòng; Brotli giúp giảm kích thước bundle Wasm khoảng 3 lần và tốt hơn gzip rất nhiều
Kích thước đó bao gồm toàn bộ forward/backward pass của deep learning, thuật toán reinforcement learning và mô phỏng động lực học
Hiện giờ vẫn chưa đến mức nặng nề, nhưng tôi tò mò có thể làm nhỏ hơn đến đâu nên muốn thử giảm kích thước trong thời gian tới
Tôi muốn hiểu vì sao nói rằng tránh floating-point và dùng số học fixed-point lại có thể tiết kiệm dung lượng
Đặc biệt, cách dùng
VecnhưBoxnhư ở mục 6 có vẻ không dẫn đến mức tiết kiệm lớnCái tên tree shaking có vẻ là một cách gọi khá sai
Trình biên dịch Virgil gọi nó là “phân tích khả năng truy cập” và tích hợp vào mô hình biên dịch
Trình biên dịch phân tích cú pháp chương trình và mã thư viện, kiểm tra kiểu và chạy mã khởi tạo, nhưng sau đó chỉ phân tích những mã có thể truy cập được bằng cách duyệt từ điểm vào main và đưa chúng vào binary cuối cùng
Nó cũng tạo tốt các chương trình chỉ có một hàm
mainduy nhất mà không cần hệ thống runtime; hệ thống runtime chỉ cần cho stack trace và garbage collection nên có thể bỏ qua nếu muốnTrường hợp đầu tiên tìm thấy là công cụ Treeshaker trong Lucid Common Lisp 4.1 năm 1992, một implementation Common Lisp thương mại cho UNIX
Lucid CL có khái niệm image, tức bản dump bộ nhớ đã lưu của heap Lisp đang chạy, và ứng dụng được cấu thành từ image và runtime
Image thường chứa gần như toàn bộ mã và dữ liệu trong bộ nhớ, nên người ta muốn tạo image nhỏ hơn để phân phối; Treeshaker loại bỏ mã và dữ liệu được đánh giá là “không dùng” trước khi lưu image
Cách làm là cắt tỉa các liên kết trong đồ thị dữ liệu và mã Lisp có thể truy cập, rồi để GC hoặc mã đặc biệt thu gom rác nhằm giảm bộ nhớ, sau đó dump thành image nhỏ hơn
Vì vậy Treeshaker không phải là công cụ biên dịch, mà là công cụ loại bỏ mã và dữ liệu không dùng khỏi heap Lisp
Image Lisp mặc định bao gồm cả trình biên dịch, trình thông dịch và implementation REPL, nên nếu ngắt chương trình đang chạy để vào REPL thì vẫn có thể dùng mọi mã trong heap được khôi phục từ image
Do đó việc loại bỏ cả trình biên dịch hoặc REPL là có ý nghĩa
Phân tích khả năng truy cập thường được dùng để xác định mã nào có thể loại bỏ, nhưng bản thân phân tích không loại bỏ mã; việc loại bỏ là bước tiếp theo
“Tree shaking” thường hàm ý loại bỏ ở cấp hàm, trong khi loại bỏ mã chết có thể thực hiện ở mức tinh vi hơn nhiều, như loại bỏ nhánh của biểu thức điều kiện, và có thể dựa trên nhiều dạng phân tích tĩnh khác nhau
Lần đầu nhìn thấy, tôi hiểu ngay ý nghĩa mà không cần tìm hiểu thêm
Đó là việc rung cây để làm rơi những thứ bám lỏng lẻo, và ở đây ý nghĩa rất rõ là các package không dùng sẽ bị “rung rơi” khỏi cây
Nếu tưởng tượng sơ đồ mã nguồn như một vật thể vật lý, khi rung nó thì những thứ không thể truy cập từ gốc sẽ rơi ra
Nó không khác nhiều so với phân tích khả năng truy cập, chỉ là một cách diễn đạt gợi suy luận không gian hơn mà thôi
Một số mã không được nối với thân cây là điểm vào
Tree shaking loại bỏ các phần không được kết nối, tức những chiếc lá lỏng lẻo và cành chết
Nếu toolchain biên dịch của một ngôn ngữ có thể tạo ra Wasm hữu dụng chỉ vài KB tính theo dung lượng truyền qua mạng, những khả năng mới sẽ mở ra
Binary cực nhỏ sẽ mở ra các use case mới cho Wasm, và WasmGC chắc chắn có ích
Java và Kotlin ngày nay cũng có thể làm khá tốt ở mức khoảng 2–3KB: https://developer.chrome.com/blog/wasmgc, https://twitter.com/bashorov/status/1661377260274720770
Tuy nhiên cần chú ý vì tùy API sử dụng mà mã lớn có thể bị kéo theo
Dù vậy, nhờ WasmGC, các ngôn ngữ này không cần đóng gói vài KB mã quản lý bộ nhớ vào bundle, nên về kích thước mã thì đã tốt hơn C++ và Rust khá nhiều
Khi xử lý đối tượng JavaScript thì có thể hữu ích, và cũng có thể dùng như một allocator bộ nhớ thay thế kém hiệu quả hơn
Dù vậy, allocator Rust mặc định trên Wasm có lẽ đủ ổn trong phần lớn trường hợp
Nếu bắt đầu tối ưu kích thước, dùng
wasm-optvà nén bằng Brotli, có thể nhét một lượng mã khổng lồ vào dưới 100KB tính theo dung lượng tải xuốngSo sánh trực tiếp chi phí 100KB Wasm với 100KB JavaScript đóng gói là sai lầm, vì JavaScript mất thời gian parse và khởi tạo chậm hơn vài lần
Thời gian tải xuống là chi phí thật, nhưng về thời gian hiển thị màn hình đầu tiên thì 100KB Wasm tốt hơn 100KB JavaScript nhiều
Dù vậy càng nhỏ càng tốt, và thật đáng kỳ vọng khi Java, Kotlin, C#, Python, Go, v.v. trở thành các ngôn ngữ thực dụng cho ứng dụng web
Cũng tò mò kích thước ứng dụng thực tế sẽ ra sao
Khác biệt lớn nhất có lẽ sẽ đến từ thiết kế framework; so sánh virtual DOM chắc chắn luôn phức tạp và chậm hơn các thư viện component phản ứng như Svelte, SolidJS hay Leptos của Rust
Khi WasmGC được hỗ trợ ở mọi nơi, có lẽ lựa chọn web framework sẽ ảnh hưởng đến hiệu năng lớn hơn nhiều so với ngôn ngữ
Tôi nghi ngờ liệu câu “Wasm khiến việc lập trình DOM bằng các ngôn ngữ không phải JavaScript trở nên có thể nghĩ tới” có thực sự đúng không
Theo tôi biết, để thao tác DOM trong các ngôn ngữ như Rust thì rốt cuộc vẫn cần binding để tuần tự hóa các lời gọi sẽ được thực thi ở phía JavaScript
Với Wasm ở dạng hiện tại, tôi có cảm giác nó vẫn bị ràng buộc với JavaScript
Về lý thuyết, giờ đây có thể lấy các hàm DOM từ runtime rồi gọi chúng bằng tham chiếu đối tượng DOM, qua mặt JavaScript và gọi trực tiếp runtime
Tôi không chắc trong thực tế có làm được không, nhưng GC ít nhất cung cấp cơ chế tiền đề để đi tới điểm đó
Về cơ bản nó giống SolidJS
#[component] fn App() -> impl IntoView { let (count, set_count) = create_signal(0); view! { "Click me: "{move || count()} } }So với JavaScript thì Wasm có overhead về kích thước, nhưng không nghiêm trọng
Sau khi dùng
wasm-optvà nén Brotli, bundle Wasm của ứng dụng bộ đếm này là 37KB, nằm trong khoảng tương tự React nhưng sau khi chạy thì nhanh hơn nhiềuTôi chưa thử thao tác DOM trực tiếp, nhưng có vẻ tốt cho các component thông thường
Ví dụ, một DOM được triển khai bằng Rust có thể được dùng từ module Wasm viết bằng Rust mà không cần thực thi JavaScript
Tuy vậy, có những phần của DOM API được đặc tả theo ngữ nghĩa JavaScript nên khá khó xử lý, vì vậy có vẻ họ đang làm trước những thứ ít di sản JavaScript hơn như request HTTP, socket TCP, truy cập filesystem
Thuật ngữ “loại bỏ mã chết” đã có từ lâu, nên tôi tò mò vì sao cách nói tree shaking lại xuất hiện
“Tree shaking” chỉ việc phân tích toàn chương trình để vứt bỏ toàn bộ module và hàm không được gọi
Về khái niệm thì giống nhau, nhưng người viết compiler thường phải triển khai riêng, nên có hai tên gọi là hữu ích
Tree shaking dễ liên tưởng, dễ nói và dễ tiếp cận hơn “loại bỏ mã chết”, nên có vẻ đã trở thành thuật ngữ phổ biến hơn
Tôi thử tìm xem thuật ngữ nào có trước, thì lần dùng sớm nhất của “dead-code elimination” mà tôi tìm được là trong một bài báo năm 1973: https://research-repository.st-andrews.ac.uk/bitstream/handle/10023/22636/NicholasAlexandrakisMScThesis1973_original_C.pdf?sequence=1
Trên Google Scholar, tôi không tìm thấy cách dùng “tree shaking” hay “tree shaker” trong lĩnh vực máy tính; phần lớn là nội dung liên quan đến cây cối như cây có múi
Thảo luận có vẻ sớm nhất là một bài viết trên comp.lang.lisp: https://groups.google.com/forum/#!topic/comp.lang.lisp/pspFr1XByZk
Tôi nghĩ ẩn dụ tree shaking có thể đến từ một số cách thu hoạch cây ăn quả
Rung cây thì quả chín sẽ rụng xuống
Tuy nhiên, trong thu hoạch trái cây thì thứ rụng xuống là thứ ta muốn, còn khi lưu ảnh đã tuần tự hóa thì thứ rụng xuống lại bị bỏ đi, nên đây không phải là một ẩn dụ quá hay
Trái cây mỏng manh hơn, nếu rơi quá xa thì dễ bị dập nên thường được hái bằng tay
Quá trình rung khá thô bạo, nhưng không gây hại cho cây
Triết lý của tôi là tối ưu thật kỹ kích thước và hiệu năng trong JavaScript thông qua việc đơn giản hóa thuật toán và thiết kế, để dành dung lượng bundle và CPU cho những đoạn mã cần tính toán kiểu brute-force
Trong dự án ClubCompy, tôi đang dùng Wasm để triển khai hệ thống tệp FAT trên local storage, và hóa ra chi phí tính toán rất lớn
Cuối năm nay, khi đưa lại tính năng phát hiện va chạm sprite chính xác đến từng pixel, tôi cũng định dùng Wasm
Bản triển khai đầu tiên là JavaScript thuần, và khi 256 sprite trên màn hình va chạm với nhau, framerate tụt xuống dưới 1fps
Tôi nghĩ có thể xử lý gần như miễn phí trên worker thread để không ảnh hưởng đến hiệu năng
Phần lớn game 2D dùng hộp va chạm hình chữ nhật là có lý do: người chơi dễ dự đoán liệu có va chạm hay không
Với va chạm theo từng pixel, cùng một chuyển động có thể va chạm hoặc không tùy theo pha của chu kỳ animation
Sẽ rất bực nếu một động tác trước đây luôn thực hiện được lại thất bại chỉ vì timing animation khớp không may
Hơn nữa, theo từng pixel cũng không phải lúc nào cũng thực tế; các chi tiết nhỏ của sprite có thể là những phần như vải hoặc tóc, thực tế không gây va chạm cứng
Sprite cũng thường di chuyển nhiều pixel mỗi frame, nên va chạm ở từng pixel riêng lẻ làm tăng khả năng chúng xuyên qua nhau
Phát hiện va chạm đơn giản và dễ dự đoán nhìn chung là tốt nhất
1fps là framerate lẽ ra chỉ xuất hiện ở khoảng 20.000 va chạm
Nếu là theo từng pixel, khi còn dư bộ nhớ, có một cách đơn giản là render toàn bộ sân chơi lên một canvas offscreen, vẽ mỗi sprite bằng stencil có màu khác nhau rồi kiểm tra nó
Thời gian tuyến tính và cũng không cần phân vùng riêng
Tuy nhiên, các biện pháp chống fingerprinting có thể biến các bit thấp của dữ liệu canvas thành nhiễu, nên có thể phải dùng các bit cao
Bài viết này nói đúng. Wasm có vấn đề về kích thước mã
Trên trình duyệt, đây là vấn đề vì phải tải xuống toàn bộ mã trước khi trang web khởi động; trong kiến trúc serverless cũng vậy, vì mã được tải theo nhu cầu từ cold storage lên một máy chủ cụ thể trong lúc client chờ đợi
Tree shaking có thể giúp, nhưng có lẽ chỉ dừng ở tối ưu hóa từng bước
Về cơ bản, lý do chương trình Wasm phình to là vì phải mang theo toàn bộ runtime ngôn ngữ và thư viện chuẩn của từng ngôn ngữ
Ngược lại, JavaScript được trình duyệt cung cấp phần triển khai và các thư viện cơ bản
Dễ nghĩ rằng trình duyệt không thể nạp sẵn mọi runtime ngôn ngữ, nhưng cũng nên cân nhắc một cách tiếp cận khác: thư viện dùng chung và liên kết động
WebAssembly hỗ trợ liên kết động, và có thể tải đồng thời nhiều mô-đun Wasm để chúng gọi lẫn nhau
Nhưng nhiều toolchain Wasm không muốn hỗ trợ điều này, và được thiết kế để liên kết tĩnh toàn bộ chương trình cùng runtime ngôn ngữ vào một mô-đun khổng lồ
Pyodide (CPython trên Wasm) là một phản ví dụ, hiện được thiết kế với liên kết động trong đầu
Chính nhờ vậy mà Cloudflare Workers gần đây có thể bổ sung Python như một tính năng hạng nhất: https://blog.cloudflare.com/python-workers
Từ góc nhìn của technical lead cho toàn bộ nền tảng Workers, mọi Worker chạy trên cùng một máy đều chia sẻ một runtime Pyodide đã biên dịch, nên không cần tải riêng cho từng Worker
Nếu liên kết động được hỗ trợ rộng rãi hơn, có thể hình dung một cấu trúc trong đó trình duyệt nạp sẵn các runtime ngôn ngữ phổ biến, thậm chí cả các thư viện phổ biến, và mọi trang web cần runtime đó cùng chia sẻ một bản sao mã chỉ đọc
Các runtime này vẫn chạy trong sandbox, nên trình duyệt không cần tin cậy chúng, chỉ cần cung cấp chúng
Nhờ vậy có thể tạo ra các trình duyệt “tích hợp sẵn” hỗ trợ những ngôn ngữ ngoài JavaScript mà người bảo trì trình duyệt không cần xác minh hay lo nghĩ hoàn toàn về phần triển khai ngôn ngữ
Tôi không rành lắm, nhưng hiểu rằng cách đó đã không hoạt động tốt
Có quá nhiều phiên bản thư viện nên từng phiên bản riêng lẻ thực ra không được dùng rộng rãi, và về sau do lo ngại quyền riêng tư, trình duyệt đã đi theo hướng tách cache theo từng site hoặc origin
Có thể ý tưởng này không nhắm tới caching, nhưng đây là một vấn đề khó, gần với vấn đề xã hội hơn là vấn đề kỹ thuật
Nếu đưa thêm nhiều runtime ngôn ngữ vào hỗ trợ mặc định của trình duyệt, rào cản gia nhập cho trình duyệt mới sẽ cao hơn, và cũng không thể hỗ trợ mọi thư viện và runtime mà mọi người muốn
Nếu để mỗi bên tự mang theo rồi kỳ vọng vào cache, câu hỏi còn lại là làm sao tránh các vấn đề từng gặp với caching thư viện JavaScript
Ví dụ, có thể đưa runtime và những phần cốt lõi của thư viện chuẩn mà nhiều chương trình dùng vào một thư viện dùng chung của Go
Ngay cả khi không hỗ trợ thư viện động bên trong ứng dụng, vẫn có thể giảm kích thước của mọi chương trình Go, và runtime ngôn ngữ cũng không cần tối ưu dung lượng quá mạnh
Vì nó đã được tải sẵn, và nếu có chương trình nào dùng dù chỉ một hàm thì đó không còn là phần dung lượng bị lãng phí
Khi đó mô hình chi phí cho việc tối ưu kích thước của chương trình bằng ngôn ngữ đó sẽ thay đổi
Các hàm thư viện chuẩn đã được bao gồm về cơ bản trở thành miễn phí ngay khi dùng ngôn ngữ đó, nên cứ dùng chúng là được
Tuy nhiên, vấn đề sẽ lặp lại với các thư viện và framework thường dùng
Khi chạy trên Cloudflare, hẳn cũng sẽ muốn chia sẻ thư viện chuẩn Cloudflare cho Go
Vấn đề là ngôn ngữ không tiến hóa cùng tốc độ với runtime
Việc hỗ trợ nhiều phiên bản ngôn ngữ có thể bị hạn chế, hoặc thư viện dùng chung có thể tích tụ theo thời gian khiến hiệu quả chia sẻ giữa các ứng dụng giảm đi
JavaScript có mô hình phiên bản kiểu “không có lựa chọn”, đòi hỏi tương thích ngược mạnh và đôi khi cần polyfill
Điều đó có thể kém phù hợp hơn với các ngôn ngữ khác
Nếu runtime thật sự muốn giảm dung lượng, chỉ cần hạn chế sự đa dạng của plugin
Dù có nhiều phàn nàn, cách “phải dùng JavaScript” đã hoạt động khá tốt trên trình duyệt
Các ngôn ngữ dựa trên WebAssembly có thể không cần quá đa dạng, và sự đa dạng kiểu tháp Babel luôn đi kèm chi phí
Nếu tree shaking dựa trên thông tin vận hành thực tế, có lẽ có thể cắt bỏ nhiều mã chết hoặc gần như chết mà không cần triển khai các thuật toán phân tích tĩnh tinh vi
Đặc biệt trong bối cảnh như Hoot, những thứ như
appendChildlà các hàm bên ngoài được gọi từ trong Scheme: https://spritely.institute/news/building-interactive-web-pages-with-guile-hoot.htmlVề lý thuyết, trong bất kỳ môi trường Wasm nào cũng có thể dùng nhiều phần của thư viện chuẩn JavaScript theo cách này
Zig hoàn hảo cho mục đích này
Cá nhân tôi cho rằng nếu kích thước tệp Wasm dưới 100KB thì không phải yếu tố quan trọng, còn khi vượt quá MB thì mới quan trọng
GC tích hợp quan trọng với một số ứng dụng nhưng không phải tất cả, và ứng dụng web tốt nhất là nên được tạo ra không cần GC
Yếu tố quan trọng nhất để ứng dụng dùng Wasm thành công vẫn là lợi thế về hiệu năng
Tôi đang chạy ứng dụng Blazor trên Cloudflare Pages; tốc độ tải xuống nhanh và hiệu năng cũng tốt, nhưng thời gian load thì kinh khủng
Tôi nghĩ với .NET thì không thể giải quyết được, và vấn đề cốt lõi có vẻ là các ngôn ngữ hướng đối tượng về mặt thiết kế khiến mọi thứ bị đan xen với nhau
Ngoài ra, khó mà cạnh tranh với quy mô tiền đổ vào JavaScript; JavaScript còn có copy/paste như thể là một tính năng ngôn ngữ, giống như cheat code vậy
Thứ ba, ngay cả với Blazor vẫn cần JavaScript và năng lực ở phía đó, và tôi xem đây là vấn đề chính
Vấn đề là các ngôn ngữ thời đó phụ thuộc vào reflection trong nhiều trường hợp sử dụng
Mọi người đang nỗ lực loại bỏ các trường hợp sử dụng như vậy trong phần lớn .NET và đánh dấu là an toàn để loại bỏ mã không dùng đến
Nếu có khả năng một phương thức được gọi bằng reflection, sẽ rất khó biết có thể loại bỏ an toàn những gì
Dù có vẻ không có nơi nào gọi
Foo.Bar(), nhưng nếu ai đó gọiReflection.getClass(someClass).runMethod(someVar)và các biến đó được đặt thành"Foo"và"Bar"thì saoVí dụ Dart không cho phép reflection trong các ứng dụng biên dịch trước, nhờ đó có thể loại bỏ mã không dùng đến một cách an toàn: https://docs.flutter.dev/resources/faq#does-flutter-come-with-a-reflection-mirrors-system
Dart là ngôn ngữ hướng đối tượng, nhưng tránh sinh mã lúc runtime và reflection lúc runtime, thay vào đó chọn sinh mã ở thời điểm biên dịch
.NET cũng đang đi theo hướng này, nhưng không phải chuyện có thể xong trong một đêm
Tuy nhiên, như những người khác đã nói, các ngôn ngữ không phải JavaScript còn gặp vấn đề là phải tự gửi kèm những phần chức năng thư viện chuẩn mà chúng dùng, vốn đã có sẵn trong runtime JavaScript của trình duyệt
Mô hình đóng gói hiện tại không tận dụng tốt được khả năng trimming của .NET do giới hạn của việc Wasm được đóng gói trên Mono
Để xem thực tế có thể rút gọn tốt đến đâu, tốt hơn là thử build một ứng dụng thông thường bằng AOT; khi đó sẽ có binary nhỏ
Hỗ trợ thử nghiệm cho target Wasm NativeAOT-LLVM trong
dotnet/runtimelabcho kích thước bundle nhỏ hơn nhiều và hiệu năng tốt hơn nhiều, nhưng hiện vẫn nằm trongdotnet/runtimelabchứ chưa phảidotnet/runtime, nên không biết khi nào mới dùng được