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

Luôn nhanh

Trong các bài viết trước 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 tận dụng thư viện C/C++ rộng rãi 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++ thành 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ó cấu trúc mạnh theo cách có thể được biên dịch và tối ưu hoá cho hệ thống lưu trữ nhanh hơn nhiều so với JavaScript. WebAssembly cung cấp một môi trường để chạy mã có tính đến hộp cát và nhúng ngay từ đầu.

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

Con đường nóng

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

Hàm này lặp lại mỗi pixel của hình ảnh đầu vào rồi 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 một hình ảnh 4094px x 4096px (16 megapixel), bạn cần hơn 16 triệu vòng lặp của khối mã bên trong, đây là cái được gọi là "đường dẫn nóng". Mặc dù số lần lặp lại khá lớn, nhưng 2 trên 3 trình duyệt mà chúng tôi đã kiểm thử sẽ hoàn thành tác vụ trong vòng 2 giây. Thời lượng 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ố chiến lược tối ưu hoá để thực thi thô, một số tối ưu hoá để tương tác với DOM. Trong trường hợp này, chúng ta đã gặp phải đường dẫn chưa được tối ưu hoá trong một trình duyệt.

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

WebAssembly cho hiệu suất có thể dự đoán

Nhìn chung, JavaScript và WebAssembly có thể đạt được cùng một 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ì trên "đường dẫn nhanh đó". Một lợi ích chính mà WebAssembly mang lại là có thể dự đoán hiệu suất, ngay cả trên nhiều trình duyệt. Việc nhập chính xác và cấu trúc cấp thấp cho phép trình biên dịch đảm bảo chắc chắn hơn để mã WebAssembly chỉ phải được 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 các 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 không thực sự tác động đến mã của thư viện, chúng tôi chỉ viết một lượng nhỏ mã C/C++ để tạo 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 thay đổi: Chúng tôi muốn viết một cái gì đó từ đầu theo hướng WebAssembly để có thể tận dụng những ưu điểm mà WebAssembly có.

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 trang web 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 một tệp .wasm chứa nội dung khai báo mô-đun. Nội dung khai báo này bao gồm danh sách các mục "nhập" mà mô-đun dự kiến từ môi trường của mô-đun, danh sách các tệp xuất mà mô-đun này cung cấp cho máy chủ (các hàm, hằng số, phần bộ nhớ) và tất nhiên là 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 bộ nhớ mà các mô-đun WebAssembly sử dụng. Ngăn xếp hoàn toàn lưu trong máy ảo và nhà phát triển web không thể truy cập vào (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ớ và chỉ sử dụng ngăn xếp máy ảo nội bộ.

Trong trường hợp này, chúng ta sẽ cần sử dụng thêm một số bộ nhớ để 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ớ đó theo cách nào đó. Những phần nào của bộ nhớ đang được sử dụng? Những chương trình nào miễn phí? Ví dụ: trong C, bạn có hàm malloc(n) tìm dung lượng bộ nhớ là n byte liên tiếp. Các hàm thuộc loại này còn được gọi là "trình phân bổ". 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ẽ tăng kích thước tệp. 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 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à kích thước của hình ảnh đầu ra) trước khi chạy mô-đun WebAssembly. Ở đây, chúng tôi đã nhìn thấy một cơ hội: Thông thường, chúng tôi 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 xoay dưới dạng một 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), nên chúng ta có thể đưa hình ảnh đầu vào vào bộ nhớ WebAssembly bằng JavaScript, chạy mô-đun WebAssembly để tạo hình ảnh xoay thứ 2 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 tính năng quản lý bộ nhớ nào!

Không tha hồ lựa chọn

Nếu nhìn vào hàm JavaScript gốc mà chúng ta muốn WebAssembly-fy, bạn có thể thấy đó là một mã hoàn toàn tính toá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à AssemblyScript. Câu hỏi duy nhất chúng ta cần trả lời cho từng 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 hàm quản lý bộ nhớ?

C và Emscripten

Emscripten là một trình biên dịch C cho mục tiêu WebAssembly. Mục tiêu của Emscripten là hoạt động như một giải pháp thay thế bổ sung cho các trình biên dịch C phổ biến như GCC hoặc clang và hầu hết đều 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ì họ muốn quá trình biên dịch mã C và C++ hiện có sang WebAssembly dễ dàng nhất có thể.

Việc truy cập bộ nhớ thô thuộc bản chất của C và các con trỏ tồn tại 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ỏ tới số nguyên 8 bit (hoặc byte) chưa ký. Cách này sẽ biến ptr một cách hiệu quả thành một mảng bắt đầu từ địa chỉ bộ nhớ 0x124. Chúng ta có thể sử dụng mảng này 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 xét 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 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 không dấu. Theo quy ước, hình ảnh đầu vào của chúng tôi 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 sẽ 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 sẽ chỉ chiếm khoảng 260 Byte, trong khi mã keo là khoảng 3,5 KB sau gzip. Sau một thời gian tìm hiểu, chúng tôi đã có thể loại bỏ mã keo và tạo thực thể cho các mô-đun WebAssembly bằng API vanilla. Bạn thường có thể thực hiện điều này với Emscripten miễn là bạn không sử dụng bất cứ thứ gì từ 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 hệ thống kiểu dữ liệu phong phú, không có thời gian chạy và mô hình quyền sở hữu giúp đảm bảo độ an toàn của bộ nhớ cũng như độ an toàn cho luồng. Rust cũng hỗ trợ WebAssembly dưới dạng một tính năng cốt lõi và nhóm Rust đã đóng góp rất nhiều công cụ tuyệt vời vào hệ sinh thái WebAssembly.

Một trong những công cụ này là wasm-pack, của nhóm làm việc Hiệu quả độc lập. 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, hoạt động ngay lập tức với các trình đóng 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ỉ áp dụng cho Rust. Nhóm này đang cân nhắ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 thuộc C. Và 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 với mô hình an toàn bộ nhớ mà Rust thực thi. Vì vậy, để hiểu rõ cách 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

$ wasm-pack build

tạo ra một 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ẻ nhằm trở thành một trình biên dịch TypeScript-to-WebAssembly. Tuy nhiên, điều quan trọng cần lưu ý là nó sẽ không sử dụng bất kỳ TypeScript nào. AssemblyScript sử dụng cú pháp tương tự như TypeScript nhưng chuyển thư viện chuẩn cho riêng chúng. Thư viện chuẩn của họ mô hình hoá các chức năng của WebAssembly. Điều đó có nghĩa là bạn không thể biên dịch bất kỳ TypeScript nào bạn muốn dựa vào WebAssembly, mà nghĩa là bạn không phải học một 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;
      }
    }

Xem xét giao diện kiểu nhỏ mà hàm rotate() của chúng ta có, khá dễ dàng để chuyển mã này sang AssemblyScript. Các hàm load<T>(ptr: usize)store<T>(ptr: usize, value: T) do AssemblyScript cung cấp để truy cập vào bộ nhớ thô. Để biên dịch tệp AssemblyScript, 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

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

Điều tra pháp y của WebAssembly

7.6KB của Rust lớn đến kinh ngạc 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 (bất kể ngôn ngữ mà 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 huống.

Chó săn

Twiggy là một công cụ khác của nhóm WebAssembly của Rust. Công cụ này 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 thứ 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 phần nào đang đó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 phương thứ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ừ trình phân bổ. Điều này thật đáng ngạc nhiên vì mã của chúng ta không sử dụng mô hình phân bổ động. Một yếu tố đóng góp quan trọng khác là tiểu mục "tên hàm".

dải wasm

wasm-strip là một công cụ trong WebAssembly Binary Toolkit, 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 công cụ tháo rời, biến mô-đun wasm nhị phân thành một định dạng 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 thành mô-đun wasm nhị phân. Mặc dù có dùng 2 công cụ bổ sung này để kiểm tra tệp WebAssembly, nhưng chúng tôi nhận thấy wasm-strip là hữu ích nhất. wasm-strip 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

Thao tác này giúp 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. Phương thức này sẽ sử dụng một mô-đun WebAssembly và cố gắng tối ưu hoá mô-đun đó cho cả 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ì không. 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ể cắt bớt một số byte khác để còn lại tổng cộng 6,2 KB sau gzip.

#?[no_std]

Sau khi tham khảo ý kiến và nghiên cứu, chúng tôi đã viết lại mã Rust mà không sử dụng thư viện chuẩn của Rust thông qua tính năng #![no_std]. Thao tác này cũng sẽ vô hiệu hoá hoàn toàn quá trình phân bổ bộ nhớ động, xoá mã trình 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

thu được mô-đun wasm 1,6KB sau wasm-opt, wasm-strip và gzip. Mặc dù kích thước này vẫn lớn hơn các mô-đun do C và AssemblyScript tạo ra, nhưng vẫn đủ nhỏ để được coi là một mô-đun 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 tiếp tục 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 và kết quả như thế nào?

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

Mặc dù WebAssembly là một định dạng mã byte cấp thấp, bạn vẫn cần gửi nó 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 trong nhiều giai đoạn. Nói đơ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 những phần thường được dùng và gửi các phần đó thông qua một trình biên dịch có khả năng tối ưu hoá cao hơn nhưng chậm hơn.

Trường hợp sử dụng của chúng ta rất thú vị là mã để xoay hình ảnh sẽ được sử dụng một lần, có thể là hai lần. Vì vậy, trong hầu hết các trường hợp, chúng ta sẽ không bao giờ nhận được lợi ích từ trình biên dịch tối ưu hoá. Đây là điều quan trọng cần lưu ý khi đo điểm chuẩn. Việc chạy các mô-đun WebAssembly 10.000 lần trong một vòng lặp sẽ mang lại kết quả không thực tế. Để có được cá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 con số từ 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 độ trên mỗi 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. Trong biểu đồ đầu tiên, chúng tôi so sánh theo trình duyệt, còn 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ểm quan trọng nữa là tất cả các điểm chuẩn đều 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 các biểu đồ này quá nhiều, thì rõ ràng chúng tôi đã giải quyết được vấn đề ban đầu về hiệu suất: 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 khẳng định nội dung chúng tôi đặt ra ban đầu: WebAssembly cung cấp cho bạn hiệu suất có thể dự đoán. Bất kể chúng ta chọn ngôn ngữ nào, sự chênh lệch giữa các 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 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à chúng tôi đã nỗ lực hết sức để tạo và tích hợp mô-đun WebAssembly vào squoosh. Rất khó để gán một giá trị số, vì vậy, tôi sẽ không tạo bất kỳ biểu đồ nào, nhưng tôi muốn chỉ ra một vài điều sau đây:

AssemblyScript hoạt động trơn tru. Tính năng này không chỉ cho phép bạn sử dụng TypeScript để viết WebAssembly, giúp đồng nghiệp của tôi dễ dàng xem xét mã, mà còn tạo ra các mô-đun WebAssembly không có keo có kích thước rất nhỏ với hiệu suất khá. Công cụ trong hệ sinh thái TypeScript, chẳng hạn như Prettier và tslint, có thể sẽ hoạt động hiệu quả.

Việc gỉ kết hợp với wasm-pack cũng cực kỳ tiện lợi, nhưng việc làm tốt hơn ở các dự án WebAssembly lớn hơn là cần phải liên kết và quản lý bộ nhớ. Chúng tôi đã phải phân biệt một chút so với lộ trình hạnh phúc để đạ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à hiệu quả cao ngay từ đầu, nhưng không có đủ can đảm để nhảy vào mã kết dính và giảm nó xuống mức cần thiết mà tổng kích thước (mô-đun WebAssembly + mã keo) khá lớn.

Kết luận

Vậy bạn nên sử dụng ngôn ngữ nào nếu bạn có đường dẫn nóng JS và muốn tăng tốc độ hoặc sự nhất quán của đường dẫ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 sẽ là: Còn tuỳ. Vậy chúng tôi đã vận chuyển những gì?

Biểu đồ so sánh

Khi so sánh kích thước mô-đun / đánh đổi hiệu suất của nhiều ngôn ngữ mà chúng tôi sử dụng, có vẻ như lựa chọn tốt nhất là C hoặc AssemblyScript. Chúng tôi quyết định vận chuyển Rust. Có nhiều lý do cho quyết định này: Tất cả các bộ mã hoá và giải mã được vận chuyển trong Squoosh đế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 thực tế. AssemblyScript 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ó kích thước khá lớn trong biểu đồ phân tán, nhưng trên thực tế, việc tải 500B hoặc 1,6 KB thậm chí trên 2G chỉ mất chưa đến 1/10 giây. và Rust hy vọng sẽ sớm thu hẹp khoảng cách về kích thước mô-đun.

Về hiệu suất thời gian chạy, Rust có tốc độ trung bình nhanh hơn trên các trình duyệt so với AssemblyScript. Đặc biệt là trong các dự án lớ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. Tuy nhiên, điều đó không ngăn được bạn sử dụng những gì mình thấy thoải mái nhất.

Dù sao đi nữa, chúng ta cũng có thể nói rằng AssemblyScript là một khám phá tuyệt vời. Phương thức 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 một ngôn ngữ mới. Nhóm AssemblyScript 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: Gỉ sét

Sau khi xuất bản bài viết này, Nick Fitzgerald thuộc đội ngũ Rust đã giới thiệu cho chúng tôi cuốn sách tuyệt vời của họ về Rust Wasm, trong đó có một phần về cách tối ưu hoá kích thước tệp. Làm theo hướng dẫn ở đó (đặc biệt là việc bật tính năng tối ưu hoá thời gian liên kết và xử lý tình trạng hoảng loạn theo cách 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 chi tiết, vui lòng xem bài đăng PR mà tôi đã mở trên Squoosh.

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