Investigate WASM integrations viability
Context
There is a latent desire for us to be able to use WASM-compatible versions of some Rust crates in the Typescript SDK, which should help a lot with code reuse, eliminating the need to build everything from scratch on TS land. While this sounds like a dream to be pursued, we should take a step back to validate something important: how feasible it really is.
Some parallel efforts are being made to have more WASM-compatible Rust crates, so we must thoroughly validate the points raised here before mindlessly integrating them into the TS SDK.
PoC
In the past, we did a first experiment by integrating the WASM version of fuel-asm in the program package (example). Everything worked well and it's already in use, which was great because we didn't have to re-do it from scratch in Typescript. Fantastic!
Concerns
However, there's a known downside to WASM libraries, which is their final size, and one primary concern is how this can negatively impact the final size of the SDK bundle. Will it still be usable by Frontend applications that try to be as compact as possible?
Not only that, but to make things straightforward, we can't distribute .wasm files but instead embed them as Base64 strings so the DX won't suffer. Using Base64, however, increases their size even further, which may soon become a problem.
Action Points
This issue is a first step in understanding how much weight this single WASM lib is adding to our final build, considering, for example, a NextJS app. We need to compare two identical NextJS builds where the only difference would be that one would use the vm-asm lib and the other would not. From there, we can analyze the results and brainstorm again.
[!NOTE]
- The build that will not use
vm-asmwill not work properly, and this is expected. We might need to patch some things here and there to make the final app to compile, but it doesn't need to work for this comparison.- There's no need to think about tree-shaking here because
.wasmcan't be tree-shaken, and on top of that, remember that we are usingBase64encoded version of WASM.
Follow-up issues:
- https://github.com/FuelLabs/fuel-vm/issues/579
- https://github.com/FuelLabs/fuel-vm/issues/592
I did an investigation using our internal demo-react-vite app, because nextjs splits things up and it's not as straightforward to analyze the impact of the package as it is in a classic SPA one-file js bundle.
The first thing I did was remove the @fuels/vm-asm from the package.json dependencies so that we can see the actual effect of removing that package from fuels.
I then did a production build of demo-react-vite which resulted in 233.21 KB (minified+gzipped).
Afterwards, I removed @fuels/vm-asm usage from our wallet and program packages and made typescript happy with placeholder types. This resulted in demo-react-vite's size to be reduced to 212.27 KB.
So in a production build, the @fuels/vm-asm package weighs 21 KB when it's in the fuels package.
Interesting observations about the @fuels/vm-asm package size:
- bundlephobia reports it to be 25.9 KB, which is 5 KB bigger than when it's in
fuels. - I re-added
@fuels/vm-asmtodemo-react-viteafter removing it fromfuels, and the bundle size increased to 239.04 KB, which puts the package to be 26.77 KB, a tad bit bigger than what bundlephobia reports.
Good finds!
Have you compared the demo-react-vite with and without fuels for a complete check?
Pasting the link here for reference:
- https://bundlephobia.com/package/@fuels/[email protected]
I didn't know about bundle phobia, and now I'm scared:
- https://bundlephobia.com/package/[email protected]
It looks like the ethers addition in 0.63.0 added 200kb+ gzipped!
Can we take a deeper look at this to understand whether these numbers are accurate?
We could try variations with different usages simulating real-world case scenarios to see how tree-shakable things are.
One variation is the quickstart/scaffold PR that @Dhaiwat10 started:
- https://github.com/FuelLabs/fuels-ts/pull/1408
Another is the wallet itself. (cc @FuelLabs/frontend)
There might be others as well.
It looks like the ethers addition in 0.63.0 added 200kb+ gzipped!
I already investigated it in #1450. Should've cross-referenced here before closing it, though. Sorry about that.
Have you compared the demo-react-vite with and without fuels for a complete check?
I removed both @fuels/vm-asm and fuels and got a bundle size of 46.1 kB. I then readded fuels and got 233 kB. So the bundle size of fuels is 187 kB. Bundlephobia reports it to be 441 kB, so I don't know what they're missing.
I also saw them saying that graphql takes 13.8% of our bundle so I investigated, basing myself off of #1374. The graphql package itself seems quite tree-shakeable, and the culprit is actually graphql-request. After removing it, the bundle reduced about 15 kB in size or about 8% of the bundle. It wouldn't actually be difficult to remove this package by replacing its functionality with a fetch the same way I did for subscriptions in fuel-graphql-subscriber.ts.
I didn't know about bundle phobia, and now I'm scared:
lol
There is also likely some room for improvement on the fuel-asm side to cut the binary size down further (although it is already applying super-optimization level for code size). For example, I don't see lto being utilized in the fuel-vm repo and there are probably several other low-hanging areas for filesize golfing.
@Voxelot You read my mind. I'm still trying to figure out how exactly to proceed, tho.
We could list everything used from ASM, and someone from the Client team could try timing it down.
Makes sense?
@nedsalk Just registering what we spoke during the last sync:
To conclude the first stage of this evaluation, it would be good to understand two things:
- How much KBs is saved if we cherry-pick only what's needed?
- How much KBs is saved by exporting a single WASM with multiple packages combined? i.e.
fuel-{tx,asm,crypto}?- The gain here would be due to the deduplication of possible shared code between the packages
- I'm not sure if there are any low-hanging fruits anymore
- Combining the current exports seems to give 227 KiB. This includes
fuel-asmandfuel-tx, which also export relatedfuel-cryptofunctionality.
Thanks for the details @Dentosal.
I'm afraid 200kb+ might be a no-go from a bundle size perspective.
Not sure if there's anything else we could do to make it smaller.
cc @Voxelot @xgreenx
I want to highlight that we still plan to dry run transactions on the user side in the future, and it requires the whole fuel-vm that will be significantly more=)
Is it possible to cache the wasm binary on the user side in the local storage and only download it once?
@xgreenx Great point!
Client cache is under the user's control since their app will be hosted by themselves, and the browser should cache things automatically following whatever configurations they have on their server.
The main problem is the first app load, which is why many Frontend apps perform poorly, resulting in user drops. This is a common pain point for Frontend apps and why smaller bundle sizes are pursued.
One option to evaluate is to provide another separate server-only package that users can optionally put under an API endpoint on their server that they can use to dry-run things. I have not considered this entirely, but it could be an intermediary solution. Would you have any thoughts on this?
@luizstacio @digorithm Please feel free to chime in here.
Another possibility(?) is to use dynamic imports. I don't currently know the full implications of such an approach, but doing something like this:
class Provider {
static async create(...) {
globalThis.fuelWasm ??= await import("wasm-package")
}
}
apparently leads to bundlers lazy-loading the dynamic import. This would probably be in conflict with #1397, though.
Another solution might be to put the wasm package on a CDN:
class Provider {
static async create(...) {
globalThis.fuelWasm ??= await fetchFuelWasmFromCdnOrCache();
}
}
We could lazy load the wasm and read it later from the browser cache.
These are just rough proposals that could be further investigated if there's interest in it.
@nedsalk The first approach is similar to what I suggested, but without the server involved, and should also work.
The second one, however, could be disastrous.
It seems that integrating WASM packages is a no-go for now as the effect on bundle size is prohibitive. I'm closing this issue as the investigation is finished.