## 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.