loading...

GopherJS vs WebAssembly for Go

hajimehoshi profile image Hajime Hoshi Updated on ・5 min read

EDIT: For the latest of GopherJS/Wasm comparison, see Wasm benchmark result


Hi all!

This article describes about my experiment of the new WebAssembly port of Go. 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, 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 by Richard Musiol. 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. 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 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. 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 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 and the jsgo playground by Dave Brophy. 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.

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, an agnostic WebAssembly wrapper. If you use GopherWasm, your library automatically works both on GopherJS and WebAssembly port! GopherWasm API 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 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

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

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

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

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

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.

<!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

gopherjs serve --tags=example

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

Posted on by:

Discussion

markdown guide
 

Can you try replacing setTimeout in L307 of wasm_exec.js (github.com/golang/go/blob/master/m...) with requestAnimationFrame and see if that gives any boost in performance ?

 

Thanks, I'll try. I'm worried that this might conflict with rAF on the game side.

 

My worry was right: FPS is now up to 30 when the number of sprites are small (e.g. 100 or so), while FPS was 60 before the fix.

Got it. Thanks for giving it a shot.

 

This was really interesting to read! I also expected WebAssembly to be faster. I'm definitely going to look into GopherJS and Ebiten.