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

Luôn nhanh chóng

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 kiểu và cấu trúc chặt chẽ theo cách có thể 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ăng 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à vẽ quá nhiều gây ra, nhưng thỉnh thoảng, ứng dụng cần thực hiện một tác vụ tốn kém về mặt tính toán và mất nhiều thời gian. WebAssembly có thể giúp bạn trong trường hợp này.

Đường dẫn nóng

Trong squoosh, chúng ta đã viết một hàm JavaScript xoay 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 việc này, nhưng lớp này không được hỗ trợ trên các trình duyệt mà chúng tôi nhắm đến và có một chút lỗi trong Chrome.

Hàm này lặp lại trên mọi pixel của hình ảnh đầu vào và sao chép pixel đó sang một vị trí khác trong hình ảnh đầu ra để thực hiện thao tác xoay. Đối với hình ảnh 4094px x 4096px (16 megapixel), bạn sẽ cần hơn 16 triệu lần lặp của khối mã bên trong, đây là điều mà chúng ta 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 thử nghiệm đã hoàn thành tác vụ trong vòng 2 giây trở xuống. Thời lượng được chấp nhận 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 trình duyệt tối ưu hoá JavaScript rất phức tạp và các công cụ khác nhau sẽ tối ưu hoá cho các mục đích khác nhau. Một số tối ưu hoá cho việc thực thi thô, một số tối ưu hoá cho việc 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, có thể dự đoán trên các trình duyệt cho mã như thế này, WebAssembly có thể giúp ích.

WebAssembly để dự đoán hiệu suất

Nhìn chung, JavaScript và WebAssembly có thể đạt được cùng một hiệu suất đỉnh. Tuy nhiên, đối với JavaScript, bạn chỉ có thể đạt được hiệu suất này trên "đường dẫn nhanh" và thường khó duy trì "đườ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. Việc nhập liệu 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 mạnh mẽ 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 ta đã 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 các thư viện đó trên web. Chúng ta không thực sự đụng đến mã của các thư viện, mà 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 ta khác: Chúng ta muốn viết một số nội dung từ đầu bằng WebAssembly để có thể tận dụng các lợi thế 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ề bản chất của WebAssembly.

Để trích dẫn 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 "lệnh nhập" mà mô-đun dự kiến sẽ nhận được từ môi trường của mô-đun, danh sách lệnh xuất mà mô-đun này cung cấp cho máy chủ (hàm, hằng số, khối bộ nhớ) và tất nhiên là các lệnh nhị phân thực tế cho các hàm có trong đó.

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

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ẽ thấy cần phải quản lý bộ nhớ đó theo cách nào đó. Những phần bộ nhớ nào đang được sử dụng? Những tính năng nào miễn phí? Ví dụ: trong C, bạn có hàm malloc(n) tìm không gian bộ nhớ của n byte liên tiếp. Các hàm thuộc loại này cũng được gọi là "trình phân bổ". Tất nhiên, việc triển khai trình phân bổ đang sử dụng phải được đưa vào mô-đun WebAssembly và sẽ làm 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 đá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 phương thức triển khai để bạn 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 đó là kích thước của hình ảnh đầu ra) trước khi chạy mô-đun WebAssembly. Ở đây, chúng ta thấy một cơ hội: Theo truyền thống, chúng ta sẽ truyền vùng đệm RGBA của hình ảnh đầu vào dưới dạng tham số đến một hàm WebAssembly và trả về hình ảnh đã xoay dưới dạng giá trị trả về. Để tạo giá trị trả về đó, chúng ta phải sử dụng trình phân bổ. Nhưng vì chúng ta 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ể đặ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ứ hai, đã xoay rồi sử dụng JavaScript để đọc lại kết quả. Chúng ta có thể giải quyết vấn đề này mà không cần sử dụng bất kỳ tính năng quản lý bộ nhớ nào!

Có nhiều lựa chọn

Nếu đã xem hàm JavaScript ban đầu mà chúng ta muốn chuyển sang WebAssembly, bạn có thể thấy đó là một mã thuần tuý về tính toán mà không có API dành riêng cho JavaScript. Do đó, bạn có thể chuyển mã này sang bất kỳ ngôn ngữ nào một cách dễ dàng. Chúng tôi đã đánh giá 3 ngôn ngữ biên dịch sang WebAssembly: C/C++, Rust và AssemblyScript. Câu hỏi duy nhất chúng ta cần trả lời cho mỗi ngôn ngữ là: Làm cách 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 trình thay thế thả vào cho các trình biên dịch C nổi tiếng như GCC hoặc clang và chủ yế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ì nó muốn biên dịch mã C và C++ hiện có sang WebAssembly một cách dễ dàng nhất có thể.

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

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

Ở đây, chúng ta đang chuyển số 0x124 thành con trỏ đến số nguyên (hoặc byte) 8 bit không dấu. Thao tác này giúp biến ptr trở thành một mảng bắt đầu tại đị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 từng byte để đọc và ghi. Trong trường hợp này, chúng ta đang xem xét một vùng đệm RGBA của hình ảnh mà chúng ta muốn sắp xếp lại để thực hiện việ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). Để 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 tại đị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ư mọi khi, 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. Xin lưu ý rằng mô-đun wasm chỉ nén được khoảng 260 Byte, trong khi mã keo có kích thước khoảng 3,5 KB sau khi nén. Sau một số thao tác, chúng tôi có thể loại bỏ mã keo và tạo bản sao các mô-đun WebAssembly bằng API gốc. Bạn thường có thể làm được điều 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 hệ thống loại phong phú, không có thời gian chạy và mô hình quyền sở hữu đảm bảo an toàn cho bộ nhớ và an toàn cho luồng. Rust cũng hỗ trợ WebAssembly làm một tính năng cốt lõi và nhóm Rust đã đóng góp nhiều công cụ tuyệt vời cho hệ sinh thái WebAssembly.

Một trong những công cụ này là wasm-pack, do nhóm làm việc rustwasm phát triển. 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 kết hợp như webpack. wasm-pack là một trải nghiệm cực kỳ thuận tiện, nhưng hiện chỉ hoạt động với Rust. Nhóm này đang cân nhắc việc hỗ trợ thêm 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 trong 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 của mình. Điều này trái với mô hình an toàn bộ nhớ mà Rust thực thi, vì vậy, để đạt được mục tiêu, 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,6 KB với khoảng 100 byte mã keo (cả sau khi gzip).

AssemblyScript

AssemblyScript là một dự án khá mới, hướng đến việc trở thành trình biên dịch TypeScript sang WebAssembly. Tuy nhiên, điều quan trọng cần lưu ý là công cụ này sẽ không chỉ sử dụng bất kỳ TypeScript nào. AssemblyScript sử dụng cú pháp giống như TypeScript nhưng chuyển đổi thư viện tiêu chuẩn thành thư viện của riêng mình. 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ể chỉ biên dịch bất kỳ TypeScript nào bạn có sẵn sang WebAssembly, nhưng không có 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;
      }
    }

Xét bề mặt 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 npm AssemblyScript/assemblyscript và chạy

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

AssemblyScript sẽ cung cấp cho chúng ta một mô-đun wasm khoảng 300 Byte và không có mã keo. Mô-đun này chỉ hoạt động với các API WebAssembly cơ bản.

Pháp y WebAssembly

7,6 KB của Rust lớn một cách đáng ngạc nhiên so với 2 ngôn ngữ còn lại. 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ữ tạo ra tệp) và cho bạn biết điều gì đang xảy ra cũng như giúp bạn cải thiện tình hình.

Twiggy

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 các mục như biểu đồ lệnh gọi của mô-đun, xác định các phần không sử dụng hoặc thừa và tìm hiểu xem 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 việc này bằng lệnh top của Twiggy:

$ twiggy top rotate_bg.wasm
Ảnh chụp màn hì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 của chúng ta bắt nguồn từ trình phân bổ. Điều đó thật đáng ngạc nhiên vì mã của chúng ta không sử dụng cơ chế phân bổ động. Một yếu tố đóng góp lớn khác là tiểu mục "tên hàm".

wasm-strip

wasm-strip là một công cụ trong Bộ công cụ nhị phân WebAssembly, viết tắt là wabt. Công cụ 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ú pháp giúp chuyển đổi 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 nhận thấy wasm-strip là công cụ 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

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

wasm-opt

wasm-opt là một công cụ của Binaryen. Công cụ này lấy 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ì không. Bạn nên thử tiết kiệm thêm một số byte 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 giảm một số byte khác để tổng cộng còn 6,2 KB sau khi 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 tiêu chuẩn của Rust, bằng cách sử dụng tính năng #![no_std]. Thao tác này cũng vô hiệu hoá hoàn toàn việc 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

đã tạo ra một mô-đun wasm 1,6 KB 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 ra, nhưng mô-đun này đủ nhỏ để được coi là 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 ta đã bắt đầu 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ả ra sao?

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 thông qua trình biên dịch để tạo mã máy dành riêng cho máy chủ lưu trữ. Tương tự như JavaScript, trình biên dịch hoạt động theo 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 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 chậm hơn.

Trường hợp sử dụng của chúng ta rất thú vị ở chỗ 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 của trình biên dịch tối ưu hoá. Bạn cần lưu ý điều này 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ẽ cho ra kết quả không thực tế. Để 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 độ 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. Trong biểu đồ đầu tiên, chúng ta so sánh theo trình duyệt, trong biểu đồ thứ hai, chúng ta so sánh theo ngôn ngữ được sử dụng. Xin lưu ý rằng tôi đã chọn một tiến trình thời gian logarit. Điều 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 kiểm thử 16 megapixel và cùng một máy chủ lưu trữ, ngoại trừ một trình duyệt không thể chạy trên cùng một máy.

Không cần phân tích quá nhiều về các biểu đồ này, rõ ràng là chúng ta đã giải quyết được vấn đề về hiệu suất ban đầu: Tất cả mô-đun WebAssembly 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 đã trình bày ở đầu bài viết: WebAssembly mang lại 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ự khác biệt giữa các trình duyệt và ngôn ngữ là rất nhỏ. Chính xác là: Độ lệch chuẩn của JavaScript trên tất cả trình duyệt là khoảng 400 mili giây, trong khi độ lệch chuẩn của tất cả mô-đun WebAssembly trên tất cả trình duyệt là khoảng 80 mili giây.

Nỗ lực

Một chỉ số khác là mức độ nỗ lực mà chúng tôi phải bỏ ra để tạo và tích hợp mô-đun WebAssembly vào squoosh. Rất khó để chỉ định một giá trị số cho nỗ lự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:

AssemblyScript không gây phiền hà. Không chỉ cho phép bạn sử dụng TypeScript để viết WebAssembly, giúp các đồ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 rất nhỏ với hiệu suất khá. Các công cụ trong hệ sinh thái TypeScript, như prettier và tslint, có thể sẽ hoạt động.

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

C và Emscripten đã tạo một mô-đun WebAssembly rất nhỏ và có hiệu suất cao ngay từ đầu, nhưng không có đủ can đảm để chuyển sang mã keo và giảm mã đó xuống mức tối thiểu, 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ỳ thuộc. Vậy chúng ta đã vận chuyển gì?

Biểu đồ so sánh

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

Tóm lại: AssemblyScript là một khám phá tuyệt vời. Nhờ đó, các nhà phát triển web có thể tạo 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 làm việc để cải thiện chuỗi công cụ của họ. Chúng tôi chắc chắn sẽ theo dõi AssemblyScript trong tương lai.

Thông tin 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 tuyệt vời của họ. Cuốn sách này có một phần về cách tối ưu hoá kích thước tệp. Việc 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ý sự cố khẩn cấp 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 bằng 370B sau khi gzip. Để biết thông tin chi tiết, vui lòng xem yêu cầu hỗ trợ mà tôi đã mở trên Squoosh.

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