Go version

go version go1.24.0 darwin/arm64

Output of go env in your module/workspace:

AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/kyon/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/kyon/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/nm/73wzznv13ld2vcfxd12d24k40000gn/T/go-build1372434015=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/Users/kyon/Code/Tiledmedia/core/go.mod'
GOMODCACHE='/Users/kyon/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/kyon/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/kyon/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.0.darwin-arm64'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/kyon/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/kyon/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.0.darwin-arm64/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.24.0'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

GOOS=js GOARCH=wasm go build -o main.wasm main.go && goexec 'http.ListenAndServe(\`:8080\`, http.FileServer(http.Dir(`.`)))'

A minimal repro is provided here: https://github.com/kyoncal/go-wasm-memory-leak

What did you see happen?

Running the sample program using the command provided leads to a memory leak in both Chrome and Firefox. The value of the pointer passed to the wasm function goFunction is never cleared. In the sample program provided, the memory usage will grow forever.

Image

What did you expect to see?

I expect to see the memory get cleared eventually, however it grows forever. There are work arounds that allow the memory to get garbage collected, such as wrapping the pointer in a JS object, and setting the field to undefined afterwards, however, this is certainly not intended behavior.

Comment From: gabyhelp

Related Issues

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: dmitshur

CC @golang/wasm.

Comment From: Zxilly

The Go object in wasm_exec.js retains all JavaScript values referenced by WASM internally. These values are only released after the Go garbage collector determines they are no longer needed. Since WASM is single-threaded, the GC process appears to be less active.

For now, you can add a defer debug.FreeOSMemory() to force memory release. I will look into whether there are any improvements that can be made to the GC mechanism in WASM.

//go:build js && wasm

package main

import (
    "runtime/debug"
    "syscall/js"
)

func main() {
    goFunction := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        defer debug.FreeOSMemory()
        return nil
    })
    js.Global().Set("goFunction", goFunction)

    select {}
}