WebAssembly + Rust trong React App: Tăng Tốc Compute-Heavy Tasks Thực Tế

7 tháng trước · 9 phút đọc
WebAssembly đang trỗi dậy - và đây là lý do bạn cần quan tâm
Mình nhớ lần đầu thử xử lý ảnh trực tiếp trên browser bằng JavaScript thuần. Chụp ảnh căn cước công dân, crop, xoay, nén rồi upload. Kết quả? Browser đơ 3-4 giây, UI freeze cứng đờ, người dùng tưởng app lỗi. Vấn đề không phải code sai - JS đơn giản là không đủ nhanh cho compute-heavy tasks kiểu đó.
WebAssembly (WASM) sinh ra để giải quyết đúng bài toán này. Không phải thay thế JavaScript - mà làm đồng đội với nó. Bạn viết phần tính toán nặng bằng Rust, compile sang WASM, rồi gọi từ JS như gọi một hàm bình thường.
WASM không thay JS, nó làm cái JS không giỏi
Trend này đang rise mạnh trong cộng đồng dev toàn cầu. Hàng nghìn dự án production đang dùng WebAssembly cho các tác vụ xử lý nặng như video encoding, PDF rendering, game engine chạy trên web. Với dev Việt, cơ hội rõ nhất nằm ở các app fintech, eKYC, và công cụ xử lý tài liệu - những bài toán image processing cực kỳ phổ biến.
Setup môi trường: Rust + wasm-pack + React
Trước khi benchmark hay viết code xử lý ảnh, cần setup đúng tool chain. Mình sẽ đi thẳng vào từng bước.
Bước 1: Cài Rust và wasm-pack
Bước 2: Tạo Rust WASM package
Sửa Cargo.toml:
Bước 3: Viết function Rust đầu tiên
Bước 4: Build và tích hợp vào React
Tool chain gọn: Rust code → wasm-pack build → JS module sẵn dùng
Sau khi build xong, bạn sẽ có folder wasm/ trong React app với 3 file chính: file .wasm (binary), file .js (glue code), và file .d.ts (TypeScript types). Không cần config webpack phức tạp - Vite và Create React App đều hỗ trợ import WASM trực tiếp.
Dùng WASM trong React: Xử lý ảnh thực tế
Mình chọn bài toán xử lý ảnh căn cước công dân - use case cực kỳ phổ biến trong fintech và eKYC Việt Nam. Người dùng chụp CCCD, app cần: chuyển grayscale, tăng contrast để OCR dễ hơn, rồi nén xuống trước khi upload.
Đây là cách gọi WASM module từ React:
Component sử dụng hook này:
Upload ảnh → WASM xử lý → kết quả hiển thị ngay, không freeze UI
Một lưu ý quan trọng: processImage ở trên vẫn chạy trên main thread. Với ảnh lớn (>5MB), bạn nên wrap nó vào Web Worker để UI hoàn toàn không bị block. Mình sẽ đề cập phần này ở section benchmark.
Benchmark thực tế: WASM vs JavaScript thuần
Số liệu đẹp nhất là số liệu bạn tự đo. Mình benchmark 3 tác vụ image processing trên ảnh CCCD thực tế (kích thước 2400×1600px, ~11.5MB pixel data):
| Tác vụ | JS thuần | WASM (Rust) | Chênh lệch |
|---|---|---|---|
| Grayscale | ~180ms | ~18ms | 10x nhanh |
| Tăng contrast | ~210ms | ~22ms | 9.5x nhanh |
| Box blur 5×5 | ~850ms | ~65ms | 13x nhanh |
| Toàn bộ pipeline | ~1240ms | ~105ms | ~12x nhanh |
Khác biệt đến từ đâu? JavaScript là dynamically typed và phải qua JIT compiler. WASM chạy bytecode đã được compile sẵn, gần với native code hơn. Với pixel loop chạy hàng triệu lần (2400×1600 = 3.84 triệu pixels), overhead nhỏ nhân lên cực lớn.
13x nhanh hơn ở blur - đây là loại tác vụ WASM sinh ra để làm
Để tự benchmark, bạn dùng performance.now():
Một điểm mình muốn thành thật: WASM không phải lúc nào cũng nhanh hơn 10x. Với tác vụ đơn giản như string concatenation hay object sorting nhỏ, overhead của việc truyền data qua JS-WASM boundary có thể khiến WASM chậm hơn JS. Rule of thumb: dùng WASM khi xử lý array/buffer data lớn với nhiều phép tính lặp đi lặp lại.
Web Worker + WASM: UI không bao giờ freeze
Benchmark trên chạy trên main thread. Với ảnh 12MP từ camera điện thoại Việt Nam thường dùng, ngay cả 105ms WASM cũng đủ để người dùng thấy lag. Giải pháp: đẩy WASM vào Web Worker.
Tạo file imageWorker.ts:
Dùng trong React hook:
Worker + WASM: UI thread hoàn toàn tự do trong khi xử lý ảnh
Kỹ thuật transferable objects ([pixels.buffer]) là chi tiết quan trọng. Thay vì copy toàn bộ buffer sang worker (tốn thêm 11.5MB memory và thời gian copy), bạn transfer ownership - gần như zero-cost. Sau khi transfer, pixels.buffer ở main thread sẽ bị detach và không dùng được nữa.
Khi nào NÊN và KHÔNG NÊN dùng WASM
Mình thấy nhiều bài viết chỉ nói về ưu điểm của WASM mà quên trade-off. Thực tế không đẹp như benchmark.
Dùng WASM khi:
- Xử lý pixel/audio/video data ở dạng raw buffer
- Thuật toán lặp nhiều triệu lần (matrix multiplication, FFT)
- Port thư viện C/C++ có sẵn (OpenCV, FFmpeg) sang web
- Game engine hoặc physics simulation
Không nên dùng WASM khi:
- Logic CRUD đơn giản (đọc/ghi data, validate form)
- String processing thông thường
- DOM manipulation (WASM không access DOM trực tiếp)
- Tác vụ chạy < 5ms - JS đã đủ nhanh, overhead WASM không đáng
Chọn đúng tool: WASM không phải silver bullet
Còn một thực tế khác: bundle size. File .wasm cho image processing của mình là ~85KB sau optimization. Với user Việt Nam dùng mạng 4G, đây là cost cần tính đến. Dùng lazy loading (dynamic import()) để chỉ load WASM khi người dùng thật sự cần - như mình đã làm ở hook useImageProcessor trên.
Cuối cùng, Rust learning curve là thật. Nếu team bạn không có ai biết Rust, có thể dùng AssemblyScript (TypeScript-like syntax compile sang WASM) hoặc Emscripten để port thư viện C. Performance thấp hơn Rust một chút nhưng onboarding dễ hơn nhiều. Tài nguyên để bắt đầu: AssemblyScript docs.
Giờ bạn đã biết đủ để bắt đầu. Bước tiếp:
- Clone template project Rust + Vite + React: Rust + Vite + React WASM starter
- Đo performance điểm đau thực tế trong app của bạn trước
- Chỉ port sang WASM nếu tác vụ đó chiếm > 100ms
Gặp vấn đề gì khi setup, drop comment - mình sẽ support.
