Godot 4 web export stutters on mobile? It's probably your art pipeline, not your code — 31fps to 60fps (benchmarks inside)

## Godot 4 web export stutters on mobile? It’s the art pipeline, not the engine

We ship a Godot 4 web game (WebGL2). It ran fine on desktop, but on a mid-range Android it was a slideshow: **31fps in battle, 22s load time, 612MB RAM**. We blamed the engine for weeks.

The truth: **~80% of web-game performance problems live in the art pipeline** — textures, atlases and loading order. Same scenes, same art, only the delivery pipeline changed, and we went from 31fps → 60fps, 612MB → 152MB.

Here’s what mattered, mapped to Godot specifically.

### 1. Compress for GPU, not just for download

The #1 mistake: exporting everything as WebP.

WebP saves bandwidth, but the browser still **decompresses it to full RGBA in memory** at runtime. A 1024×1024 texture is 4MB uncompressed (16MB with mipmaps).

In Godot: select your textures in the FileSystem dock → **Import tab → Compress → “VRAM Compressed (ETC2/ASTC)”**. Godot handles format detection on Web automatically (ASTC where supported, ETC2 fallback).

Reserve WebP (“Lossless”/“Lossy” mode) for UI, icons and loading screens — things that don’t get sampled at scale in 3D scenes.

### 2. Atlas everything, with padding AND extrude

Hundreds of loose sprites = hundreds of draw calls and HTTP requests. In Godot, pack sprites into atlases and reference regions with **AtlasTexture** (or generate atlases with a tool and load the JSON at runtime).

Two rules that actually matter:

- **2px padding** around each sprite — stops bleeding artifacts with linear filtering

- **1px extrude** (duplicate edge pixels) — kills edge halos completely, which padding alone doesn’t fix

We went from 250 loose sprites → 4 atlases. Draw calls: 240 → 38. Requests: 250 → 4. Biggest single win on the art side.

### 3. Lazy load by priority tiers

Loading everything at boot is how you get 20s load screens. Split assets:

| Tier | What | When |

|—|—|—|

| **Critical** | Menu, first scene | Preload at boot |

| **Near** | Next 1-2 scenes | Preload during current scene |

| **Far** | Bosses, later levels | Lazy-load on demand |

In Godot, use `ResourceLoader.load_threaded_request()` for background loads, and trigger loads by proximity/zone entry rather than one flat preload list. The loading screen should never wait for content the player can’t see yet.

### 4. Adapt quality to the device

A desktop RTX and a 2019 Android phone should not receive the same texture set. Detect once at boot (`OS.has_feature(‘web’)` + device hints like `navigator.deviceMemory` / `navigator.hardwareConcurrency` via JavaScriptBridge if you want the details), then pick a quality profile: texture scale, viewport size, shadow settings.

Monitor FPS and drop quality gracefully below ~40fps. Players prefer a slightly softer texture over a stuttering game. Quality adaptation is a UX feature, not a compromise.

### 5. Real benchmarks

Mobile RPG prototype, 3 scenes, 260 sprites, heavy effects:

| Metric | Before | After |

|—|—|—|

| First paint | 8.2s | 2.1s |

| Full load | 22s | 4.2s |

| Peak memory | 612MB | 152MB |

| Draw calls (battle) | 240 | 38 |

| Avg FPS (mid-range Android) | 31 | 60 |

**Checklist before you write more optimization code:** GPU-compressed scene textures, atlases with padding + extrude, priority-tier lazy loading, device-adapted quality, automated build (never hand-edit atlases).

-–

Full write-up with import settings screenshots, the atlas toolchain and loader code:

What’s the biggest performance surprise you’ve hit with Godot web export? I’m curious what’s breaking for other people.

1 Like