Skip to content

Wait for web rendering before first-frame event - #189500

Merged
auto-submit[bot] merged 8 commits into
flutter:masterfrom
Yusufihsangorgel:fix/web-first-frame-presentation
Aug 11, 2026
Merged

auto-submit[bot] merged 8 commits into
flutter:masterfrom
Yusufihsangorgel:fix/web-first-frame-presentation

Conversation

@Yusufihsangorgel

@Yusufihsangorgel Yusufihsangorgel commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

The flutter-first-frame browser event currently fires as soon as the
framework sends the flutter/service_worker platform message. Web scene
rendering is asynchronous, so the event can remove an HTML splash while
SkWasm is still rasterizing and briefly expose a blank frame.

This change tracks scene renders that are still in progress, waits for them,
and dispatches the event on the next browser animation frame. The tests cover
both the asynchronous browser-event contract and an explicitly gated scene
render, so the latter proves that the event cannot overtake rasterization.

Fixes #189499.

Validation

  • felt test --gcs-prod --browser chrome test/engine/window_test.dart test/ui/async_rendering_test.dart
    passed in all seven selected suites:
    • dart2js / CanvasKit (Chromium and full variants)
    • dart2wasm / CanvasKit
    • dart2wasm / SkWasm (threaded and forced single-threaded)
    • dart2wasm / Wimp
  • dart analyze reported no issues for the implementation and both test files.
  • A minimal SkWasm reproduction exposed a blank compositor frame after the
    event in 3/3 cold runs before this change. The first visible Flutter frame
    followed the event by 90.3–143.0 ms.

Pre-launch Checklist

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

@github-actions github-actions Bot added engine flutter/engine related. See also e: labels. platform-web Web applications specifically team-web Owned by Web platform team labels Jul 15, 2026
@Yusufihsangorgel
Yusufihsangorgel marked this pull request as ready for review July 15, 2026 14:21

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request ensures that the flutter-first-frame browser event waits for any in-progress scene renders to complete before being dispatched by tracking pending renders in _sceneRendersInProgress. Feedback recommends updating the documentation comment for _sceneRendersInProgress to use /// instead of // to comply with the repository style guide.

Comment thread engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart Outdated

@harryterkelsen harryterkelsen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this fix! Please take a look at my comment


final Arena frameArena = Arena();

/// The framework reports its first frame from a post-frame callback, while

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a Set<Future<void>> is overkill for what we need here. We really just want to be able to answer the question "has the first batch of renders completed yet?"

What if we just have a Completer<void>? _firstFrameCompleter (and a counter since in multi-view apps we need to wait for multiple render requests to finish) which completes after the first frame has finished rendering? It could look something like this:

  int _pendingFirstFrameRenders = 0;
  Completer<void>? _firstFrameCompleter = Completer<void>();

  // ... inside render():
    if (shouldRender) {
      final Future<void> sceneRender = renderer.renderScene(scene, target);
      
      if (_firstFrameCompleter != null) {
        _pendingFirstFrameRenders++;
        
        // If a render fails, immediately fail the completer.
        sceneRender.catchError((Object error, StackTrace stack) {
          if (_firstFrameCompleter != null && !_firstFrameCompleter!.isCompleted) {
            _firstFrameCompleter!.completeError(error, stack);
          }
        });
        
        sceneRender.whenComplete(() {
          _pendingFirstFrameRenders--;
          // When all views in the first frame have finished rendering...
          if (_pendingFirstFrameRenders == 0 && _firstFrameCompleter != null) {
            if (!_firstFrameCompleter!.isCompleted) {
              _firstFrameCompleter!.complete();
            }
            // Null it out to completely disable tracking for all future frames.
            _firstFrameCompleter = null;
          }
        });
      }

      await sceneRender;
    }

and then in _dispatchFirstFrameEventAfterRender:

  Future<void> _dispatchFirstFrameEventAfterRender() async {
    try {
      if (_firstFrameCompleter != null) {
        await _firstFrameCompleter!.future;
      }
    } catch (_) {
      // Rendering owns error reporting. A failed render is not a first frame.
      return;
    }
    
    domWindow.requestAnimationFrame((_) {
      domWindow.dispatchEvent(createDomEvent('Event', 'flutter-first-frame'));
    });
  }

This way, we remove the overhead of adding and removing the render Futures to a Set on every frame for the entire app lifetime, and instead only track the thing we actually care about ("has the first frame rendered yet?")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented, with one deviation: completion also happens at the end of the first invokeOnDrawFrame, not only when a render finishes.

_firstFrameCompleter != null as the only guard breaks two ways, both reproduced in a VM harness copied from renderScene. Renders that outlast the frame interval keep the count above zero: the count oscillates between 1 and 2 for ten frames and never reaches 0, because later frames keep feeding it. A first frame that renders nothing never completes, so anything hiding a loading overlay on flutter-first-frame stays up. Master sent the event unconditionally.

So the guard is a bool _isBuildingFirstFrame, flipped where frameArena.collect() already means the frame is done. Happy to paste the harness runs if useful.

The Set of pending futures becomes a Completer plus a counter, as asked in
review. Completion also has to happen at the end of the first
invokeOnDrawFrame: a startup animation whose renders outlast the frame
interval never lets the count reach zero, and a first frame that renders
nothing would otherwise leave the completer pending, which regresses against
both master and the Set version for an app that hides its loading screen on
the browser event.

@harryterkelsen harryterkelsen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a couple small comments about the tests, but otherwise I think this looks ready to land! Thanks for your contribution

final Zone innerZone = Zone.current.fork();
test('dispatches browser event asynchronously on flutter/service_worker channel', () async {
// Each dispatcher tracks its own first frame, and only the first one. The
// singleton's is already over by now, because 'onDrawFrame preserves the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: just say "previous tests" instead of naming the test above, otherwise this could quickly get out of date

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, switched to "previous tests".

Comment thread engine/src/flutter/lib/web_ui/test/ui/async_rendering_test.dart
Comment thread engine/src/flutter/lib/web_ui/test/ui/async_rendering_test.dart

@harryterkelsen harryterkelsen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@harryterkelsen harryterkelsen added the CICD Run CI/CD label Jul 31, 2026
@flutter-zl flutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label Jul 31, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Jul 31, 2026
@auto-submit

auto-submit Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/189500, because - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label.

@Yusufihsangorgel

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews, and for merging master in.

@harryterkelsen this is blocked on Google testing now. Could you add it to the Google testing queue? It is not in the project yet.

One thing I found while looking at it. flutter-first-frame used to dispatch straight from the flutter/service_worker message, which arrives in a post-frame callback of the warm-up frame. It now waits on a completer that cannot be released until invokeOnDrawFrame runs, which the warm-up path never reaches, and then on one more requestAnimationFrame. Both of those come off the engine's frame loop, so the event needs a rendering opportunity where it needed none before. On a visible boot a real frame follows and it should still arrive, just later. My tests call invokeOnDrawFrame directly, so they would not show that either way.

I am digging into the warm-up path.

@Yusufihsangorgel

Copy link
Copy Markdown
Contributor Author

Found it, I think. AutomatedTestWidgetsFlutterBinding overrides ensureFrameCallbacksRegistered to leave onDrawFrame null on purpose, and pumps frames by calling handleDrawFrame directly. _renderFrame skips invokeOnDrawFrame at that null check, and that is the only place _isBuildingFirstFrame is cleared, so under flutter test on web the completer never completes and the event never fires. The flutter/service_worker message still arrives, since post-frame callbacks run either way.

That is source reading rather than a run, since exercising it needs an engine build. If it holds, the release cannot hang off the engine's frame loop, and I would rather have your read on the shape than guess at one.

@flutter-zl flutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 5, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 5, 2026
@auto-submit

auto-submit Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/189500, because - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label.

@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Aug 5, 2026
@Yusufihsangorgel

Copy link
Copy Markdown
Contributor Author

@harryterkelsen Google testing is the only required check still red. It reports Engine artifacts not found on the current run, and the run before it, on Jul 31, got as far as Smoke tests failed. The only thing that landed between those two runs is the Aug 5 master merge, which is also when CICD came off. Can you re-apply it? I still don't know what the Jul 31 run hit and can't open go/frob_pr from outside.

@harryterkelsen harryterkelsen added the CICD Run CI/CD label Aug 10, 2026
@harryterkelsen

Copy link
Copy Markdown
Contributor

@Yusufihsangorgel Sorry, I forgot to check in on this PR. I updated the branch and re-applied the CICD label. Hopefully the tests deflake this time and we can get this landed. Thanks for your patience

@harryterkelsen harryterkelsen added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 10, 2026
@Yusufihsangorgel

Copy link
Copy Markdown
Contributor Author

Thanks for re-applying it. I'll keep an eye on the run.

@auto-submit

auto-submit Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/189500, because - The status or check suite Mac_arm64_verify_binaries has failed. Please fix the issues identified (or deflake) before re-applying this label.

@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 11, 2026
@Yusufihsangorgel

Copy link
Copy Markdown
Contributor Author

Mac_arm64_verify_binaries failed on a 404 for sky_engine.zip at 00:45:45, 41 seconds after the engine build reported all three targets green. That URL serves 200 now. A re-run should clear it, and verify_binaries_pre_codesigned was skipped, so nothing was actually checked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-web Web applications specifically team-web Owned by Web platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[web] flutter-first-frame fires before the first SkWasm frame is presented

3 participants