fuels-ts icon indicating copy to clipboard operation
fuels-ts copied to clipboard

Investigate WASM integrations viability

Open arboleya opened this issue 2 years ago • 15 comments

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]

  1. The build that will not use vm-asm will 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.
  2. There's no need to think about tree-shaking here because .wasm can't be tree-shaken, and on top of that, remember that we are using Base64 encoded version of WASM.

arboleya avatar Nov 23 '23 19:11 arboleya

Follow-up issues:

  • https://github.com/FuelLabs/fuel-vm/issues/579
  • https://github.com/FuelLabs/fuel-vm/issues/592

arboleya avatar Nov 23 '23 19:11 arboleya

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-asm to demo-react-vite after removing it from fuels, 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.

nedsalk avatar Nov 24 '23 12:11 nedsalk

Good finds!

Have you compared the demo-react-vite with and without fuels for a complete check?

Pasting the link here for reference:

I didn't know about bundle phobia, and now I'm scared:

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?

arboleya avatar Nov 24 '23 18:11 arboleya

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.

arboleya avatar Nov 24 '23 18:11 arboleya

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.

nedsalk avatar Nov 24 '23 18:11 nedsalk

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

nedsalk avatar Nov 24 '23 19:11 nedsalk

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 avatar Nov 25 '23 04:11 Voxelot

@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?

arboleya avatar Nov 25 '23 11:11 arboleya

@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:

  1. How much KBs is saved if we cherry-pick only what's needed?
  2. 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

arboleya avatar Dec 19 '23 14:12 arboleya

  1. I'm not sure if there are any low-hanging fruits anymore
  2. Combining the current exports seems to give 227 KiB. This includes fuel-asm and fuel-tx, which also export related fuel-crypto functionality.

Dentosal avatar Jan 04 '24 14:01 Dentosal

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

arboleya avatar Jan 09 '24 15:01 arboleya

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 avatar Jan 10 '24 13:01 xgreenx

@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.

arboleya avatar Jan 10 '24 15:01 arboleya

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 avatar Jan 15 '24 10:01 nedsalk

@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.

arboleya avatar Jan 15 '24 15:01 arboleya

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.

nedsalk avatar Feb 22 '24 09:02 nedsalk