Description
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:
Source Code
main.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
// +build wasm
package main
import "testing"
const WasmOrGJS = "WebAssembly"
func BenchmarkGoGopherJS(b *testing.B) {}
gopherjs.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
}
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?