Real Blazor WebAssembly Production Pitfalls
(With Google Maps as a Case Study)
Blazor WebAssembly works beautifully in development — but the moment you publish a Release build with PublishTrimmed=true, you start seeing an entirely different runtime behavior:
- Features that worked locally stop working
- External SDKs fail without console errors
- JS interop calls disappear into the void
- Reflection stops working
- Maps, charts, and 3rd‑party UI break silently
This blog post is a deep, technical walkthrough of the real pitfalls you hit when you move Blazor WASM into production — with Google Maps (Advanced Markers + MapId/StyleId + lazy load) as a concrete case study.
This is not theory.
This is what actually breaks when you ship.
1. Trimming Removes .NET Methods Used by JS Interop
Blazor’s linker (ILLink) removes all methods and types that appear unused.
This includes methods you call from JavaScript.
❌ Symptom
In Debug:
DotNet.invokeMethodAsync("MyApp", "OnMarkerClicked")
→ Works.
In Release with trimming:
→ No error.
→ Nothing happens.
→ The method was removed.
✔ Fix
Mark all JS‑callable .NET methods:
[JSInvokable]
public static void OnMarkerClicked(string id) { ... }
If you rely on reflection/dynamic serializers:
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MyDto))]
public void Serialize() { ... }
Or root the assembly:
<ItemGroup>
<TrimmerRootAssembly Include="$(AssemblyName)" />
</ItemGroup>
2. External Libraries Load Faster in Production
JS bundles load faster after trimming + minification.
This reveals race conditions you never see locally.
Google Maps example:
In Debug:
- Your script loads
- Then Google Maps loads
- Then you call
AdvancedMarkerElement
In Release:
- Your code runs before the Maps library exists
-
AdvancedMarkerElementis undefined - No errors from Google Maps (silent fail)
✔ Proper Fix (Google's recommended way)
Use importLibrary():
const mapsLib = await google.maps.importLibrary("maps");
const markerLib = await google.maps.importLibrary("marker");
This removes 100% of all race conditions.
3. JS Exceptions Are Hidden in Minified Release
Many JavaScript errors disappear after minification.
Especially inside Google Maps, Stripe, and other SDKs.
❌ Symptom
Everything “just stops working” — no console errors.
✔ Fix
Wrap interop calls in try/catch and surface errors to .NET:
export async function safe(fn, ...args) {
try {
return await fn(...args);
} catch (e) {
console.error("JS Error", e);
throw e;
}
}
Then call via:
await module.InvokeVoidAsync("safe", "setMarkers", items);
Now errors show up again.
4. JSON Serialization + Trimming = Missing Properties
If your DTOs are used dynamically — especially when loading external data (API → map markers) — trimming removes properties.
❌ Symptom
Objects arrive missing fields:
{ "lat": 0, "lng": 0 }
✔ Fix
Use PreserveReferencesHandling or DynamicDependency:
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class MarkerDto { ... }
Or root your shared models assembly.
5. External APIs Fail Only in Production
Google Maps, Stripe, Firebase, and many others check:
- referrer domains
- HTTPS
- billing enabled
- CORS
- CSP rules
❌ Symptom
Everything works locally.
In prod — nothing loads.
✔ Fix Checklist
Allow these in production:
https://*.yourdomain.com/*- HTTPS only
- CSP rules:
script-src maps.googleapis.com maps.gstatic.com 'self'
connect-src https://maps.googleapis.com
And verify billing + quotas.
6. “Silent Failures” From Adblockers and Privacy Extensions
In production environments, up to 40% of users run blockers that:
- Block Maps scripts
- Block geolocation
- Block telemetry endpoints
- Block Blazor's own
_frameworkfiles (rare, but happens)
✔ Fix
Detect Maps load failure:
try {
await google.maps.importLibrary("maps");
} catch (e) {
showFallbackMap();
}
7. Hosting Pitfalls: Caching, Routing & Fingerprints
Typical issues:
- CDN caching old versions of
_framework/*.wasm -
index.htmlcached too long - 404s for lazy-loaded modules
- Wrong MIME types for
.dlland.wasm
✔ Fix
Add correct headers:
_cache-control: no-store for index.html
_cache-control: immutable for .dll, .wasm
And SPA fallback:
Rewrite ^/(.*)$ /index.html
8. Google Maps Case Study: What Was Actually Broken
A real-world case:
❌ Local debug:
Everything works.
❌ Production:
- No map
- No markers
- No console errors
- Nothing loads
Root Causes
- Google Maps library loaded after app code
-
AdvancedMarkerElementaccessed before marker library existed - DTO properties trimmed away
- JS → .NET callbacks trimmed
- CSP blocked maps scripts
✔ Final Working Solution
- Inline Google Maps bootstrap loader
- Lazy load via
importLibrary() - All JS-callable .NET methods marked
[JSInvokable] - Assembly rooted
- CSP + referrer config fixed
- Wrapped all JS interop in safe error forwarding
→ 100% stable in production.
Final Thoughts
Blazor WebAssembly is powerful — but once you publish with trimming, you are no longer running the same application as in Debug.
If something works locally but breaks silently in prod, it is almost always one of these:
- Trimming removed something
- JS executed too early
- External SDK blocked
- CSP/Referrer issues
- DTOs missing due to ILLink
- JS exceptions swallowed by minification
Use the patterns in this post, and your WASM apps will behave identically in Debug and Release.
Top comments (0)