GopherJS vs WebAssembly for Go
Tags: go, webassembly
Published: 2018-06-15 19:49:51

---
title: GopherJS vs WebAssembly for Go
published: true
description: This article describes about my experiment of the new WebAssembly port of Go.
tags: go, webassembly, wasm
---

**EDIT**: For the latest of GopherJS/Wasm comparison, see [Wasm benchmark result](https://github.com/hajimehoshi/wasm-bench)

----

Hi all!

This article describes about my experiment of [the new WebAssembly port of Go](https://github.com/golang/go/issues/18892). WebAssembly port is now available on the master branch of Go, and you'd need to compile Go yourself.

# tl;dr

* I have created [GopherWasm](https://github.com/hajimehoshi/gopherwasm), an agnostic WebAssembly wrapper that works both on GopherJS and WebAssembly port.
* Performance of GopherJS and WebAssembly depends on browsers. GopherJS is faster than WebAssembly on some environments, and slower on other environments. For Ebiten 'sprite' example, `(GopherJS on Chrome) > (WebAssembly on Firefox) > (GopherJS on Firefox) > (WebAssembly on Chrome)` with 5000 sprites.

# Go on browsers

Running Go applications on web browsers must be awesome. Needless to say, Go is an awesome language. I don't discuss how good Go is in this article :-)

There is a transpiler from Go to JavaScript - [GopherJS](https://github.com/gopherjs/gopherjs/) by [Richard Musiol](https://github.com/neelance). This also enables Go programs to run both on browsers and Node.js. You can use all the features of Go. The compilation result is reasonably readable JavaScript. The performance is so-so due to some overhead. To emulate Go behaviors precisely, GopherJS adds some overhead like boundary check of index access to slices. Instead of emulating Go behavior by JavaScript, executing binaries like WebAssembly on browsers seems much more efficient.

WebAssembly is a performance-wise format compared to JavaScript. WebAssembly is supported by [most of modern browsers](https://caniuse.com/#feat=wasm). WebAssembly is a low-level language as the name says, and it is expected that WebAssembly binary is generated from other languages. Actually C, C++ and Rust already support WebAssembly port.

[The latest Go version 1.11](https://tip.golang.org/doc/go1.11) supports WebAssembly port by Richard Musiol, the same author of GopherJS. Now Go 1.11 is on the way releasing, but you can test WebAssembly APIs with the latest Go by compiling yourself. Your compiled program for WebAssembly is available both on browsers and Node.js. You can use full features of Go including goroutines. You can call any JavaScript functions from Go, and you can pass Go function as a JavaScript callback. The API is defined at [`syscall/js` package](https://tip.golang.org/pkg/syscall/js/?GOOS=js&GOARCH=wasm). The environment variables for WebAssembly are `GOOS=js` and `GOARCH=wasm`. As WebAssembly is performance-wise format, this should be faster than GopherJS, right? Unfortunately, this was not true. I'll describe this later.

# Ebiten

[Ebiten](https://hajimehoshi.github.io/ebiten/) is a dead simple 2D game library by me. This is basically an OpenGL wrapper. This works on browsers with WebGL by GopherJS, and actually you can see some examples work on [the website](https://hajimehoshi.github.io/ebiten/) and [the jsgo playground](https://play.jsgo.io/github.com/hajimehoshi/ebiten/examples/flappy) by [Dave Brophy](https://github.com/dave). Recently (actually today!) I fixed Ebiten (master branch) to accept WebAssembly compilation of the latest Go compiler except for the audio part. Thus, Ebiten now works both on GopherJS and WebAssembly!

# Port GopherJS library to WebAssembly

As I said, Ebiten can already work with GopherJS. GopherJS's API is similar to WebAssembly, but different. For example, the counterpart of `js.Object` of GopherJS is `js.Value` of `syscall/js`. 

* [GopherJS API](https://godoc.org/github.com/gopherjs/gopherjs/js)
* [`syscall/js` API](https://tip.golang.org/pkg/syscall/js/?GOOS=js&GOARCH=wasm)

Then, how can I write libraries to accept both GopherJS and WebAssembly? Of course it is easily possible to write similar duplicated code, but isn't there a more elegant way?

I've created [GopherWasm](https://github.com/hajimehoshi/gopherwasm), an agnostic WebAssembly wrapper. If you use GopherWasm, your library automatically works both on GopherJS and WebAssembly port! [GopherWasm API](https://godoc.org/github.com/hajimehoshi/gopherwasm/js) is almost same as `syscall/js`. The only one difference is `js.ValueOf` accepts `[]float32` or other slices in GopherWasm, not in `syscall/js`. I have already [filed](https://github.com/golang/go/issues/25532) to fix `syscall/js.ValueOf` to accept such slices, so the situation might change in near future.

# Performance comparison

I've compared the performances between GopherJS and WebAssembly port with my Ebiten example 'sprites'.

- [Sprites (GopherJS)](https://jsgo.io/github.com/hajimehoshi/ebiten/examples/sprites)
- [Sprites (WebAssembly)](https://storage.googleapis.com/ebiten-test/index.html)

![Sprites](https://thepracticaldev.s3.amazonaws.com/i/7fdraepf6f6k9cn3dxxe.png)

By pressing left or right arrow keys, you can change the number of sprites and see how FPS (frames per second) changes.

On my MacBook Pro 2014, I took very rough measurements by showing 5000 sprites:

```
GopherJS on Chrome:     55-60 FPS
GopherJS on Firefox:    20-25 FPS
WebAssembly on Chrome:  15-20 FPS
WebAssembly on Firefox: 40-45 FPS
```

* Chrome: Version 67.0.3396.87 (Official Build) (64-bit)
* Firefox: 60.0.2 (64-bit)
* Ebiten: 460c47a9ebaa21bcce730a460a7f87fa6cbe56ed
* Go: 534ddf741f6a5fc38fb0bb3e3547d3231c51a7be

This is a very interesting result. Before this experiment, I thought WebAssembly should always be faster than GopherJS. However, the result depended on browsers. For 5000 sprites, the result was `(GopherJS on Chrome) > (WebAssembly on Firefox) > (GopherJS on Firefox) > (WebAssembly on Chrome)`. I guess optimization way is different among browsers.

I took rough profile and it looks like allocation (`runtime.mallocgc`) was the heaviest task on WebAssembly. This is different tendency from GopherJS. I'm not sure the details how objects are allocated on WebAssembly, but at least WebAssembly requires different optimization from GopherJS.

![Profiling](https://thepracticaldev.s3.amazonaws.com/i/0gcl7n9t51xpv5kwupm4.png)

I plan to do optimization to keep 60 FPS as much as possible. Stay tuned!

# Binary size comparison

```
-rw-r--r--    1 hajimehoshi  staff  7310436 Jun 16 05:23 sprites.js
-rw-r--r--    1 hajimehoshi  staff   278394 Jun 16 05:23 sprites.js.map
-rwxr-xr-x    1 hajimehoshi  staff  8303883 Jun 16 04:03 sprites.wasm
```

It looks like WebAssembly binary is slightly bigger.

# Appendix - How to do experiments

## Install Ebiten and other libraries

```sh
go get -u github.com/hajimehoshi/ebiten/...
go get -u github.com/hajimehoshi/gopherwasm
go get -u github.com/gopherjs/gopherjs
```

## Get the latest Go and compile it

```sh
cd
git clone https://go.googlesource.com/go go-code
cd go-code/src

# Compile Go. ./all.bash is also fine if you want to run tests.
./make.bash
```

## Compile an Ebiten example for WebAssembly

```sh
cd /path/to/your/wasm/project

# Compile 'sprites' example for WebAssembly
GOOS=js GOARCH=wasm ~/go-code/bin/go build -tags=example -o sprites.wasm github.com/hajimehoshi/ebiten/examples/sprites

# Copy wasm_exec.js
cp ~/go-code/misc/wasm/wasm_exec.js .
```

Prepare an HTML file to run the wasm file. This file is based on `~/go-code/misc/wasm/index.html`.

```html
<!DOCTYPE html>
<script src="wasm_exec.js"></script>
<script>
// Polyfill
if (!WebAssembly.instantiateStreaming) {
  WebAssembly.instantiateStreaming = async (resp, importObject) => {
    const source = await (await resp).arrayBuffer();
    return await WebAssembly.instantiate(source, importObject);
  };
}

const go = new Go();
WebAssembly.instantiateStreaming(fetch("sprites.wasm"), go.importObject).then(result => {
  go.run(result.instance);
});
</script>
```

Run an HTTP server as you like.

## Run GopherJS server

```sh
gopherjs serve --tags=example
```

Then access `http://localhost:8080/github.com/hajimehoshi/ebiten/examples/sprites/` to see the example.