Performance Notes
- Take your time to do this thoroughly.
- Quality is more important than speed.
Running C# in the Editor
Every step below that reads or writes a Player Setting runs inside a live Editor through the Unity
CLI. The unity-cli skill owns getting you there — installing the CLI, confirming a connected
Editor, adding the project's com.unity.pipeline package, telling a genuinely absent Editor apart
from one stuck in Safe Mode, and discovering the Editor's command catalog. Follow it first; don't
re-derive any of it here.
Two things it can't know for you:
- You need
evalin particular, not just a reachable Editor. Confirm it appears in the catalog. Its presence depends on the Pipeline package version, not on the CLI, so a healthy install can still lack it — if it's missing, say so and stop. - Player Settings can be read from
ProjectSettings/ProjectSettings.assetin a pinch, but do not write them that way. The serialized names don't match the API names, several of these settings are per-build-target, and a hand-edited value silently disagrees with what the build actually uses. An unreachable Editor is a stop for the write steps.
Run C# with unity command eval --caller plugin --skill optimize-web --code '<snippet>'. unity command defaults to a 30 second
timeout.
Passing C# to eval
eval compiles a statement block, not a file. Two consequences, both compile errors rather than
warnings:
- No
usingdirectives. The compiler readsusing UnityEditor;as a resource-disposal statement and rejects it (CS0210). - Types must be fully qualified. A bare
PlayerSettingsdoes not resolve (CS0246), and a bareObjectis ambiguous withobject(CS0104).
Reading the settings this skill audits
One call returns the whole Pre-Flight picture. Verified against Unity 6000.5.7f1:
Three API names to get right, because the obvious spellings do not exist and fail to compile:
UserBuildSettings lives in the WebGL build-support module, so it only resolves when that module is
installed. Read it in a separate call from the rest, and treat a resolution failure as "the Web
module isn't installed" rather than as a bad snippet.
codeOptimization is the one setting here that does not live in the project file. It persists to
Library/EditorUserBuildSettings.asset, and Library/ is gitignored by every standard Unity
.gitignore, so this value is per-machine and does not travel: teammates and CI do not inherit it.
Two consequences. Read it back through the API and do not look for it in
ProjectSettings/ProjectSettings.asset — it is absent there even on a successful apply, so its
absence is not a failure. And if the release build runs in CI, apply it there as a build step rather
than assuming the repository carries it.
Applying the settings
Most of the writes in this skill are a single batch, and
resources/WebOptimizer.cs [blocked] already is that batch. It declares a class
with a [MenuItem], so it is a project file, not eval input — a class declaration cannot be
flattened into a statement block. Save it under Assets/Editor/, let Unity compile, then invoke it
in one line:
Keep its using directives; they are correct in a file. For one-off changes — a single quality
level, a frame-rate flip — an inline eval statement is fine.
Verify from disk, not from the objects you just wrote
Applying a setting and reading it back in the same session proves nothing. Player Settings are
in-memory objects until something saves them, so every read-back returns the value you just
assigned whether or not it ever reached
ProjectSettings/ProjectSettings.asset. A run that skips the save reports success, and when the
Editor session ends the whole change is gone. This was observed, not theorised: a run applied
everything, read back Brotli / High / None, said it was done, and the file on disk never changed.
So after any write:
- Save.
UnityEditor.AssetDatabase.SaveAssets().WebOptimizer.csnow does this itself; an inlineevalwrite has to do it explicitly. - Read the values back and report them. Not "applied successfully" but the actual values, so the
user can see what is stored.
WebOptimizer.cslogs all nine. - For anything per-build-target, name the target you read. Several of these settings exist once per target, so a value can be correct for one target and unset for the one being built.
The same trap exists on the file-editing route in reverse: a hand-edited
ProjectSettings.asset reads back fine while the running Editor and the build still use the old
value. Verifying after a save and a reimport is what catches both.
Do not try to reproduce this in batch mode. A batch Editor invoked with -quit saves settings on
exit, so an unsaved write persists anyway and the run looks correct. Both a saving and a non-saving
version of the script pass under -quit. The bug only appears in a live Editor session, which is
where it was found. Concluding from a green batch run that the save is unnecessary is the wrong
conclusion from a test that cannot see the defect.
0. Pre-Flight
- Confirm Web build target: Read, with the Pre-Flight snippet above,
EditorUserBuildSettings.activeBuildTarget— must beWebGL; if not, warn the user. - Read compression and stripping settings: Read
compressionFormat,decompressionFallback,stripEngineCodeand the managed stripping level with the Pre-Flight snippet above. Note the stripping level isPlayerSettings.GetManagedStrippingLevel(NamedBuildTarget.WebGL)— there is noPlayerSettings.managedStrippingLevelproperty. - Read exception and optimization settings: Read
PlayerSettings.WebGL.exceptionSupportfrom the Pre-Flight snippet above. For the wasm code optimization level useUnityEditor.WebGL.UserBuildSettings.codeOptimization— thePlayerSettings.WebGL.codeOptimizationandoptimizationLevelspellings do not exist and will not compile. - Read frame rate settings: Read, with the Pre-Flight snippet above,
Application.targetFrameRateandQualitySettings.vSyncCount. - Read additional player settings: Read, with the Pre-Flight snippet above,
PlayerSettings.WebGL.dataCaching,PlayerSettings.WebGL.debugSymbolMode,PlayerSettings.WebGL.maximumMemorySize, andPlayerSettings.GetApiCompatibilityLevel. - Proceed only after compression, stripping, frame rate, and player settings are confirmed.
1. Assess Current State
- Check Build Report: Instruct the user to open
Window > General > Build Reportafter a build and identify the largest asset and code size contributors. - Verify server configuration: Ask the user to confirm whether the hosting server sends
Content-Encoding: br(Brotli) orContent-Encoding: gzipheaders, and whetherContent-Type: application/wasmis set for.wasmfiles. - Check frame rate config: Confirm, with the Pre-Flight snippet above,
Application.targetFrameRate— should be-1for Web (let the browser drive). - Check memory settings: Read, with the Pre-Flight snippet above,
PlayerSettings.WebGL.initialMemorySizeandPlayerSettings.WebGL.memoryGrowthMode. - Report findings before making recommendations.
2. Understand Request
3. Web Build Optimization Workflow
IMPORTANT: One-click optimization script
Always offer to generate this script for the user. Unity's official web optimization docs provide a single editor menu script that applies all recommended release settings at once. Place in Assets/Editor/WebOptimizer.cs — see resources/WebOptimizer.cs [blocked] for the template.
Adapt the script to the user's project needs (e.g. keep exceptions if they use try/catch, switch Brotli to Gzip for HTTP hosting). This script is the single most impactful action for a new web project — it prevents settings from being missed.
Player Settings audit
Verify and set these values through eval:
Compression and server configuration
Configure the server to:
- Serve
.brfiles withContent-Encoding: br. - Serve
.gzfiles withContent-Encoding: gzip. - Set
Content-Type: application/wasmfor.wasm,application/javascriptfor.js. - Enable HTTP/2 or HTTP/3 to parallelize chunk fetches.
If the host cannot inject Content-Encoding: set Decompression Fallback = On as a fallback only — it adds ~150 KB JS and slows startup.
Exception handling
Wasm 2023 introduces a cheaper exception model; switching from Explicitly Thrown Only (legacy) to Wasm exceptions reduces both size and cost when browser targets support it.
Remove unused resources
Three categories to audit for build size reduction:
1. Unused packages — Check Packages/manifest.json and the Package Manager In Project and Built-in views. Remove or disable packages the project does not use. The Input System package is a significant size contributor if unused.
2. Shader stripping — Configure in Edit > Project Settings > Graphics:
Test after stripping — ensure no referenced shaders were removed.
3. Web Stripping Tool (com.unity.web.stripping-tool) — Analyzes the WebAssembly binary and identifies unused Unity engine submodules (e.g. 3D graphics in a 2D-only game). Install via Package Manager, profile the build, then configure which submodules to exclude. Can yield substantial size reductions beyond what Managed Stripping Level achieves alone.
Quality settings for Web
Lower quality levels reduce load time and improve runtime performance. Set via Edit > Project Settings > Quality:
- Use Very Low or Low as the default Web quality level.
- Set it with
eval:UnityEngine.QualitySettings.SetQualityLevel(0, true);where 0 = Very Low. - Consider creating a Web-specific quality level that disables features unnecessary in-browser (real-time shadows, post-processing effects, high particle counts).
Frame rate on Web
- Set it with
eval:UnityEngine.Application.targetFrameRate = -1;— let the browser userequestAnimationFrame. - Note: Safari caps at 60 fps in WebGL; high-refresh targets do not apply.
- Use
OnDemandRendering.renderFrameIntervalto drop to 5–10 fps on static/idle screens to save battery.
KTX / Basis Universal textures
KTX2 with Basis Universal supercompression ships a single texture file that transcodes at load time to the optimal GPU format for the browser's device (BC7 on desktop, ASTC on mobile, ETC2 on older Android). This avoids shipping separate texture variants for each GPU family — critical for Web where the target hardware is unknown.
toktx CLI examples: See resources/toktx-examples.sh [blocked] for commands covering albedo (ETC1S), normals/detail (UASTC), ICC profile errors, and linear data.
Streaming on Web
- Use Addressables with remote groups hosted on a CDN with Brotli / Gzip.
- Avoid bundling the entire game into the initial download; stream levels on demand.
- Target < 30 MB initial download for "instant play"; level data follows.
- For streamed textures targeting mixed GPU hardware, prefer KTX2 bundles over per-platform variants — one bundle serves all browsers.
Profiling Web builds
Symptom → tool quick reference:
Embedding profiling symbols — browser profilers show mangled Wasm function names by default. To get readable C# method names in Chrome/Firefox flamegraphs, either enable Player Settings > Publishing > Debug Symbols for dev builds, or add a build processor:
Emscripten built-in profilers — enable one at a time via PlayerSettings.WebGL.emscriptenArgs:
GPU debugging — No Frame Debugger support on Web. Use Spector.js as a browser-based alternative — it captures draw calls and WebGL state.
Firefox about:memory — type about:memory as a URL in Firefox, click Measure to see per-tab breakdown: WASM code size, WASM heap, .data file, web audio. Watch for WASM heap > 300 MB (crash risk, especially on iOS Safari).
Editor Play Mode does not represent browser runtime; always measure in browser. Chrome and Safari GC and JIT behavior differ — test both.
Web memory directives
- Disable Read/Write Enabled on textures and meshes — it duplicates data into the WASM heap.
- Reduce
.datafile size by moving assets to Addressables or AssetBundles. - Use compressed texture formats (KTX2/Basis) to reduce both download and decoded memory cost.
iOS Safari memory limits
- iOS < 18: WebContent process limit ~1.5 GB. WASM memory (Gigacage) capped at 2 GB. Typed arrays share this pool. On iPhone X (iOS 16) heap growth caps at ~512 MB, but setting Initial Memory Size to 512 MB–1.5 GB upfront works.
- iOS 18+: Limits largely lifted; iPhone 11 can allocate ~4 GB.
- On iOS, set Initial Memory Size to the target peak rather than relying on growth — Safari handles large upfront allocations better than incremental growth.
- WASM heap > 300 MB risks crashes on older iOS; target < 200 MB for broad compatibility.
Video and audio on Web
- Video: Playback only works from a URL (server with CORS enabled) or from StreamingAssets. On iOS the server must support HTTP range requests for streaming. Use browser-compatible formats (MP4/H.264).
- Audio: AudioEffects (mixer effects) require compute shaders — not available on WebGL. Mixers and MixerGroups work for volume control only. Set audio to Mono to improve loading. If
about:memoryshows web audio > 100 MB, audio is likely uncompressed — switch to Vorbis.
Canvas and DPI
If the canvas is scaled up it takes the new resolution. Use devicePixelRatio in the web template to offset DPI scaling and avoid rendering at unnecessarily high resolution.
4. Validation
- Re-read the Player Settings with the Pre-Flight snippet (compression, stripping, exceptions, targetFrameRate).
- Rebuild the player and compare Build Report file sizes with baseline.
- Verify in at least Chrome and Safari (GC and JIT behavior differ).
- Max 3 iterations before asking the user for feedback.
5. Troubleshooting
Build still large after enabling Strip Engine Code
- Is Managed Stripping Level set to Medium or Low? → Set to High for release.
- Are plug-ins using reflection to access engine modules that would otherwise be stripped? → Add a
link.xmlto preserve needed symbols. - Is Exceptions set to Full? → Full adds the largest code overhead; switch to None or Explicitly Thrown Only.
Brotli not working — Decompression Fallback required
- Is the server sending
Content-Encoding: br? → Without this header the browser won't decompress; the fallback JS decompressor is then needed. - Is the build hosted over HTTP (not HTTPS)? → Brotli requires a secure context; degrade to Gzip for HTTP hosting.
Stutter in Safari but not Chrome
- Does the project set
Application.targetFrameRate = 60? → On Safari WebGL this conflicts with browser pacing; set to-1. - Are there shaders that behave differently on Safari's WebGL implementation? → Test on device; Safari's WebGL/Wasm runtime differs from Chromium — some GLSL constructs are handled differently.
Memory growth slow path triggered
- Is Initial Memory Size too small for the project's peak? → Wasm memory growth requires a full buffer copy; set Initial Memory Size to a realistic peak estimate.
- Is Memory Growth Mode set to Linear? → Switch to Geometric for saner growth curve.
Frame rate set to 60 but browser runs erratically
- Is
Application.targetFrameRate = 60set in code? → On Web this conflicts withrequestAnimationFramebrowser pacing. Set to-1. - Is
vSyncCountnon-zero? → Set to 0; the browser handles pacing.
Firefox cache rejecting large files
Firefox limits individual cache entries via browser.cache.disk.max_entry_size. If the build exceeds this (default ~50 MB), assets won't cache. Solution: use Addressables to split into bundles < 51 MB, or instruct users to increase the setting in about:config.
Local dev server setup
For testing builds locally with proper MIME types:
For Brotli testing, use HTTPS — Brotli requires a secure context. Generate a self-signed cert with OpenSSL for local testing.
6. Completion
- Summarize: initial download size delta, settings changed (compression, stripping, exceptions, targetFrameRate), server configuration confirmed.
- List follow-up actions: CDN setup for Addressables remote groups, Safari testing, Wasm 2023 feature set upgrade when browser baseline allows.
See also
These point at Unity tooling rather than other skills, because the topics they cover are not in this plugin:
- Addressables package — remote groups served over a CDN, when the download budget needs content moved out of the initial payload.
- Unity Profiler, connected to the browser — the cross-platform profiling methodology. Section 3 covers the Web-specific part of attaching it.
- Shader variant stripping (Graphics settings → Shader Stripping, and
ShaderVariantCollection) — variant count feeds directly into Wasm size, so it is worth checking when stripping alone hasn't moved the number. - Project Settings → Player — the same flags this skill reads, if the user would rather see them in the inspector than have them reported.
- Mobile browser battery behaviour follows the same frame-rate and quality-level guidance in Sections 3 and 4; there is no separate mobile path here.

