Toollance

Firefox chạy trong WebAssembly — ba bài học cho bản port của bạn

dev-toolswebassemblyperformance

Puter vừa công bố Firefox in WebAssembly: engine Gecko của Mozilla được biên dịch sang WASM, render giao diện Firefox thật ngay bên trong một tab browser khác. Simon Willison đã viết về nó, và nó lên trang nhất Hacker News với 259 điểm.

Xét như một sản phẩm thì nó không phải sản phẩm. Payload WASM khoảng 233MB và JIT thì không ổn định. Nhưng xét như một case study về việc port một ứng dụng native lớn lên browser, nó hữu ích hơn nhiều — vì ba vấn đề khó mà nó gặp cũng chính là ba vấn đề bạn sẽ gặp.

1. Khả năng chạy single-process quyết định codebase nào port được

Dự án chọn Gecko thay vì Chromium, và lý do được nêu là Gecko có hỗ trợ single-process mạnh.

Đó là ràng buộc mà đa số hay xem nhẹ. Ứng dụng desktop hiện đại được thiết kế đa tiến trình — một process chính, các process worker, renderer chạy trong sandbox, IPC giữa chúng. Browser không có thứ gì tương đương fork(). Bạn có một main thread, cộng thêm Web Worker với ranh giới truyền message và không chia sẻ không gian địa chỉ ngoài SharedArrayBuffer.

Nếu ứng dụng của bạn vốn đã chạy được tử tế trong một process, bản port sẽ khó. Nếu nó về bản chất đòi hỏi nhiều process phối hợp, bạn không đang port — bạn đang phải viết lại kiến trúc trước đã.

Phiên bản thực tế của câu hỏi này cho codebase của bạn: hôm nay nó có chạy được với --single-process hoặc tương đương không? Nếu câu trả lời là không, đó mới là hạng mục công việc đầu tiên, chứ không phải bản build WASM.

2. Bạn không mở được socket, nên bạn sẽ phải vận hành một proxy

Toàn bộ công việc của Gecko là mở kết nối mạng. Một tab browser thì không được phép làm thế. Nó chỉ có fetch() và WebSocket, cả hai đều bị CORS giới hạn.

Cách xử lý ở đây là tunnel mọi thứ qua WebSocket bằng giao thức Wisp tới một server mở kết nối thật. Từ góc nhìn của browser, HTTPS vẫn hoạt động end-to-end, và tunnel chỉ mang traffic đã mã hóa.

Đây là chi tiết âm thầm thay đổi bản chất của một bản port WASM. Một ứng dụng “chạy hoàn toàn trong browser” mà cần socket thô thì thực ra không hề serverless — nó đi kèm hạ tầng, chi phí băng thông, và một proxy nhìn thấy metadata kết nối của bạn. Mọi bản port của công cụ thiên về mạng đều thừa hưởng điều đó.

Các công cụ chỉ xử lý dữ liệu bạn đã có sẵn thì hoàn toàn không gặp vấn đề này, và đó là lý do tính toán thuần túy mới là chỗ WASM phía client thực sự tỏa sáng — cũng chính là lý do JSON formatterhash generator của chúng tôi chạy trọn vẹn trong tab của bạn.

3. JIT bên trong WASM là khả thi và hiện tại vẫn đau đầu

Dự án có kèm một JIT compiler từ JavaScript sang WebAssembly — sinh WASM lúc runtime rồi instantiate nó, vì WASM không có cách nào phát ra mã máy native.

README mô tả nó là thử nghiệm và không ổn định trên nhiều website, đồng thời cung cấp một lối thoát:

GECKO_NOWASMJIT=1

Cái flag đó là bản tóm tắt trung thực về hiện trạng công nghệ. Sinh code động bên trong WASM là làm được, chỉ là chưa đạt mức tin cậy để bạn yên tâm bật mặc định.

Cách build, nếu bạn muốn thử

Theo repository, các yêu cầu được ghi lại:

Emscripten vẫn là câu trả lời cho các codebase C++ lớn. Lựa chọn target của Rust đáng chú ý — wasm32-unknown-emscripten chứ không phải wasm32-unknown-unknown, vì phần Rust phải link với cùng runtime Emscripten như phần C++.

Rút ra được gì

Không ai nên ship Gecko-trong-WASM cho người dùng thật. Nhưng bản port này là một cận trên hữu ích: với Emscripten, một tunnel WebSocket và một bản build single-process, bạn có thể đưa cả một browser engine chạy trong tab browser.

Nếu ứng dụng của bạn nhỏ hơn Firefox, cần ít truy cập mạng hơn Firefox, và sinh ít code lúc runtime hơn Firefox — và gần như mọi ứng dụng đều thỏa cả ba — thì thứ cản giữa bạn và một bản port lên browser không phải là công cụ. Đó là kiến trúc.

Nguồn: Firefox in WebAssembly của Puter, HeyPuter/firefox-wasm trên GitHub, và bài viết của Simon Willison.