Godot 4.5 Web Export WASM Memory OOM After Browser Tab Refocus - How to Fix
Problem: Your Godot 4.5 HTML5 demo on itch.io runs fine for ten minutes. The player alt-tabs to Discord, returns, and the canvas freezes, reloads, or shows a gray screen. DevTools may log RuntimeError: memory access out of bounds, wasm memory limit exceeded, or silent tab discard with no stack trace.
Who is affected now: June 2026 festival teams shipping browser clips beside PC Next Fest builds. Boot already passes itch WASM MIME checks—this failure happens after load when visibility changes re-allocate textures, audio decode buffers, or threaded work on a fragmented heap. Distinct from MIME boot hang and from audio silent first load.
Fastest safe fix: Reproduce with a blur/focus matrix → on visibilitychange pause tree + suspend audio when hidden → release prior floor epoch assets → cap texture cache → run ten refocus cycles on Chrome + Firefox → file wasm_memory_smoke_receipt_v1.json before updating the public demo link.
Direct answer
Browser tabs are not desktop play mode. When a tab goes hidden, Chromium may throttle, discard, or trim WASM memory; on refocus Godot may re-init subsystems while old floor scenes, atlases, and audio buses still hold references. WASM cannot grow like OS virtual memory—fix lifecycle on hidden and epoch teardown on floor change, not “buy more RAM.”
Why this issue spikes in H2 2026
- Dual-SKU demos — PC floor logic reused for itch without a web memory budget.
- Fest clip traffic — Streamers alt-tab constantly; QA only tests uninterrupted play.
- Godot 4.5 threaded web exports — Loader threads keep allocating if the tree never pauses on hidden (COOP/SAB hosting).
- Roguelite session length — Players exceed the heap that passed a five-minute smoke test.
- Tab discard policy — Laptops aggressively reclaim background tabs; refocus replays init on a smaller budget (WASM ceiling playbook).
Pair with unscoped HTML5 SKU opinion when marketing promises a full PC run in the browser.
Symptoms and search phrases
- Freeze or full reload only after alt-tab / minimize / second monitor switch.
- Console:
memory access out of bounds,abort(),wasm memory. - Works in editor Play for 30+ minutes; fails on itch in 15–40 minutes with refocus.
- Memory column in Chrome Task Manager climbs each refocus cycle.
- Threaded export: hang correlates with SharedArrayBuffer hosts and background throttling.
- No MIME error—boot hang help already green.
Root causes (check in order)
- No pause on
document.hidden— simulation + loaders continue off-screen. - Textures/audio not released on floor transition—prior floors stay referenced.
- Missing
window.blur/visibilitychangewiring in web bootstrap. - Thread pool keeps allocating while tab backgrounded (threaded preset).
- SAB pool too small for refocus re-init path—or growth disabled with assets too large.
- Tab discard — browser kills instance; reload doubles peak if caches not cleared.
- Press-kit iframe embed — parent page lifecycle + tighter memory vs itch top-level tab.
Beginner path (first 30 minutes)
Prerequisites: Godot 4.5.x, HTML5 export preset, itch demo URL, Chrome DevTools.
- Open demo → play until floor 2–3 loads.
- Open DevTools → Performance monitor or Task Manager → note memory.
- Alt-tab away 30 seconds → return.
- If freeze → open Console; screenshot errors.
- Repeat ten times—note if failure is cycle 1 or cycle 6+.
- Retry in Firefox (different discard timing).
- If never reproduces without refocus → suspect long-session OOM; still add hidden pause (prevention).
Common mistake: Testing only in editor or only in uninterrupted fullscreen—refocus is the test case.
Fastest safe fix path
Step 1 — Repro matrix (blur / focus / discard)
| Case | Action | Pass |
|---|---|---|
| A | Alt-tab 30s, return | No freeze; memory stable ±10% |
| B | Minimize window 60s | Same |
| C | Open second tab on same window, return | Same |
| D | itch embed iframe on press kit | Same as top-level or document embed gap |
| E | Ten rapid alt-tabs | No OOM by cycle 10 |
Log browser + preset in release-evidence/qa-and-repro/web-refocus-matrix.md.
Step 2 — Pause tree and suspend audio when hidden
Add an autoload or main scene hook (GDScript 4.x):
# WebVisibility.gd (autoload)
func _ready() -> void:
if OS.has_feature("web"):
var win := JavaScriptBridge.get_interface("window")
if win:
win.addEventListener("visibilitychange", Callable(self, "_on_visibility_change"))
func _on_visibility_change(_ev = null) -> void:
var hidden := JavaScriptBridge.eval("document.hidden", true)
if hidden:
get_tree().paused = true
AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), true)
else:
get_tree().paused = false
AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), false)
Pro tip: Do not spawn new ResourceLoader.load_threaded_request while document.hidden is true.
Step 3 — Floor epoch teardown (release prior floor)
On floor transition, free the previous scene instance and clear caches:
func _transition_to_floor(packed: PackedScene) -> void:
if _current_floor:
_current_floor.queue_free()
_current_floor = null
# Optional: drop atlases held only by prior floor
ResourceLoader.clear_cache() # use sparingly; prefer targeted unref
_current_floor = packed.instantiate()
add_child(_current_floor)
See WASM memory ceiling playbook for epoch tables and roguelite scope caps.
Step 4 — Cap texture cache and streaming
| Knob | Web demo guidance |
|---|---|
| Max texture size | 2048 or 1024 for UI atlases on web SKU |
| Simultaneous floors in memory | 1 active + optional preload 1 |
| Particles/VFX | Disable heavy GPUParticles on web preset |
| Audio streams | Stream Ogg; avoid loading full WAV banks |
Export a Web_demo_scope preset separate from PC—document deltas in release-evidence/README.
Step 5 — Threaded export decision
If refocus OOM only on threaded itch preset:
- Re-test non-threaded export (MIME help preset table).
- Or keep threads but enforce Step 2–3 strictly.
- Confirm itch SharedArrayBuffer toggle matches COOP help.
Step 6 — Emscripten memory flags (advanced)
When custom export hooks exist, align with Emscripten memory growth docs—raising MAXIMUM_MEMORY without teardown delays OOM; it does not fix retained floors. Prefer release discipline first.
Verification checklist
- [ ] Ten refocus cycles without OOM on Chrome (latest)
- [ ] Same on Firefox
- [ ] Chrome Task Manager: memory does not climb unbounded across cycles
- [ ] Hidden tab:
get_tree().paused == true(debug overlay or log) - [ ] Floor 6+ transition still releases prior floor (heap step-down visible)
- [ ] itch embed + fullscreen both pass matrix row D
- [ ]
wasm_memory_smoke_receipt_v1.jsoncommitted withpass: true
Working dev path — wasm_memory_smoke_receipt_v1.json
{
"schema": "wasm_memory_smoke_receipt_v1",
"engine": "godot-4.5",
"host": "itch.io",
"export_preset": "Web_demo_nonthreaded",
"checked_at_utc": "2026-05-24T18:00:00Z",
"browsers": ["chrome", "firefox"],
"refocus_cycles": 10,
"refocus_pass": true,
"floor_reached": 6,
"peak_heap_mb_estimate": 412,
"visibility_pause_enabled": true,
"floor_epoch_teardown": true,
"pass": true
}
Wire a manual or CI step: human runs matrix E, fills receipt, attaches to Wednesday demo smoke ritual folder beside BUILD_RECEIPT.
Proof table (fest gate)
| Check | Evidence | Owner |
|---|---|---|
| Refocus ×10 | wasm_memory_smoke_receipt_v1.json |
QA |
| Hidden pause | Screenshot of debug flag or log line | Engineering |
| Web SKU scope doc | Web_demo_scope preset diff vs PC |
Design |
| Embed vs tab | Matrix row D notes | Marketing |
| Long session 45m | Optional heap log in web-heap-log.md |
QA lead |
Alternative fixes
| Branch | When | Action |
|---|---|---|
| Boot never loads | MIME / SAB errors | itch WASM MIME fix |
| Audio only broken after refocus | Buses not suspended | Web audio gesture help |
| OOM without refocus | Long roguelite run | WASM ceiling playbook |
| Phaser/HTML5 sibling | Cross-engine team | Phaser tab-refocus OOM blog |
| Export preset vanished | After 4.5 patch | EditorExportPlugin preset help |
Prevention
- Add refocus cycles to browser demo scope doc before trailer capture.
- Block public itch updates until receipt
pass: true. - Cap web SKU floors/enemies separately from PC in design doc.
- Run web CI matrix preflight when toggling threaded presets.
- Pair with 5-day crash log challenge for repro folders.
FAQ
Is this the same as itch WASM MIME boot hang?
No. MIME failures stall at the progress bar before main scene. Refocus OOM happens after gameplay starts.
Will increasing Godot export memory fix it?
Only buys time. Without epoch teardown and hidden pause, you will still OOM on long runs or repeated refocus.
Does editor Play reproduce it?
Rarely. Test exported HTML5 on itch or python -m http.server with the same preset.
Should I disable threads?
Try non-threaded if refocus OOM correlates with background loader activity; many jams ship non-threaded successfully.
Steam wrapper vs itch?
Steam browser shell has its own lifecycle; run matrix on the same surface players use in fest clips.
Related links
- itch.io HTML5 Godot 4.5 WASM MIME Boot Hang Fix
- Godot Web Export SharedArrayBuffer COOP COEP Fix
- Godot 4.5 Web Export Audio Silent First Load Fix
- Godot 4.5 EditorExportPlugin Preset Missing After Patch
- Godot 4.5 WASM Memory Ceiling H2 2026 (blog)
- Your itch Browser Demo Is Not Free Marketing (blog)
- 15 Free Godot 4.5 Web Export Resources
- Godot 4.5 Web Export CI Matrix (guide)
- Official: Godot exporting for the Web, Page Visibility API (MDN)
Run ten refocus cycles before you send the fest clip link—uninterrupted playtests lie about browser demos.