Repository: go
Issue: syscall/js: performance considerations
Labels: Performance,NeedsInvestigation,arch-wasm

I was porting some frontend Go code to be compiled to WebAssembly instead of GopherJS, and noticed the performance was noticeably reduced. The Go code in question makes a lot of DOM manipulation calls and queries, so I decided to benchmark the performance of making calls from WebAssembly to the JavaScript APIs via `syscall/js`.

I found it's approximately 10x slower than native JavaScript.

Results of running a benchmark in Chrome 75.0.3770.80 on macOS 10.14.5:

```
  131.212518 ms/op - WebAssembly via syscall/js
   61.850000 ms/op - GopherJS via syscall/js
   12.040000 ms/op - GopherJS via github.com/gopherjs/gopherjs/js
   11.320000 ms/op - native JavaScript
```

Here's the benchmark code I used, written to be self-contained:

<details><summary>Source Code</summary>

#### main.go

```Go
package main

import (
	"fmt"
	"runtime"
	"syscall/js"
	"testing"
	"time"

	"honnef.co/go/js/dom/v2"
)

var document = dom.GetWindow().Document().(dom.HTMLDocument)

func main() {
	loaded := make(chan struct{})
	switch readyState := document.ReadyState(); readyState {
	case "loading":
		document.AddEventListener("DOMContentLoaded", false, func(dom.Event) { close(loaded) })
	case "interactive", "complete":
		close(loaded)
	default:
		panic(fmt.Errorf("internal error: unexpected document.ReadyState value: %v", readyState))
	}
	<-loaded

	for i := 0; i < 10000; i++ {
		div := document.CreateElement("div")
		div.SetInnerHTML(fmt.Sprintf("foo <strong>bar</strong> baz %d", i))
		document.Body().AppendChild(div)
	}

	time.Sleep(time.Second)

	runBench(BenchmarkGoSyscallJS, WasmOrGJS+" via syscall/js")
	if runtime.GOARCH == "js" { // GopherJS-only benchmark.
		runBench(BenchmarkGoGopherJS, "GopherJS via github.com/gopherjs/gopherjs/js")
	}
	runBench(BenchmarkNativeJavaScript, "native JavaScript")

	document.Body().Style().SetProperty("background-color", "lightgreen", "")
}

func runBench(f func(*testing.B), desc string) {
	r := testing.Benchmark(f)
	msPerOp := float64(r.T) * 1e-6 / float64(r.N)
	fmt.Printf("%f ms/op - %s\n", msPerOp, desc)
}

func BenchmarkGoSyscallJS(b *testing.B) {
	var total float64
	for i := 0; i < b.N; i++ {
		total = 0
		divs := js.Global().Get("document").Call("getElementsByTagName", "div")
		for j := 0; j < divs.Length(); j++ {
			total += divs.Index(j).Call("getBoundingClientRect").Get("top").Float()
		}
	}
	_ = total
}

func BenchmarkNativeJavaScript(b *testing.B) {
	js.Global().Set("NativeJavaScript", js.Global().Call("eval", nativeJavaScript))
	b.ResetTimer()
	js.Global().Get("NativeJavaScript").Invoke(b.N)
}

const nativeJavaScript = `(function(N) {
	var i, j, total;
	for (i = 0; i < N; i++) {
		total = 0;
		var divs = document.getElementsByTagName("div");
		for (j = 0; j < divs.length; j++) {
			total += divs[j].getBoundingClientRect().top;
		}
	}
	var _ = total;
})`
```

#### wasm.go

```Go
// +build wasm

package main

import "testing"

const WasmOrGJS = "WebAssembly"

func BenchmarkGoGopherJS(b *testing.B) {}
```

#### gopherjs.go

```Go
// +build !wasm

package main

import (
	"testing"

	"github.com/gopherjs/gopherjs/js"
)

const WasmOrGJS = "GopherJS"

func BenchmarkGoGopherJS(b *testing.B) {
	var total float64
	for i := 0; i < b.N; i++ {
		total = 0
		divs := js.Global.Get("document").Call("getElementsByTagName", "div")
		for j := 0; j < divs.Length(); j++ {
			total += divs.Index(j).Call("getBoundingClientRect").Get("top").Float()
		}
	}
	_ = total
}
```

</details>

I know `syscall/js` is documented as "Its current scope is only to allow tests to run, but not yet to provide a comprehensive API for users", but I wanted to open this issue to discuss the future. Performance is important for Go applications that need to make a lot of calls into the JavaScript world.

What is the current state of `syscall/js` performance, and are there known opportunities to improve it?

/cc @neelance @cherrymui @hajimehoshi