I'm creating a game for which I chose a layout of 4K (3840x2160) because I want it to look as crisp as possible and also support that resolution "natively". My primary target is a native app on computers (Windows, Mac, Linux) but I'm using WebAssembly to share my progress with some friends easily so that they don't have to download a binary each time I update something.
However, I noticed that my game is using more and more memory in the browser and eventually crashes because of this. Even though the game runs fine as a native app and doesn't use a lot of memory at all.
I have made the following example app to demonstrate this problem: https://github.com/vzx/ebiten-memory-usage
Which you can see in action here: https://vzx.github.io/ebiten-memory-usage/
On my Windows computer as a native app, the Alloc stays around 20-40MB, while the Sys stays around 90-100MB all the time. Not much at all. Though the Total is constantly increasing, quite rapidly, which indicates to me that a lot of things are created, discarded, then cleaned up by the garbage collector.
In my browser, the Alloc keeps rising, though quite slowly. More interestingly, the Sys keeps rising as well. When that reaches 1GB it crashes on Firefox, when it reaches 2GB it crashes on Chrome (it looks like that is the amount of memory allocated to the Go WebAssembly runtime on those browsers).
When I use a lower resolution (eg. 1280x720), the problem isn't as obvious and it takes a lot longer to crash. But I definitely see the memory usage rising slowly. Unfortunately (fortunately?), none of the examples show this because they generally use a very low resolution, and memory usage is rising very very slowly.
It could be that this is just a bug in the Go WebAssembly runtime (I'm using Go 1.15), but so far I haven't been able to find a good way to reproduce it and couldn't find any open bug reports about this.
I'm not entirely sure that this is in fact a bug in Ebiten, but I felt this was a good first starting point to raise the issue.
Edit:
I made a very small console app which just runs an infinite loop, create a new array of 1MB every iteration an fills it with data. After running that for about 20 minutes, it looks like this:
Alloc: 3.03 MiB; Total: 36.74 GiB; Sys: 10.87 MiB; NextGC: 4.00 MiB; NumGC: 9405
I was already thinking that it might have been related to not being able to run the garbage collector often enough, but even when I put runtime.GC() in the Update() function it doesn't seem to help.
Thanks. Confirmed that Alloc increases monotonically even with GCs on Chrome and Firefox.
Interestingly, layoutWidth, layoutHeight = 1920, 1080 doesn't cause the issue but layoutWidth, layoutHeight = 3840, 2160 causes the issue. Hmm?
A minimized test case:
package main
import (
"fmt"
_ "image/png"
"log"
"runtime"
"time"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
const (
// The threshold depends on the actual canvas size.
layoutWidth, layoutHeight = 1300, 1000
windowWidth, windowHeight = 1920, 1080
)
var (
isWasm = runtime.GOARCH == "wasm"
)
type Game struct {
start time.Time
totalRunTime time.Duration
ticks uint64
memStats *runtime.MemStats
}
func (g *Game) Update() error {
if !isWasm && ebiten.IsKeyPressed(ebiten.KeyEscape) {
return fmt.Errorf("esc pressed")
}
if inpututil.IsKeyJustPressed(ebiten.KeyG) {
runtime.GC()
}
if inpututil.IsKeyJustPressed(ebiten.KeyT) {
max := ebiten.MaxTPS()
if max == ebiten.UncappedTPS {
ebiten.SetMaxTPS(60)
} else {
ebiten.SetMaxTPS(ebiten.UncappedTPS)
}
}
g.ticks++
if g.ticks%30 == 0 {
runtime.ReadMemStats(g.memStats)
g.totalRunTime = time.Now().Sub(g.start)
}
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
ms := g.memStats
msg := fmt.Sprintf(`TPS: %0.2f (max: %d); FPS: %0.2f
Run time: %v
ticks: %d
Alloc: %s
Total: %s
Sys: %s
NextGC: %s
NumGC: %d
<G>: run garbage collection
<T>: toggle unlimited TPS`,
ebiten.CurrentTPS(), ebiten.MaxTPS(), ebiten.CurrentFPS(),
g.totalRunTime,
g.ticks,
formatBytes(ms.Alloc), formatBytes(ms.TotalAlloc), formatBytes(ms.Sys),
formatBytes(ms.NextGC), ms.NumGC,
)
ebitenutil.DebugPrintAt(screen, msg, 10, 54)
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return layoutWidth, layoutHeight
}
func formatBytes(b uint64) string {
if b >= 1073741824 {
return fmt.Sprintf("%0.2f GiB", float64(b)/1073741824)
} else if b >= 1048576 {
return fmt.Sprintf("%0.2f MiB", float64(b)/1048576)
} else if b >= 1024 {
return fmt.Sprintf("%0.2f KiB", float64(b)/1024)
} else {
return fmt.Sprintf("%d B", b)
}
}
func main() {
ebiten.SetWindowSize(windowWidth, windowHeight)
memStats := &runtime.MemStats{}
runtime.ReadMemStats(memStats)
if err := ebiten.RunGame(&Game{memStats: memStats, start: time.Now()}); err != nil {
log.Fatal(err)
}
}
The leak shows when (*uiContext).screenScale() < 1.
graphics' theVerticesBackend was grown significantly. When the memory usage jumps. Actually this backend is used only on Wasm.

Fixed! Thank you for reporting!
Wow that was fast! Thanks so much for looking into it immediately and fixing this!!