GodotWebGLWebAssemblyGame DevelopmentOptimization

Checking a Godot 4 web export: threads, file sizes, loading, and audio

··4 min read

When a working desktop game stalls in its web export, first separate file delivery, export requirements, and game startup. Rebuilding the engine before identifying which layer failed can add work without fixing the cause.

This case uses Last Pitch Derby’s committed files and settings at revision 4a7aec3. File sizes and loader code were checked on September 21, 2026; iPhone performance was not retested. Earlier abandonment-rate, instant-loading, and 35-to-8 MB claims lacked supporting measurements and have been removed.

Start with the export configuration

The site’s export_presets.cfg leaves custom_template/release empty and sets both variant/thread_support and variant/extensions_support to false. This is not evidence of a custom stripped template. Record the editor, export template, and feature flags before applying advice from a different export configuration.

ini
custom_template/release=""
variant/extensions_support=false
variant/thread_support=false

Godot documents cross-origin isolation requirements for Thread Support and Extensions Support. When enabling either, verify the actual HTTP response and embedding environment rather than merely finding a _headers file. A single-threaded export still needs gameplay and audio checks.

Measure the compressed file, engine, and game data separately

In this revision, index.wasm.gz is 10,052,173 bytes, its decoded WASM is 39,509,339 bytes, and index.pck is 371,884 bytes. These are file measurements, not browser-memory measurements or load times. Network conditions, caches, hardware, and server configuration affect the waiting time.

javascript
import { readFileSync } from 'node:fs';
import { gunzipSync } from 'node:zlib';
import { createHash } from 'node:crypto';
const root = 'public/games/last-pitch-derby/';
for (const file of ['index.wasm.gz', 'index.pck']) {
  const bytes = readFileSync(root + file);
  console.log(file, bytes.length,
    createHash('sha256').update(bytes).digest('hex'));
  if (file.endsWith('.gz')) {
    console.log('decoded WASM bytes', gunzipSync(bytes).length);
  }
}

Save the code as measure.mjs and run node measure.mjs from that revision’s repository root. If results differ, compare hashes and revisions before attributing the change to compression. When the PCK grows, inspect project resources and export filters instead of focusing only on the engine binary.

Download the measurements, hashes, and reproduction steps

Verify the path through the loader

The packaging script produces a gzip-compressed WASM file. The generated HTML intercepts requests for index.wasm and fetches index.wasm.gz. When the server declares Content-Encoding: gzip, the loader avoids manually decoding again; otherwise it uses DecompressionStream and constructs an application/wasm response. Renaming a gzip file does not implement that delivery path.

For a blank screen, inspect WASM and PCK responses in Network: status, body, and size. An incorrect path may deliver an HTML error page, or the loader may receive bytes it cannot decode. This loader explicitly requires DecompressionStream; its absence is a compatibility boundary of this deployment approach.

Downloaded is not the same as interactive

The HTML uses current/total from onProgress and hides its loading notice after engine.startGame resolves. It does not measure compilation as 80–95% or shader work as the final 5%. Use an indeterminate startup message when no stage progress exists. Measuring the first interactive frame requires a separate signal from the game.

Record request start, download completion, and the first input that produces a game response. Compare cold and warm cache runs on the same device with network conditions noted. That separates transfer delay from startup work; smaller gzip bytes alone cannot establish a fourfold speedup.

Diagnose audio and mobile failures with observable steps

Test audio from a fresh page: press an explicit start control, check sound, switch away, and return. Do not take over the engine using an assumed Engine.getAudioContext API, or label every silent run a permanent deadlock. Inspect the configured audio mode and actual console errors first.

A reload near 90% does not by itself establish an out-of-memory failure. Record hardware and software versions, verify assets, and compare reduced texture, scene, or rendering workloads. Change one factor at a time and record whether the failure remains. This article supplies neither a universal iPhone memory ceiling nor a magic audio-buffer size.

Keep this release record

Keep export settings, WASM/PCK hashes, the production URL, browser version, and results for loading, first input, audio, backgrounding, and reload. File and configuration checks can run locally. Whether an iPhone Safari session is playable still requires interaction on that device.

Sources