Thay thế một đường dẫn nóng trong JavaScript của ứng dụng bằng WebAssembly

Luôn nhanh, yo

Trong các bài viết trước đây, tôi đã nói về cách WebAssembly cho phép bạn đưa hệ sinh thái thư viện C/C++ lên web. Một ứng dụng sử dụng rộng rãi thư viện C/C++ là squoosh, ứng dụng web của chúng tôi cho phép bạn nén hình ảnh bằng nhiều bộ mã hoá và giải mã đã được biên dịch từ C++ sang WebAssembly.

WebAssembly là một máy ảo cấp thấp chạy mã byte được lưu trữ trong các tệp .wasm. Mã byte này được nhập và cấu trúc rõ ràng theo cách có thể được biên dịch và tối ưu hoá cho hệ thống máy chủ nhanh hơn nhiều so với JavaScript. WebAssembly cung cấp một môi trường để chạy mã có hộp cát và nhúng ngay từ đầu.

Theo kinh nghiệm của tôi, hầu hết các vấn đề về hiệu suất trên web đều do bố cục bắt buộc và quá nhiều vẽ, nhưng thỉnh thoảng ứng dụng cần phải thực hiện một tác vụ tính toán tốn nhiều thời gian. WebAssembly có thể giúp bạn ở đây.

Con đường nóng bỏng

Trong squoosh, chúng tôi đã viết một hàm JavaScript xoay một vùng đệm hình ảnh theo bội số của 90 độ. Mặc dù OffscreenCanvas là lựa chọn lý tưởng cho trường hợp này, nhưng nó không được hỗ trợ trên các trình duyệt mà chúng tôi đang nhắm đến và có một chút lỗi trong Chrome.

Hàm này lặp lại mọi pixel của hình ảnh đầu vào và sao chép hình ảnh đó vào một vị trí khác trong hình ảnh đầu ra để đạt được độ xoay. Đối với hình ảnh 4094px x 4096px (16 megapixel), hệ thống cần hơn 16 triệu lần lặp lại khối mã bên trong. Đây gọi là "đường dẫn nóng". Mặc dù số lần lặp lại khá lớn, nhưng 2 trong số 3 trình duyệt mà chúng tôi đã kiểm thử đều hoàn thành tác vụ trong chưa đầy 2 giây. Thời lượng có thể chấp nhận được cho loại tương tác này.

for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

Tuy nhiên, một trình duyệt mất hơn 8 giây. Cách các trình duyệt tối ưu hoá JavaScript thực sự phức tạp và các công cụ khác nhau sẽ tối ưu hoá cho những mục đích khác nhau. Một số tối ưu hóa cho thực thi thô, một số tối ưu hóa cho tương tác với DOM. Trong trường hợp này, chúng ta đã gặp phải một đường dẫn chưa được tối ưu hoá trong một trình duyệt.

Mặt khác, WebAssembly được xây dựng hoàn toàn dựa trên tốc độ thực thi thô. Vì vậy, nếu chúng ta muốn có hiệu suất nhanh, dự đoán trên các trình duyệt cho mã như thế này, WebAssembly có thể giúp bạn.

WebAssembly giúp dự đoán hiệu suất

Nhìn chung, JavaScript và WebAssembly có thể đạt được cùng hiệu suất cao nhất. Tuy nhiên, đối với JavaScript, hiệu suất này chỉ có thể đạt được trên "đường dẫn nhanh" và thường rất khó để duy trì được "đường dẫn nhanh" đó. Một lợi ích chính mà WebAssembly mang lại là hiệu suất có thể dự đoán được, ngay cả trên các trình duyệt. Kiểu nhập nghiêm ngặt và cấu trúc cấp thấp cho phép trình biên dịch đưa ra các đảm bảo chắc chắn hơn để mã WebAssembly chỉ phải tối ưu hoá một lần và sẽ luôn sử dụng "đường dẫn nhanh".

Viết cho WebAssembly

Trước đây, chúng tôi đã lấy thư viện C/C++ và biên dịch các thư viện đó thành WebAssembly để sử dụng chức năng của chúng trên web. Chúng tôi thực sự không sử dụng mã của thư viện mà chỉ viết một số lượng nhỏ mã C/C++ để tạo thành cầu nối giữa trình duyệt và thư viện. Lần này, động lực của chúng tôi khác biệt: Chúng tôi muốn viết một điều gì đó từ đầu với ý tưởng về WebAssembly để có thể tận dụng những ưu điểm của WebAssembly.

Cấu trúc WebAssembly

Khi viết cho WebAssembly, bạn nên hiểu thêm một chút về WebAssembly thực sự là gì.

Cách trích dẫn nội dung của WebAssembly.org:

Khi biên dịch một đoạn mã C hoặc Rust thành WebAssembly, bạn sẽ nhận được tệp .wasm chứa nội dung khai báo mô-đun. Phần khai báo này bao gồm một danh sách các lệnh "nhập" mà mô-đun dự kiến từ môi trường của nó, một danh sách các tệp xuất mà mô-đun này cung cấp cho máy chủ lưu trữ (các hàm, hằng số, các phần bộ nhớ) và tất nhiên là cả các lệnh nhị phân thực tế cho các hàm có trong đó.

Điều mà tôi không nhận ra cho đến khi xem xét vấn đề này: Ngăn xếp khiến WebAssembly trở thành một "máy ảo dựa trên ngăn xếp" không được lưu trữ trong phân đoạn bộ nhớ mà các mô-đun WebAssembly sử dụng. Ngăn xếp này hoàn toàn nằm trong máy ảo nội bộ và không thể truy cập được đối với các nhà phát triển web (ngoại trừ thông qua Công cụ cho nhà phát triển). Do đó, bạn có thể viết các mô-đun WebAssembly mà không cần thêm bộ nhớ nào và chỉ sử dụng ngăn xếp nội bộ máy ảo.

Trong trường hợp này, chúng ta sẽ cần sử dụng một số bộ nhớ bổ sung để cho phép truy cập tuỳ ý vào các pixel của hình ảnh và tạo phiên bản xoay của hình ảnh đó. Đây là mục đích của WebAssembly.Memory.

Quản lý bộ nhớ

Thông thường, sau khi sử dụng thêm bộ nhớ, bạn sẽ cần phải quản lý bộ nhớ đó bằng cách nào đó. Những phần nào của bộ nhớ đang được sử dụng? Những ứng dụng nào miễn phí? Ví dụ: trong C, bạn có hàm malloc(n) tìm không gian bộ nhớ gồm n byte liên tiếp. Các hàm thuộc loại này còn được gọi là "allocators". Tất nhiên, việc triển khai trình phân bổ đang được sử dụng phải được đưa vào mô-đun WebAssembly và sẽ làm tăng kích thước tệp của bạn. Kích thước và hiệu suất của các hàm quản lý bộ nhớ này có thể thay đổi khá đáng kể tuỳ thuộc vào thuật toán được sử dụng. Đó là lý do tại sao nhiều ngôn ngữ cung cấp nhiều cách triển khai để lựa chọn ("dmalloc", "emmalloc", "wee_alloc", v.v.).

Trong trường hợp này, chúng ta biết kích thước của hình ảnh đầu vào (và do đó kích thước của hình ảnh đầu ra) trước khi chạy mô-đun WebAssembly. Ở đây, chúng ta đã nhìn thấy cơ hội: Thông thường, chúng ta sẽ truyền vùng đệm RGBA của hình ảnh đầu vào dưới dạng một tham số cho hàm WebAssembly và trả về hình ảnh được xoay làm giá trị trả về. Để tạo giá trị trả về đó, chúng ta phải sử dụng trình phân bổ. Tuy nhiên, vì biết tổng dung lượng bộ nhớ cần thiết (gấp đôi kích thước của hình ảnh đầu vào, một lần cho đầu vào và một lần cho đầu ra), chúng ta có thể đặt hình ảnh đầu vào vào bộ nhớ WebAssembly bằng JavaScript, chạy mô-đun WebAssembly để tạo hình ảnh thứ 2 được xoay rồi sử dụng JavaScript để đọc lại kết quả. Chúng ta có thể thoát mà không cần sử dụng bất kỳ công cụ quản lý bộ nhớ nào!

Tha hồ lựa chọn

Nếu đã xem hàm JavaScript gốc mà chúng ta muốn WebAssembly-fy, bạn có thể thấy rằng đó chỉ là một mã tính toán đơn thuần không có API dành riêng cho JavaScript. Do đó, việc chuyển mã này sang bất kỳ ngôn ngữ nào cũng khá đơn giản. Chúng tôi đã đánh giá 3 ngôn ngữ biên dịch thành WebAssembly: C/C++, Rust và hộiScript. Câu hỏi duy nhất chúng ta cần trả lời cho mỗi ngôn ngữ là: Làm thế nào để truy cập vào bộ nhớ thô mà không cần sử dụng các chức năng quản lý bộ nhớ?

C và Emscripten

Emscripten là trình biên dịch C cho mục tiêu WebAssembly. Mục tiêu của Emscripten là hoạt động thay thế các trình biên dịch C nổi tiếng như GCC hoặc clang và chủ yếu là tương thích với cờ. Đây là một phần cốt lõi trong sứ mệnh của Emscripten vì công ty này muốn việc biên dịch mã C và C++ hiện có thành WebAssembly trở nên dễ dàng nhất có thể.

Về bản chất, việc truy cập vào bộ nhớ thô của C và con trỏ tồn tại cũng vì lý do đó:

uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;

Ở đây, chúng ta sẽ chuyển số 0x124 thành một con trỏ đến các số nguyên 8 bit (hoặc byte) chưa ký. Điều này sẽ chuyển biến ptr thành một mảng một cách hiệu quả bắt đầu từ địa chỉ bộ nhớ 0x124 mà chúng ta có thể sử dụng như bất kỳ mảng nào khác, cho phép chúng ta truy cập vào các byte riêng lẻ để đọc và ghi. Trong trường hợp này, chúng ta đang xem vùng đệm RGBA của hình ảnh mà chúng ta muốn sắp xếp lại để đạt được độ xoay. Để di chuyển một pixel, chúng ta thực sự cần phải di chuyển 4 byte liên tiếp cùng một lúc (một byte cho mỗi kênh: R, G, B và A). Để làm việc này dễ dàng hơn, chúng ta có thể tạo một mảng số nguyên 32 bit chưa ký. Theo quy ước, hình ảnh đầu vào sẽ bắt đầu ở địa chỉ 4 và hình ảnh đầu ra sẽ bắt đầu ngay sau khi hình ảnh đầu vào kết thúc:

int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);

for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

Sau khi chuyển toàn bộ hàm JavaScript sang C, chúng ta có thể biên dịch tệp C bằng emcc:

$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c

Như thường lệ, emscripten tạo một tệp mã keo có tên là c.js và một mô-đun wasm có tên là c.wasm. Lưu ý rằng mô-đun wasm gzip chỉ đạt ~260 Byte, trong khi mã keo là khoảng 3,5KB sau gzip. Sau một số thử thách, chúng tôi đã có thể bỏ mã kết nối và tạo thực thể cho các mô-đun WebAssembly bằng các API vanilla. Bạn thường có thể thực hiện việc này với Emscripten, miễn là bạn không sử dụng bất kỳ nội dung nào trong thư viện chuẩn C.

Rust

Rust là một ngôn ngữ lập trình mới, hiện đại với một hệ thống kiểu phong phú, không có thời gian chạy và một mô hình quyền sở hữu đảm bảo độ an toàn của bộ nhớ cũng như độ an toàn của luồng. Rust cũng hỗ trợ WebAssembly như một tính năng cốt lõi và đội ngũ Rust đã đóng góp rất nhiều công cụ tuyệt vời cho hệ sinh thái WebAssembly.

Một trong những công cụ đó là wasm-pack, do nhóm làm việc rustwasm tạo ra. wasm-pack lấy mã của bạn và biến mã đó thành một mô-đun thân thiện với web, có thể hoạt động ngay với các gói như webpack. wasm-pack là một trải nghiệm cực kỳ tiện lợi, nhưng hiện chỉ hoạt động với Rust. Nhóm này đang cân nhắc việc thêm tính năng hỗ trợ cho các ngôn ngữ nhắm mục tiêu WebAssembly khác.

Trong Rust, lát cắt là những mảng nằm trong C. Giống như trong C, chúng ta cần tạo các lát cắt sử dụng địa chỉ bắt đầu. Điều này đi ngược lại mô hình an toàn của bộ nhớ mà Rust thực thi. Vì vậy, để làm được điều này, chúng ta phải sử dụng từ khoá unsafe, cho phép chúng ta viết mã không tuân thủ mô hình đó.

let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
    inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
    outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}

for d2 in 0..d2Limit {
    for d1 in 0..d1Limit {
    let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
    outBuffer[i as usize] = inBuffer[in_idx as usize];
    i += 1;
    }
}

Biên dịch các tệp Rust bằng cách sử dụng

$ wasm-pack build

tạo ra mô-đun wasm 7,6KB với khoảng 100 byte mã keo (cả hai sau gzip).

AssemblyScript

AssemblyScript là một dự án khá trẻ, hướng đến mục tiêu trở thành trình biên dịch TypeScript đến WebAssembly. Tuy nhiên, điều quan trọng cần lưu ý là khối này không chỉ sử dụng bất kỳ TypeScript nào. MultiplexScript sử dụng cú pháp tương tự như TypeScript nhưng chuyển đổi thư viện chuẩn cho riêng chúng. Thư viện chuẩn của họ mô hình hoá các tính năng của WebAssembly. Điều đó có nghĩa là bạn không thể chỉ biên dịch bất kỳ TypeScript nào bạn đang sử dụng WebAssembly, nhưng điều đó có nghĩa là bạn không cần phải học ngôn ngữ lập trình mới để viết WebAssembly!

    for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
      for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
        let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
        store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
        i += 1;
      }
    }

Vì bề mặt kiểu dữ liệu nhỏ mà hàm rotate() có, nên bạn có thể dễ dàng chuyển mã này sang CouncilScript. Các hàm load<T>(ptr: usize)store<T>(ptr: usize, value: T) do hộiScript cung cấp để truy cập vào bộ nhớ thô. Để biên dịch tệp hộiScript của mình, chúng ta chỉ cần cài đặt gói AssemblyScript/assemblyscript npm và chạy

$ asc rotate.ts -b assemblyscript.wasm --validate -O3

hộiScript sẽ cung cấp cho chúng ta một mô-đun wasm ~300 Byte và không có mã keo. Mô-đun này chỉ hoạt động với API WebAssembly vanilla.

Điều tra viên WebAssembly

7,6KB của Rust lớn bất ngờ khi so sánh với 2 ngôn ngữ khác. Có một số công cụ trong hệ sinh thái WebAssembly có thể giúp bạn phân tích các tệp WebAssembly của mình (bất kể bạn tạo bằng ngôn ngữ nào) và cho bạn biết điều gì đang xảy ra, đồng thời giúp bạn cải thiện tình hình của mình.

Twiggy

Twiggy là một công cụ khác của nhóm WebAssembly của Rust, có chức năng trích xuất một loạt dữ liệu chi tiết từ mô-đun WebAssembly. Công cụ này không dành riêng cho Rust và cho phép bạn kiểm tra những nội dung như biểu đồ lệnh gọi của mô-đun, xác định các phần không dùng đến hoặc không cần thiết và tìm ra những phần đóng góp vào tổng kích thước tệp của mô-đun. Bạn có thể thực hiện thao tác sau bằng lệnh top của Twiggy:

$ twiggy top rotate_bg.wasm
Ảnh chụp màn hình quá trình cài đặt Twiggy

Trong trường hợp này, chúng ta có thể thấy rằng phần lớn kích thước tệp bắt nguồn từ bộ phân bổ. Điều đó thật đáng ngạc nhiên vì mã của chúng ta không sử dụng tính năng phân bổ động. Một yếu tố lớn khác góp phần quan trọng là phần phụ "tên hàm".

dải wasm

wasm-strip là một công cụ trong Bộ công cụ nhị phân WebAssembly, hay viết tắt là wabt. Thư viện này chứa một số công cụ cho phép bạn kiểm tra và thao tác với các mô-đun WebAssembly. wasm2wat là một trình phân tách có thể biến mô-đun wasm nhị phân thành định dạng mà con người có thể đọc được. Wabt cũng chứa wat2wasm cho phép bạn chuyển định dạng mà con người có thể đọc được trở lại thành mô-đun wasm nhị phân. Mặc dù đã sử dụng hai công cụ bổ sung này để kiểm tra các tệp WebAssembly, nhưng chúng tôi thấy wasm-strip là công cụ hữu ích nhất. wasm-strip sẽ xoá các phần và siêu dữ liệu không cần thiết khỏi mô-đun WebAssembly:

$ wasm-strip rotate_bg.wasm

Điều này làm giảm kích thước tệp của mô-đun gỉ từ 7,5KB xuống còn 6,6KB (sau gzip).

wasm-opt

wasm-opt là một công cụ của Binaryen. Nó sử dụng một mô-đun WebAssembly và cố gắng tối ưu hoá mô-đun đó cả về kích thước và hiệu suất chỉ dựa trên mã byte. Một số công cụ như Emscripten đã chạy công cụ này, một số công cụ khác thì chưa. Thông thường, bạn nên thử và lưu một số byte bổ sung bằng cách sử dụng các công cụ này.

wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm

Với wasm-opt, chúng ta có thể xoá bớt một số byte khác để còn lại tổng cộng 6,2KB sau gzip.

#![no_std]

Sau một thời gian tham khảo và nghiên cứu, chúng tôi đã viết lại mã Rust mà không dùng thư viện chuẩn của Rust bằng tính năng #![no_std]. Thao tác này cũng sẽ tắt hoàn toàn cơ chế phân bổ bộ nhớ động, xoá mã bộ phân bổ khỏi mô-đun của chúng ta. Biên dịch tệp Rust này bằng

$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs

tạo ra mô-đun wasm 1,6KB sau wasm-opt, wasm-strip và gzip. Mặc dù vẫn lớn hơn các mô-đun do C vàassemblyScript tạo, nhưng khối này đủ nhỏ để được coi là một khối nhẹ.

Hiệu suất

Trước khi đi đến kết luận chỉ dựa trên kích thước tệp — chúng tôi đã thực hiện hành trình này để tối ưu hoá hiệu suất chứ không phải kích thước tệp. Vậy chúng tôi đo lường hiệu suất như thế nào và kết quả là gì?

Cách đo điểm chuẩn

Mặc dù WebAssembly là một định dạng mã byte cấp thấp, nhưng bạn vẫn cần gửi định dạng này thông qua một trình biên dịch để tạo mã máy dành riêng cho máy chủ. Cũng giống như JavaScript, trình biên dịch hoạt động ở nhiều giai đoạn. Nói một cách đơn giản: Giai đoạn đầu tiên biên dịch nhanh hơn nhiều nhưng có xu hướng tạo mã chậm hơn. Sau khi mô-đun bắt đầu chạy, trình duyệt sẽ quan sát xem những phần nào thường được sử dụng và gửi các phần đó thông qua một trình biên dịch tối ưu hoá hơn nhưng có tốc độ chậm hơn.

Trường hợp sử dụng của chúng ta thú vị ở chỗ mã để xoay hình ảnh sẽ được sử dụng một lần hoặc có thể hai lần. Vì vậy, trong phần lớn trường hợp, chúng tôi sẽ không bao giờ nhận được lợi ích của trình biên dịch tối ưu hoá. Điều này rất quan trọng cần lưu ý khi đo điểm chuẩn. Chạy các mô-đun WebAssembly của chúng tôi 10.000 lần trong một vòng lặp sẽ cho ra kết quả không thực tế. Để có con số thực tế, chúng ta nên chạy mô-đun một lần và đưa ra quyết định dựa trên các số liệu trong lần chạy đó.

So sánh hiệu suất

So sánh tốc độ theo ngôn ngữ
So sánh tốc độ theo trình duyệt

Hai biểu đồ này là các chế độ xem khác nhau về cùng một dữ liệu. Ở biểu đồ đầu tiên, chúng tôi so sánh theo trình duyệt, trong biểu đồ thứ hai, chúng tôi so sánh theo ngôn ngữ được sử dụng. Xin lưu ý rằng tôi đã chọn thang thời gian logarit. Một điều quan trọng nữa là tất cả các điểm chuẩn đều phải sử dụng cùng một hình ảnh thử nghiệm 16 megapixel và cùng một máy chủ, ngoại trừ một trình duyệt không thể chạy trên cùng một máy.

Nếu không phân tích quá nhiều các biểu đồ này, thì rõ ràng chúng ta đã giải quyết được vấn đề về hiệu suất ban đầu: Tất cả các mô-đun WebAssembly đều chạy trong khoảng 500 mili giây trở xuống. Điều này xác nhận những gì chúng tôi đã đề ra ngay từ đầu: WebAssembly mang đến cho bạn hiệu suất dự đoán. Bất kể chúng tôi chọn ngôn ngữ nào, sự khác biệt giữa trình duyệt và ngôn ngữ là rất nhỏ. Chính xác: Độ lệch chuẩn của JavaScript trên tất cả các trình duyệt là ~ 400 mili giây, trong khi độ lệch chuẩn của tất cả các mô-đun WebAssembly của chúng tôi trên tất cả các trình duyệt là ~ 80 mili giây.

Nỗ lực

Một chỉ số khác là mức độ nỗ lực của chúng tôi để tạo và tích hợp mô-đun WebAssembly vào squoosh. Thật khó để chỉ định một giá trị số cho công sức, vì vậy tôi sẽ không tạo bất kỳ biểu đồ nào nhưng có một vài điều tôi muốn chỉ ra:

hộiScript dễ sử dụng. API này không chỉ cho phép bạn sử dụng TypeScript để viết WebAssembly, giúp đồng nghiệp dễ dàng xem xét mã mà còn tạo ra các mô-đun WebAssembly không có keo, rất nhỏ với hiệu suất tốt. Công cụ trong hệ sinh thái TypeScript, như đẹp hơn và tslint, sẽ chỉ hoạt động.

Rust kết hợp với wasm-pack cũng cực kỳ thuận tiện, nhưng hiệu quả hơn ở các dự án WebAssembly lớn hơn là liên kết và cần quản lý bộ nhớ. Chúng tôi đã phải chuyển hướng một chút khỏi lộ trình phù hợp để đạt được kích thước tệp cạnh tranh.

C và Emscripten đã tạo ra một mô-đun WebAssembly rất nhỏ và có hiệu suất cao ngay từ đầu, nhưng nếu không có can đảm đi vào mã keo và giảm kích thước xuống mức cần thiết, thì tổng kích thước (mô-đun WebAssembly + mã keo) sẽ khá lớn.

Kết luận

Vậy bạn nên sử dụng ngôn ngữ nào nếu có đường dẫn nóng JS và muốn làm cho đường dẫn đó nhanh hơn hoặc nhất quán hơn với WebAssembly. Như thường lệ với các câu hỏi về hiệu suất, câu trả lời là: Tuỳ trường hợp. Vậy chúng tôi đã vận chuyển những gì?

Biểu đồ so sánh

So sánh ở kích thước mô-đun / sự đánh đổi hiệu suất của các ngôn ngữ khác nhau mà chúng tôi đã sử dụng, lựa chọn tốt nhất có vẻ là C hoặc CouncilScript. Chúng tôi quyết định gửi Rust. Có nhiều lý do dẫn đến quyết định này: Tất cả các bộ mã hoá và giải mã được chuyển trong Squoosh cho đến nay đều được biên dịch bằng Emscripten. Chúng tôi muốn mở rộng kiến thức về hệ sinh thái WebAssembly và sử dụng một ngôn ngữ khác trong quá trình sản xuất. hộiScript là một giải pháp thay thế mạnh mẽ, nhưng dự án còn tương đối trẻ và trình biên dịch chưa hoàn thiện như trình biên dịch Rust.

Mặc dù sự khác biệt về kích thước tệp giữa Rust và các ngôn ngữ khác có vẻ khá lớn trong biểu đồ phân tán, nhưng thực tế thì đây không phải là vấn đề lớn: Việc tải 500B hoặc 1,6KB ngay cả trên 2G chỉ mất chưa đến 1/10 giây. Và Ruust hy vọng sẽ sớm thu hẹp khoảng cách về kích thước mô-đun.

Xét về hiệu suất trong thời gian chạy, Rust có mức trung bình nhanh hơn trên các trình duyệt so với AssemblyScript. Đặc biệt là trên các dự án lớn hơn, Rust sẽ có nhiều khả năng tạo ra mã nhanh hơn mà không cần tối ưu hoá mã theo cách thủ công. Nhưng điều đó sẽ không ngăn bạn sử dụng những gì bạn thấy thoải mái nhất.

Tất cả những gì đang nói đến: CouncilScript là một khám phá tuyệt vời. API này cho phép các nhà phát triển web tạo các mô-đun WebAssembly mà không cần phải học ngôn ngữ mới. Nhóm BoardScript phản hồi rất nhanh và đang tích cực tìm cách cải thiện chuỗi công cụ của họ. Chắc chắn chúng tôi sẽ theo dõi AssemblyScript trong tương lai.

Cập nhật: Rust

Sau khi xuất bản bài viết này, Nick Fitzgerald thuộc nhóm Rust đã giới thiệu cho chúng tôi cuốn sách Rust Wasm xuất sắc của họ, trong đó có một phần hướng dẫn cách tối ưu hoá kích thước tệp. Làm theo hướng dẫn ở đó (đáng chú ý nhất là bật tính năng tối ưu hoá thời gian liên kết và xử lý hoảng loạn thủ công) cho phép chúng tôi viết mã Rust "bình thường" và quay lại sử dụng Cargo (npm của Rust) mà không làm tăng kích thước tệp. Mô-đun Rust kết thúc với 370B sau gzip. Để biết thông tin chi tiết, vui lòng xem tài liệu PR mà tôi mở trên Squoosh.

Xin đặc biệt cảm ơn Ashley Williams, Steve Klabnik, Nick FitzgeraldMax Graey vì đã luôn giúp đỡ trong hành trình này.