Minimizing Go WebAssembly Binary Size

Estimated reading time: 4 Minutes

In both of the previous posts I’ve described different aspects of WebAssembly. The first of the two explained what WebAssembly is and how to run a simple Go web application, with WebAssembly, in the browser. The second post was a Go WebAssembly performance benchmark. If you are unfamiliar with Go WebAssembly, I’d recommend reading the first post before diving into this one. In this post I’m going to discuss the Go WebAssembly binary size.

While working with Go WebAssembly I’ve noted that the .wasm binary files are quite big. Not huge for my simple tests, but several MBs for a just a few functions. That made me ask myself – is that the best we can get assuming we want these files to be served over the web? In this blog post I’ll show you how to minimize your Go WebAssembly binary size so it will be much more web friendly.

Current State

Going back to my encoding example from the introductory post, you can see that the encode.wasm Go WebAssembly binary file size is roughly 2MB:

Current Go WebAssembly binary size

Currently the encoding function code imports only two packages:

import (
	"encoding/base64"
	"syscall/js"
)

If I was to import another package, let’s say the fmt package, how do you think that will affect the .wasm binary file size? let’s try it out:

Go WebAssembly binary size with when adding the fmt package

That’s a lot. Importing a simple and basic package such as fmt costs us roughly 0.4MB. A takeaway from this is that you need to keep in mind, that packages imports leads to a larger binary file.

For the rest of this post, I’ll remove the fmt package and work with the original code.

Test #1 – Serving Gzipped Content

For my first try to reduce the .wasm file size, I ran my server with a gzip handler. This means that every file file being served to the browser is first compressed using gzip on the server side, and extracted by the browser. You can read more about the method of serving compressed files here.

For the purpose of this test I used the gziphandler package in order to create a handler that compresses server responses:

package main

import (
	"github.com/NYTimes/gziphandler"
	"log"
	"net/http"
)

func main() {
	err := http.ListenAndServe(":8080",
		gziphandler.GzipHandler(http.FileServer(http.Dir("/path/to/served/files"))))
	if err != http.ErrServerClosed {
		log.Fatal(err)
	}
}

Now let’s see how much do we benefit by doing so:

Go WebAssembly binary size with when gzipped

Well this is not bad at all. Gzipping the content reduced the file size by about 75% from 2MB to 0.5MB. You can see that the time it takes to serve the file increased dramatically, that is because the gzip extraction process is more complex than simply serving static files.

Can we do it better?

Test #2 – TinyGo

From the official homepage:

TinyGo is a project to bring the Go programming language to microcontrollers and modern web browsers by creating a new compiler based on LLVM.

[…] It can also be used to produce WebAssembly (WASM) code which is very compact in size.

TinyGo hompage – https://tinygo.org/

In other words – TinyGo is a Go compiler, focused on creating small binaries. Since TinyGo does not aim to compile any possible Go program, it lacks some language and packages support. In other words, this means that you might need to modify your code in order for it to be compiled by TinyGo.

Luckily for me, both of the packages my code imports are fully supported by TinyGo:

Now that we understand it’s purpose, let’s give it a try.

I chose to install TinyGo on my machine, you can also use a Docker image if you don’t want to install it locally. The official documentation on using TinyGo for WebAssembly is available here.

Compiling Using TinyGo

Now I needed to compile my main.go file to encode.wasm using the TinyGo compiler. This will replace the older 2MB file:

$ tinygo build -o encode.wasm -target wasm main.go

After creating the binary file, I then tried refreshing my browser session, only to get the following weird error:

Uncaught (in promise) TypeError: WebAssembly.instantiate(): Import #0 module="wasi_unstable" error: module is not an object or function

Well, turns out that the JavaScript glue code (which we talked about) used by the TinyGo compiler is a different one. So I had to replace it:

$ cp $(tinygo env TINYGOROOT)/targets/wasm_exec.js .

Now refreshing worked, and so did my basic application!

Let’s take a look a the file size: **drums**

Go WebAssembly binary size with when compiled with TinyGo

Amazing! We got our .wasm binary file down from 2MB to 86KB.

Key Takeaways

  • Go WebAssembly is still in early stages but as you can see it’s quite ready to handle various use cases.
  • Decreasing the binary file size can be a bit tricky for larger applications but is feasible.
  • I would definitely recommend experimenting with this interesting technology which, in my opinion, we’ll see more and more of in the future as it matures.

As always, feel free to comment, like and share. See you on the next one!

Leave a Reply