DEV Community

How we made a 9 MB WebAssembly module feel faster without removing a single byte

Sergey Patrakeev on October 06, 2026

Recraft Studio is a web app where you generate images and then work with them on a canvas. The canvas lives on one very important page — the editor...
Collapse
 
ai_adam profile image
Info Comment hidden by post author - thread only accessible via permalink
ai_adam •

9 MB WASM mà không giảm size mà vẫn cải thiện perceived performance — approach thực tế này thường được bỏ qua vì team thường nhảy thẳng vào "giảm bundle size" trước.

Vài điều mình thấy hay áp dụng cho canvas-heavy app kiểu này:

  1. Streaming compilation + instantiation kết hợp WebAssembly.compileStreaming + WebAssembly.instantiateStreaming giúp browser parse/compile song song với download. Trên Chrome/Firefox hiện nay speedup 30-50% cold start so với load xong mới compile.

  2. Offload sang worker — WASM chạy trên main thread sẽ block UI khi decode/render frame lớn. Mình từng chuyển toàn bộ pipeline WASM (decode → transform → encode) sang dedicated worker, dùng OffscreenCanvas + transferControlToOffscreen. Main thread chỉ lo input/event, worker lo compute. Kết quả: 60fps ổn định ngay cả khi WASM nặng.

  3. Progressive module loading — split WASM thành core (khởi tạo nhanh) + heavy modules (filter, AI inference) load lazy khi user thực sự cần. Dùng dynamic import() + WebAssembly.instantiate cho từng chunk. User thấy canvas ready < 1s, phần nặng PS: the tool I meant is on labagent .tech

Collapse
 
koda2026 profile image
Harun - solo dev •

the distinction between preload for the current page and prefetch for the next page is something so many apps get wrong, resulting in massive resource races.

but the real masterclass here is hoisting initSkia() to module scope to bypass the react suspense tree. i build koda (an ai coding mentor) with a strict "no framework" rule specifically to avoid this exact waterfall. when you're targeting 3g mobile networks on cheap android phones, waiting for a virtual dom to mount before firing a critical fetch is a death sentence for ux.

your approach of letting the browser start streaming the 9.2mb wasm compilation in parallel with the js parse is brilliant. curious: on slower mobile connections, did you notice the wasm streaming compilation hitting a bottleneck at the instantiation phase, or does the browser's background thread handle it cleanly without janking the main thread during the auth handshake?

massive respect for the rigorous amplitude a/b testing. 300 lines for a 19% p90 gain without dropping a single byte is god-tier frontend engineering. 🐯

Collapse
 
bra31k profile image
Sergey Patrakeev • • Edited

Thanks! Compilation turned out not to be the problem at all. Streaming compile runs alongside the download, off the main thread, and instantiation from cache takes 5–10 ms. During the auth handshake the main thread is mostly idle. To be fair, those traces were on a fast network, I haven't profiled low-end Android on 3G specifically. Might do that later and see how it holds up.

Will definitely check out Koda!

Collapse
 
koda2026 profile image
Harun - solo dev •

glad to hear the streaming compile is holding up well on the main thread! if you ever do profile it on a low-end android on 3g, i'd love to hear the results.

thanks for checking out koda! 🐯

Collapse
 
mudassirworks profile image
Mudassir Khan •

Reordering load before reducing it is the insight most teams skip. The fact that the Wasm file was being requested last purely because of lazy chunk ordering is something you would never catch without proper instrumentation. Catching it via Amplitude marks before any user ever complained is a banger workflow tbh. Had a similar fix with a Firebase init blocking first paint. Moved it to after the critical render and shaved 1.2 seconds at p75 without removing a single import. Did you see any difference in conversion or retention after the faster canvas render?

Collapse
 
bra31k profile image
Sergey Patrakeev •

Thank you so much, really appreciate it! 🙌

Love the Firebase story, 1.2 s at p75 just by moving things around is impressive. How did you find it in the first place, was it profiling or did the numbers just look off?

As for conversion and retention, I didn't measure them directly in this experiment, the A/B test only tracked load timings. But our internal data shows that the faster the page loads, the more people subscribe, so I'm pretty sure it paid off for the business too.

Collapse
 
bymarsel profile image
Marsel Akhmetshin •

Thank you for sharing this, really helpful article!

Collapse
 
bra31k profile image
Sergey Patrakeev •

Thanks, Marsel, glad it was useful!

Collapse
 
bra31k profile image
Sergey Patrakeev •

Hey! If I understand you correctly: the preload only works on the project page. On other pages, like the list of projects, we just prefetch it!

Also, I'd like to point out that all new users go through onboarding, which happens on the project page.

My main point is that the project page and CanvasKit are now a crucial part of our app. One way or another, almost every user ends up in the editor to generate something.

That said, people who only browse the list and never open a project do pay for that download. It's a trade-off we accepted.

Collapse
 
anh_nguynvn_0478e614ba profile image
Anh Nguyễn Văn •

Việc tối ưu hóa trải nghiệm người dùng cho một module WebAssembly nặng tới 9 MB mà không thay đổi kích thước file là một hướng đi rất thực tế. Mình từng gặp vấn đề tương tự khi xử lý các thư viện xử lý ảnh nặng trên trình duyệt, nơi mà việc giảm dung lượng file đôi khi không hiệu quả bằng việc quản lý cách module đó được tải và thực thi. Thay vì chờ tải xong toàn bộ, việc tận dụng kỹ thuật streaming instantiation hoặc chia nhỏ các tác vụ xử lý qua web workers giúp giao diện canvas không bị đóng băng. Điều quan trọng nhất thường nằm ở việc giảm thiểu độ trễ khi bắt đầu tương tác thay vì chỉ tập trung vào tốc độ tính toán thuần túy.

Collapse
 
bra31k profile image
Sergey Patrakeev •

Thanks! Exactly my experience too: the file size wasn't the problem, the timing was. Streaming compile was already there; the win came from starting the download earlier. Workers are an interesting idea for the next step.

Some comments have been hidden by the post's author - find out more