DEV Community

Jan Hjørdie
Jan Hjørdie

Posted on

Real Blazor WebAssembly Production Pitfalls

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")
Enter fullscreen mode Exit fullscreen mode

→ 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) { ... }
Enter fullscreen mode Exit fullscreen mode

If you rely on reflection/dynamic serializers:

[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MyDto))]
public void Serialize() { ... }
Enter fullscreen mode Exit fullscreen mode

Or root the assembly:

<ItemGroup>
  <TrimmerRootAssembly Include="$(AssemblyName)" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

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
  • AdvancedMarkerElement is 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");
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Then call via:

await module.InvokeVoidAsync("safe", "setMarkers", items);
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

✔ Fix

Use PreserveReferencesHandling or DynamicDependency:

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class MarkerDto { ... }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 _framework files (rare, but happens)

✔ Fix

Detect Maps load failure:

try {
  await google.maps.importLibrary("maps");
} catch (e) {
  showFallbackMap();
}
Enter fullscreen mode Exit fullscreen mode

7. Hosting Pitfalls: Caching, Routing & Fingerprints

Typical issues:

  • CDN caching old versions of _framework/*.wasm
  • index.html cached too long
  • 404s for lazy-loaded modules
  • Wrong MIME types for .dll and .wasm

✔ Fix

Add correct headers:

_cache-control: no-store for index.html
_cache-control: immutable for .dll, .wasm
Enter fullscreen mode Exit fullscreen mode

And SPA fallback:

Rewrite ^/(.*)$ /index.html
Enter fullscreen mode Exit fullscreen mode

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

  1. Google Maps library loaded after app code
  2. AdvancedMarkerElement accessed before marker library existed
  3. DTO properties trimmed away
  4. JS → .NET callbacks trimmed
  5. 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)