Repository navigation
Description
Summary
With a flutter build web --wasm (skwasm) build, an HtmlElementView whose platform-view factory returns an empty container div — into which content (a real third-party <iframe>) is appended later, after an async gap — silently fails to ever show that content when the page is running under real GPU-accelerated rendering. No console error, no exception, nothing in Network — the container div exists, but stays empty forever. The exact same build works correctly when Chrome is forced to --disable-gpu (software rendering), which is what makes this easy to miss in CI/headless testing that defaults to software rendering.
Concretely, this breaks Google Identity Services' "Sign in with Google" button (google_identity_services_web's gis.id.renderButton(), which injects Google's own iframe into a container Element you hand it) — but the underlying issue looks like a skwasm platform-view compositing bug, not something specific to that package.
Environment
Flutter 3.47.1 • channel stable
Framework revision 6655482ec0 (2026-08-19)
Engine revision 5d5317886
Tools Dart 3.13.1
- Platform: web,
--wasm(skwasm renderer) - Reproduces in headless Chromium (
--headless=new) only with real GPU acceleration enabled (--use-gl=angle --use-angle=gl --enable-gpu-rasterization --ignore-gpu-blocklist). Does not reproduce with--disable-gpu(software/Skia CPU fallback) — that's the part that makes it easy to miss. - Reproduces identically whether skwasm is running single-threaded (the default, no cross-origin isolation) or genuinely multi-threaded (page served with
Cross-Origin-Opener-Policy: same-origin+Cross-Origin-Embedder-Policy: require-corp, confirmed viacrossOriginIsolated === trueand the single-threaded-fallback console warning disappearing). So it isn't specific to the single-threaded fallback path either.
What's affected in the real app
google_identity_services_web's GSI button, rendered via roughly this pattern (trimmed from the actual widget):
class _GoogleSignInButtonState extends State<_GoogleSignInButton> {
final _containerReady = Completer<web.Element>();
@override
void initState() {
super.initState();
ui_web.platformViewRegistry.registerViewFactory(_viewType, (int viewId) {
final container = web.document.createElement('div');
container.setAttribute('style', 'width: 100%; height: 100%; ...');
_containerReady.complete(container);
return container;
});
Future.wait([_ensureGisScriptLoaded(), _containerReady.future]).then((results) {
final container = results[1] as web.Element;
gis.id.renderButton(container, gis.GsiButtonConfiguration(/* ... */));
});
}
@override
Widget build(BuildContext context) => HtmlElementView(viewType: _viewType);
}On production (a real, deployed Flutter web app built with --wasm), this container's div is created and reachable in the DOM, google.accounts.id loads successfully, renderButton() is called with no thrown error — but document.querySelectorAll('iframe[src*="accounts.google.com"]').length is 0 afterward, and the button is visibly just empty space. Same build, same page, --disable-gpu: the iframe appears correctly (length: 1), button renders and is clickable.
What I ruled out
Trying to narrow this to a minimal reproduction, all of the following work correctly (real GPU, same Flutter version, same --wasm build):
- A bare
HtmlElementViewwhose factory returns a real<iframe>directly (synchronously, no async gap). - The same, but the factory returns an empty container div and an iframe is appended into it after an
await Future.delayed(...)— matching the real code's two-phase container-then-content shape. - The real
google_identity_services_webpackage, realgis.id.renderButton()call, real Google client ID, againsthttps://accounts.google.com/gsi/clientfor real — in a fresh, otherwise-emptyflutter createproject. - All of the above wrapped in
SelectionArea(desktop web'sSelectableRegionwraps platform views in its ownStack/HtmlElementView, which has separately-documented layout-timing issues, so this seemed like a plausible interaction) at a desktop-sized viewport (1400×1000) — still renders fine.
So this doesn't appear to be the platform-view mechanism alone, google_identity_services_web alone, or SelectionArea alone — something about the surrounding app (larger widget tree / more concurrent platform views or semantics work / navigation-triggered mount rather than initial-load mount / something else not yet isolated) is a necessary ingredient, and I wasn't able to isolate it further before filing this. Happy to help narrow it down further if it'd help — a maintainer with skwasm rendering-pipeline context probably has a much better idea where to look than blind bisection from the outside.
Reproducing
I have a real, currently-broken deployment I can point a maintainer at privately, and can capture a full trace, screen recording, or a minimal standalone project on request — didn't want to drop a live link to a business deployment straight into a public issue without being asked for one. Ping me here and I'll follow up with whatever's most useful.
Impact
This silently and completely breaks the only sign-in method on the affected page for any real (GPU-accelerated) browser — no error surfaces to the user or to error-tracking (Sentry, in our case), since nothing throws. We've reverted off --wasm in production because of this. Flagging the severity since "the primary CTA on the page silently does nothing, with zero diagnostic signal" seems like a bad failure mode for skwasm's platform-view compositing generally, beyond just this one widget.
Activity
mbcorona commentedon Sep 23, 2026
Hi @0danylo, thanks for the report.
Could you share a standalone minimal project that reproduces it, or a captured trace plus the private deployment link you offered?
0danylo commentedon Sep 24, 2026
Hi, I checked the actual DOM and there is exactly one
flt-platform-viewhost in the whole document, and it belongs toSelectableRegion's own internal context menu view, not the Google Sign In button view. The app wraps its whole desktop layout inSelectionArea, which is why that internal platform view exists on this screen at all.A full document search for the button's container div, matched by the distinctive inline style its own
registerViewFactorycallback sets (overflow: hiddenplusjustify-content: center, unique to this widget), finds zero matches anywhere on the page. The container the app creates for the button never gets attached to the DOM.Also zero network requests to
accounts.google.com/gsi/buttonare ever issued, and there are no console errors or exceptions the whole time.renderButton()presumably never gets a real attached container to work with, and Google's SDK does not throw when that happens. It just does nothing visible.So when the widget tree has both an app registered
HtmlElementViewplatform view andSelectableRegion's own internal platform view, only one platform view slot ends up attached to the document, and the other is dropped rather than composited incorrectly.One more thing: I tried an isolated repro with the real Google Identity Services button plus
SelectionAreaat desktop width, and it did not reproduce this failure on its own. So this looks necessary but not sufficient by itself. Something about the full app (more platform views elsewhere, timing, or something else I have not isolated) still seems to matter. I do not have a minimal standalone repro yet, just this stronger diagnosis from the real app.I also sent the site link to the email found on your profile, [email protected].
kevmoo commentedon Sep 24, 2026
I'm throwing some tokens at this. Stay tuned...
kevmoo commentedon Sep 24, 2026
Thanks for the detailed report — the "exactly one
flt-platform-view" detail was the key clue. I was able to reproduce this on master (3.49.0-1.0.pre) with a small app.What's happening
With the Chrome flags you listed (
--use-gl=angle --use-angle=gl --enable-gpu-rasterization --ignore-gpu-blocklist) on a machine without a working native GL driver, Chrome's GPU process fails after the first WebGL context, and skwasm's own WebGL2 context creation fails. When that happens skwasm logs a singleconsole.logline —Failed to create context!— and then never finishes initializing its surface. Every frame waits on that forever:SelectionArea's internal context-menu view) still get theirflt-platform-viewinjected, because injection happens before the wait,accounts.google.com/gsi/buttonwas made,This also explains why your isolated repros "worked": if the button is in the first frame, its container is attached (so an iframe-count check passes), even though nothing is visibly rendered.
--disable-gpuworks because Chrome then uses SwiftShader for WebGL2, which succeeds.CanvasKit handles the same failure by falling back to CPU rendering (
WARNING: Falling back to CPU-only rendering. Reason: failed to create GrContext.); skwasm has no such fallback.Questions
Failed to create context!? (It's a plain log line, not an error.)chrome://gpufrom one of those machines (especially the WebGL2 / GPU process lines)?If the answer to (1) is no, the production failure is likely a different issue and we should track it separately.
Fix in progress: #193293 (draft) makes skwasm report the context-creation failure and surface a clear error instead of hanging silently. Note this turns the silent hang into a visible error; it doesn't make rendering work when WebGL2 is unavailable.
0danylo commentedon Sep 24, 2026
answer to (1) is no, there's basically nothing in console:
Unchecked runtime.lastError: The message port closed before a response was received.
flutter_bootstrap.js:2 Flutter Web: Skwasm uses multi-threading and web workers for better performance, but your page needs to be cross-origin isolated to support multi-threading. Skwasm will run in single-threaded mode.
To enable multithreading, serve your app with these HTTP response headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
See https://web.dev/articles/coop-coep for guidance.
To silence this warning, set
suppressMultithreadingWarning: truein your Flutter configuration.kevmoo commentedon Sep 24, 2026
Thanks! That rules out the WebGL context failure for your production case. #193293 still fixes the headless-with-GPU-flags hang, but what your users hit is something else. A few things would help narrow it down:
console.debug, which Chrome hides by default. Please paste anything that shows up.BackdropFilter,ImageFiltered,Opacity/AnimatedOpacity,Transform/scale animations,ClipRect/ClipRRect, or custom page transitions between the route and theHtmlElementView. Also, does the button's parent give it a non-zero size?Also useful: the exact Flutter version you build production with (
flutter --version), and whether the same build on CanvasKit (flutter build webwithout--wasm) works on the same machines.0danylo commentedon Sep 24, 2026
Unchecked runtime.lastError: The message port closed before a response was received.
flutter_bootstrap.js:2 Flutter Web: Skwasm uses multi-threading and web workers for better performance, but your page needs to be cross-origin isolated to support multi-threading. Skwasm will run in single-threaded mode.
To enable multithreading, serve your app with these HTTP response headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
See https://web.dev/articles/coop-coep for guidance.
To silence this warning, set
suppressMultithreadingWarning: truein your Flutter configuration.U @ flutter_bootstrap.js:2
await in U
load @ flutter_bootstrap.js:35
await in load
(anonymous) @ flutter_bootstrap.js:42
main.dart.mjs:817 TrustedTypes available. Creating policy: gis-dart
main.dart.mjs:277 [Violation] 'setTimeout' handler took 70ms
main.dart.mjs:277 [Violation] 'setTimeout' handler took 121ms
5[Violation] 'requestAnimationFrame' handler took ms
skwasm.js:32 [Violation] 'requestAnimationFrame' handler took 72ms
skwasm.js:32 [Violation] 'requestAnimationFrame' handler took 162ms
main.dart.mjs:295 [Violation] 'requestAnimationFrame' handler took 98ms
main.dart.mjs:295 [Violation] 'requestAnimationFrame' handler took 55ms
skwasm.js:32 [Violation] 'requestAnimationFrame' handler took 51ms
yes, no other problems on the page, including a scrolling animation
FocusTraversalGroup → MaterialApp.router → builder: Overlay → OverlayEntry → OpineSelectionArea → (desktop) SelectionArea → GoRouter's Navigator → SignInScreen → Scaffold → SafeArea → LayoutBuilder →
SingleChildScrollView → ConstrainedBox → Padding → Align → ConstrainedBox(maxWidth:640) → Column →
_SignInButtons → Column → Center → SizedBox(height:44, width:340) → _GoogleSignInButton → Stack →
HtmlElementView
BackdropFilter/ImageFiltered: none anywhere in this chain.
Opacity/AnimatedOpacity/Transform: no true ancestor has one. There is an AnimatedOpacity in _GoogleSignInButton.build(), but it's a sibling in the Stack, not a wrapper, it's Google's own white pill placeholder that fades from opaque to opacity: 0 once _rendered flips true. Since gis.id.renderButton() never throws even when the platform view failed to attach (confirmed earlier this session), _rendered gets set regardless of whether anything actually painted, so this fade-out completing is not evidence the button worked, it just means the code thinks it did. No page-transition Transform/Fade either: app_router.dart has no custom pageTransitionsTheme or CustomTransitionPage, so it's Flutter's default, which only wraps the page during the ~300ms navigation animation, not persistently.
ClipRect/ClipRRect: SingleChildScrollView defaults to Clip.hardEdge, and Stack's default clipBehavior is also Clip.hardEdge since Flutter 3.3ish. Neither should clip the button away since it sits fully inside both, but they're real clips in the chain, worth naming since you asked for a complete list.
Non-zero parent size: yes, explicit SizedBox(height: 44, width: 340) wrapped in Center, confirmed hardcoded, not computed from intrinsic content.
The one real finding: SelectionArea genuinely sits as an ancestor of everything, including this button, not a sibling. And SelectionArea's own internal PlatformSelectableRegionContextMenu is itself a Stack over its own HtmlElementView, per the doc comment in opine_selection_area.dart:74-84. So the tree literally contains two platform views in an ancestor/descendant relationship: SelectionArea's own platform view wrapping the whole page, and the sign-in button's platform view nested inside it. That matches our earlier DOM finding (only one flt-platform-view slot ever attaches) more precisely than "sibling" did, worth telling him it's a true ancestor relationship, not two independent views.
Also: Flutter version: Flutter 3.47.1 • channel stable, revision 6655482, engine hash
11d79658c444477b06513d32b52c8c4ccb7276b0, Dart 3.13.1. It works normally without WASM. on the broken site, document.querySelectorAll('[id*=gsi]') returns nothing at all, not hidden, not zero-size, just absent from the DOM. The registerViewFactory that creates that div is never invoked.
kevmoo commentedon Sep 24, 2026
Thank you, this is really helpful, and it rules out several theories.
One detail I'd like to pin down with actual logging, since it changes where we look. You mentioned the
registerViewFactorycallback is never invoked, but also thatrenderButton()runs and_renderedflips to true.renderButton()needs the element that the factory returns. Also,document.querySelectorAll(...)only finds elements that are attached to the document. So "factory never ran" and "factory ran, but its div was never attached to the page" look the same from DevTools.Could you add some temporary logging along these lines? Plain
printis fine: it goes to the browser console in release--wasmbuilds too.On a working build (I checked this snippet locally with skwasm), you get all four lines and
isConnected=true. What I'd like to know from the broken site:[gsi-debug]lines appear at all.isConnectedistrueorfalse.document.querySelectorAll('flt-platform-view').lengthreturns after navigating to the sign-in screen.Two more questions about production:
--wasm, or a subset (e.g. certain GPUs, Linux, VMs/remote desktops)?Thanks again for sticking with this!
0danylo commentedon Sep 24, 2026
Of course, I'd also like to get this figured out.
I ran the instrumented build on staging. None of the four debug lines showed up, not even registered $viewType, which is the first line and runs synchronously in initState() before any async work starts. No exceptions either, checked Runtime.exceptionThrown and Log.entryAdded at every level, nothing beyond the usual skwasm single-threaded warning. So it's not that the factory ran and the div never got attached. initState() for that widget never runs at all, and nothing throws to explain why.
DOM state after navigating to the sign-in screen: flt-platform-view count is 1 (SelectionArea's own), zero elements match [id*=gsi].
For your questions:
New stuff: same GPU vendor (Nvidia) on both machines, same desktop layout and SelectionArea wrapping on both, and Firefox works while Brave doesn't. That seems like something Brave desktop specific rather than GPU/skwasm in general. Shields being down and trying a guest profile (all defaults) didn't fix it, so it's probably not ad/tracker blocking or extensions. Brave also randomizes WebGL/canvas output by default as anti-fingerprinting ("farbling"), separate from Shields, and I don't know if that's disabled by the same per-site Shields toggle or not. If skwasm's WebGL2 context setup is sensitive to that, it could explain a browser-specific failure.
kevmoo commentedon Sep 24, 2026
Thanks, this narrows it down a lot.
If
initState()for the button widget never runs, the engine's platform-view code isn't involved yet. The engine only gets involved once the widget is built and the framework sends thecreatemessage. So something above_GoogleSignInButtonis not building it on Brave desktop. The rest of the UI renders and animates, so skwasm's WebGL context is working there. Brave's WebGL/canvas farbling changes the values pages read back; it shouldn't prevent a widget from being built.A few things would help us tell whether this is Chromium-wide, Brave-specific, or specific to
--wasm:Plain Google Chrome on one of the failing machines (same
--wasmbuild): does it work?The same app built without
--wasm, opened in Brave desktop: does it work? (Earlier "works without WASM" — was that in Brave?)One level up. Please move the
prints to the parent that decides whether to show the Google button (_SignInButtons.build, plus whatever condition,FutureBuilderor state flag gates_GoogleSignInButton). If the button waits on aFuture, e.g. GIS SDK loading (loadWebSdk()/GoogleSignIn.instance.initialize()), please print before and after thatawait:The GIS loader completes when the SDK script calls
window.onGoogleLibraryLoad. If that script is blocked or never finishes loading, theFuturenever completes and nothing throws, which matches what you're seeing.In Brave's Network tab: what's the status of the request to
https://accounts.google.com/gsi/client? And in the console, what doestypeof google?.accounts?.idreturn on the sign-in screen?0danylo commentedon Sep 24, 2026
Moved the prints up as you asked, redeployed, confirmed the live bundle's main.dart.wasm md5 matches the local build so this isn't stale.
_SignInButtons.build fires, kIsWeb=true, so it enters the branch that should build the button. buildGoogleSignInButton called never prints. The call inside that branch never happens. initState() still never runs either. No exception anywhere, checked Runtime.exceptionThrown and Log.entryAdded at every level again. The GIS SDK itself loads fine independently of our button: the request to accounts.google.com/gsi/client returns 200, and typeof google.accounts.id is "object" on the sign-in screen.
For your other questions, tested directly on the two failing machines:
So it's not Brave. Firefox works on both machines on the --wasm build, and Chrome, Edge, and Brave all fail on it, on the same hardware. Brave without --wasm works. This looks like Chromium/Blink plus --wasm, not any one browser's features.
kevmoo commentedon Sep 25, 2026
Thanks, that's the key data point. I think I know what's going on.
Firefox doesn't actually run the wasm build. Flutter's loader only enables dart2wasm/skwasm on Chromium browsers by default (
defaultWasmSupportisblink: true,gecko: false,webkit: falseinflutter.js). Firefox and iOS browsers silently fall back to themain.dart.jsbuild thatflutter build web --wasmalso produces. So the pattern isn't "Chromium vs. Firefox". It's wasm fails, JS works, everywhere."Branch entered, but the call never happens, and no exception" points to a conditional import. Under dart2wasm,
dart:html,dart:jsanddart:js_utildon't exist, so conditional imports keyed on them pick the stub file. I checked this locally with the same four conditions in one app:--wasm(skwasm)dart.library.htmldart.library.jsdart.library.js_utildart.library.js_interopIf
buildGoogleSignInButtoncomes from something likethen the wasm build calls the stub version. The stub typically returns
SizedBox.shrink()/Container(), and it doesn't contain yourprint. That would look exactly like "the call never happens": no platform view, no exception, andinitState()never runs.Could you check how
buildGoogleSignInButton(and anything else web-specific) is imported? If it's keyed ondart.library.html,dart.library.js, ordart.library.js_util, switching the condition todart.library.js_interopshould fix it. A quick way to confirm first is aprintinside the stub's version of the function.(The skwasm hang I found earlier with the headless GPU flags is a real, separate bug; #193293 still addresses that.)
0danylo commentedon Sep 27, 2026
That's it!! Checked the actual import:
export 'stub.dart' if (dart.library.js_util) 'web.dart';
stub.dart returns const SizedBox.shrink();, matches your prediction.
Grepped the rest of the codebase for the same pattern and found six more files with the identical if (dart.library.js_util) condition, covering back/forward-cache reload, user agent detection, a tab title badge, referrer tracking, a pending-vote-through-sign-in mechanism, and a reload-on-update prompt. All seven pick their non-web stub under --wasm, none of them error. Fixing the condition to dart.library.js_interop across all seven fixes the problem and I can use WASM now.
Appreciate you sticking with this. Thanks!
github-actions commentedon Oct 11, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.[web] Report skwasm WebGL context creation failure instead of hanging (…