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 {}
}

Comment From: mknyszek

A few things: - debug.FreeOSMemory is overkill for this, especially since it doesn't do anything other than call runtime.GC on Wasm. (Side-note, debug.FreeOSMemory is almost always the wrong answer. It's a huge hammer.) It should be sufficient to just call runtime.GC. - Are we certain that the GC just isn't running? What happens when you print the GC count from runtime/metrics? Is it going up? - This may be a legitimate bug since we set a finalizer on JS objects, and finalizers are bug-prone in general. - Are you seeing this in a larger application? Or just in the minimal reproducer?

Thanks for your report!

Comment From: Zxilly

Would it be better to use runtime.AddCleanup instead? I haven't really looked into this API yet.

Comment From: Zxilly

I did a comparison test, and found that both runtime.AddCleanup and SetFinalizer can keep the total memory fluctuating around a certain value, but AddCleanup usually occupies more memory.

Comment From: cagedmantis

Yes, we posit that runtime.AddCleanup would do more efficient at releasing the memory.