Người ta gọi WebAssembly là kẻ thay đổi cuộc chơi vì nó giúp thực thi code trên trình duyệt nhanh hơn. Một số chức năng đó đã sẵn sàng ngay hôm nay , và một số sẽ sẵn sàng trong tương lai gần.

Một trong các tính năng đó là biên dịch theo stream, trình duyệt sẽ biên dịch code ngay khi nó vẫn đang được download. Cho đến hiện tại nó vẫn là tính năng có thể thực hiện trong tương lai. Nhưng với FireFox 58, nó đã chính thức cập bến trình duyệt.

Ngoài ra Firefox 58 còn bao gồm trình dịnh 2 lớp. Trình dịch cơ sở biên dịch code nhanh hơn từ 10 – 15 lần so với trình dịch tối ưu.

Kết hợp lại, hai thay đổi này giúp WebAssembly có thể dịch với tốc độ còn nhanh hơn thời gian nó được gửi qua mạng

Trên desktop, WebAssembly có thể biên dịch ở tốc độ 30-60 MB/s. Nhanh hơn tốc độ của hầu hết mạng internet

Nếu bạn dùng Firefox, bạn có thể test với thiết bị của mình. Với một mẫu điện thoại trung bình, tốc độ biên dịch cũng ở mức 8MB/s nhanh hơn hầu hết tốc độ của tất cả các mạng di động.

Điều đó có nghĩa code WebAssembly  có thể thực thi ngay khi nó được download xong.

Vậy điều này quan trọng?

Những người phát triển web thường khá khó chịu nếu trang web chứa quá nhiều Javascript. Vì download quá nhiều Javascript khiến trang web load chậm hơn.

Điều này một phần lớn là do thời gian parse và dịch code. Trong blog Steve Souders đã chỉ ra, bottleneck cũ nằm ở tốc độ mạng, nhưng bottleneck mới hiện nay nằm ở hiệu năng của CPU và cụ thể là của main thread.

Old bottleneck, the network, on the left. New bottleneck, work on the CPU such as compiling, on the right

Vì thế cần phải giảm tải nhiều nhất có thể cho main thread. Bên cạnh đó cũng cần phải bắt đầu càng sớm càng tốt để sử dụng được tối đa thời gian của CPU. Tốt hơn nữa là tối ưu thuật toán để giảm tổng thời gian sử dụng CPU.

Với Javascript bạn có thể thực hiện một số cách. Bạn có thể parse file bằng tiến trình background, ngay khi chúng được stream đến. Nhưng cho cùng thì bạn vẫn phải parsing nó, công việc này tốn rất nhiều thời gian, và bạn phải chờ cho đến khi chúng được parse xong trước khi có thể biên dịch. Và với quá trình dịch, bạn lại quay lại main thread. Điều này là do JS thường được biên dịch khá tiết kiệm khi thực thi , trình dịch phải tiến hành chạy trang web thì mới xác định được hàm Js nào thực sự cần phải dịch.

Timeline showing packets coming in on the main thread, then parsing happening simultaneously on another thread. Once parse is done, execution begins on main thread, interrupted occassionally by compiling

Với WebAssembly, có ít việc phải làm hơn. Giải mã WebAssembly đơn giản và nhanh hơn rất nhiều so với Javascript. Và quá trình này có thể chia nhỏ trên nhiều threads.

Nghĩa là nhiều tiến trình có thể đồng thời tiến hành dịch cơ sở, khi đó quá trình dịch sẽ nhanh hơn. Ngay khi dịch xong, trình dịch cơ sở có thể thực thi code trên main thread. Vì tốc độ dịch cơ bản nhanh, và nó không cần quan sát quá trình chạy thực tế nên nó không cần quay lại main thread như với JS.

Timeline showing packets coming in on the main thread, and decoding and baseline compiling happening across multiple threads simultaneously, resulting in execution starting faster and without compiling breaks.

Khi phiên bản của trình dịch cơ sở chạy trên main thread, những threads khác tiến hành dịch phiên bản tối ưu hơn. Khi phiên bản tối ưu đã sẵn sàng nó được thay thế lại vào thread chính, khi đó code sẽ thực thi nhanh hơn nữa.

Thay đổi này khiến công sức load một trang WebAssembly tiệm cận gần với load một file ảnh hơn là Javascript. Hãy nghĩ về điều này… các lập trình viên có thể rất tiêu cực về việc load 150kB JS, nhưng một file ảnh 150kB chẳng khiến ai bận tâm cả.

Developer advocate on the left tsk tsk-ing about large JS file. Developer advocate on the right shrugging about large image.

Nguyên nhân là vì file ảnh load rất nhanh, nó hầu như sẽ được giải mã bằng phần cứng, như Addy Osmani giải thích trong The Cost of JavaScript, hơn nữa giải mã ảnh không chặn main thread, như Alex Russell đã viết trong bài Can You Afford It?: Real-world Web Performance Budgets.

Điều này không có nghĩa chúng ta kì vọng file WebAssembly sẽ lớn như file ảnh. Những tool trong giai đoạn đầu của WebAssembly  tạo ra file khá lớn vì chúng phải chứa rất nhiều thư viện runtime, hiện nay có rất nhiều nỗ lực nhằm giảm dung lượng các file này. Ví dụ, Emscripten có “shrinking initiative” . Với Rust, bạn cũng có thể thấy file kết quả khá nhỏ với target wasm32-unknown-unknown, những tool như wasm-gcwasm-snip còn có thể tối ưu hóa nhiều hơn nữa.

Tất cả điều này có nghĩa là WebAssembly  sẽ load nhanh hơn rất nhiều so với file JavaScript tương đồng.

Điều này rất có ý nghĩa giống như Yehuda Katz đã chỉ ra, điều này sẽ thay đổi cuộc chơi

Tweet from Yehuda Katz saying it's possible to parse and compile wasm as fast as it comes over the network.

Vậy hãy cùng xem xét cách hoạt động của trình dịch mới

Dịch ngay trên Streaming: Bắt đầu dịch sớm hơn

Nếu bạn bắt đầu dịch sớm hơn, bạn sẽ kết thúc dịch sớm hơn. Đó là điều mà streaming compilation sẽ làm… bắt đầu dịch file .wasm sớm nhất có thể.

Khi bạn tải file, file không được chuyển trong một chuyến. Thay vào đó nó được chuyển thành một chuỗi packets.

Trước đây, với mỗi packet được download, trình duyệt đặt nó trong một ArrayBuffer.

Packets coming in to network layer and being added to an ArrayBuffer

Sau đó, khi đã tải xong, nó sẽ chuyển ArrayBuffer  cho Web VM (còn gọi là JS engine). Đó là khi WebAssembly bắt đầu được dịch.

Network layer pushing array buffer over to compiler

Nhưng thực tế chẳng có lý do gì để bắt trình dịch phải đơi. Về mặt kĩ thuật, WebAssembly  hoàn toàn có thể dịch line by line. Nghĩa là bạn có thể dịch ngay khi phần đầu tiên được chuyển đến.

Và đó là điều trình dịch mới sẽ làm, nó tận dụng tính năng của WebAssembly’s streaming API.

WebAssembly.instantiateStreaming call, which takes a response object with the source file. This has to be served using MIME type application/wasm.

Nếu bạn chuyển đối tượng trả về cho WebAssembly.instantiateStreaming, packets sẽ được chuyển thẳng đến WebAssembly engine. Trình dịch có thể bắt đầu dịch khi phần tiếp theo của file vẫn đang được download.

Packets going directly to compiler

Ngoài việc có thể dịch và download code song song, có một lợi ích khác khi thực hiện việc này.

Phân vùng code của file .wasm sẽ đến trước data (data nằm trong phân vùng memory). Vậy nên với streaming, trinh dịch có thể dịch phân vùng code trong khi data đang tải xuống. Nếu data rất lớn, cỡ một vài MB, thì cải thiện này rất có ý nghĩa.

File split between small code section at the top, and larger data section at the bottom

Với streaming, quá trính dịch sẽ bắt đầu nhanh hơn. Nhưng bản thân quá trình dịch cũng có thể nhanh hơn.

Tier 1 trình dịch cơ sở: dịch code nhanh hơn

Nếu bạn muốn code chạy nhanh, bạn cần tối ưu nó. Nhưng nếu thực hiện những tối ưu đó khi tiến hành dịch, sẽ làm quá trình dịch diễn ra chậm hơn. Đó là một dạng đánh đổi.

Chúng ta có thể có điểm tốt nhất của 2 thế giới. Nếu cúng ta dùng 2 lớp chương trình dịch, có thể dùng một cái dịch thật nhanh với không quá nhiều tối ưu, và một cái khác dịch chậm hơn nhưng thực hiện nhiều tối ưu hơn.

Cấu trúc như vậy gọi là tiered compiler. Khi code mới chuyển đến, nó sẽ được dịch bởi trình dịch thứ nhất (trình dịch cơ sở). Sau đó sau khi code cơ sở đã chạy, trình dịch thứ 2 sẽ dịch lại code một lần nữa ở background, code được tối ưu hơn.

Khi dịch xong, thay nóng phiên bản tối ưu với phiên bản trước đó. Giúp code chạy nhanh hơn

Timeline showing optimizing compiling happening in the background.

JS engine đã dùng tiered compilers từ rất lâu. Tuy nhiên nó chỉ dùng trình dịch tối ưu khi code trở nên nóng… đoạn code đó được gọi nhiều lần.

Ngược lại, với WebAssembly trình dịch tối ưu sẽ thực hiện với toàn bộ code, tối ưu toàn bộ code trong module. Trong tương lai có thể có một vài tùy chọn cho phép lập trình viên có thể tùy biến cách trình dịch tối ưu dịch toàn bộ code hay chỉ một phần.

Trình dịch cơ sở tiết kiệm rất nhiều thời gian khi khởi chạy. Nó dịch code nhanh hơn 10 – 15x so với trình dịch tối ưu. Và kết quả đạt được trong các lần test thường chỉ chậm hơn 2 lần.

Điều này có nghĩa là code cũng sẽ thực hiện khá nhanh, ngay trong lần chạy đầu tiên

Parallelize: Khiến nó nhanh hơn nữa

Trong bài viết về Firefox Quantum, Bạn đã biết về Fine-grained parallelism và Coarse-grained parallelism(bạn cũng có thể đọc về nó ở đây). WebAssembly có thể dịch theo cả hai phương pháp

Như đã nhắc đến ở trên, trình dịch tối ưu sẽ hoạt động ở tiến trình background. Nó sẽ không ảnh hưởng gì đến quá trình thực thi code ở tiến trình chính. Phiên bản dịch cơ sở vẫn có thể chạy khi trình dịch tối ưu đang làm việc.

Nhưng kể cả như vậy, hầu hết máy tính hiện nay đều có nhiều hơn 2 core. Để tận dụng tối đa số core có thể có, cả hai trình dịch cơ sở và tối ưu đều sử dụng fine-grained parallelization để chia nhỏ task.

Đơn vị xử lý song song ở đây là function. Mỗi function có thể biên dịch độc lập trên các core khác nhau. Đó là lý tưởng hóa, trên thực thể nhiều functions có thể được nhóm vào thành một vài batches, các batches này được gửi đi thực hiện ở các cores khác nhau.

… hoặc bỏ qua toàn bộ công đoạn này bằng việc dùng cache (future work)

Hiện nay, giải mã và biên dịch được thực hiện mỗi khi bạn load lại trang. Nhưng nếu bạn có cùng một file .wasm, nó sẽ biên dịch ra kết quả như nhau

Có nghĩa là với hầu hết trường hợp, công đoạn này có thể được bỏ qua. Trong tương lai, chức năng này sẽ được hiện thực hóa. Giải mã và biên dịch sẽ chỉ cần thực hiện trong lần load trang đầu tiên, và trình duyệt sẽ cache lại kết quả trên máy đích. Khi file cần dịch lại nó sẽ nạp lại kết quả từ cache.

Như vậy load time của các lần nạp sau sẽ về bằng 0.

Timeline showing all work disappearing with caching.

Với Firefox 58, một số công đoạn của chức năng này đã đang được thực hiện.  Byte code của Javascript đã có thể được cache sẵn , Chỉ cần một chút mở rộng để hỗ trợ cache cho file .wasm.

(Bài gốc)