[flutter_tools] register service worker after first frame event (#66082)

Registering the service worker immediately after the documented has loaded may cause SW initialization to compete with framework initialization. It was recommended to us that we defer the service worker setup until after the framework is done with setup, which should be sometime after the first frame.

To implement this, I augmented the binding setup to dispatch an event on the document after the binding has initialized. I don't see any obvious risks with this setup.

Fixes #66066
diff --git a/dev/integration_tests/flutter_gallery/web/index.html b/dev/integration_tests/flutter_gallery/web/index.html
index 23bcf28..3bc6123 100644
--- a/dev/integration_tests/flutter_gallery/web/index.html
+++ b/dev/integration_tests/flutter_gallery/web/index.html
@@ -24,7 +24,7 @@
   https://developers.google.com/web/fundamentals/primers/service-workers -->
   <script>
     if ('serviceWorker' in navigator) {
-      window.addEventListener('load', function () {
+      window.addEventListener('flutter-first-frame', function () {
         navigator.serviceWorker.register('flutter_service_worker.js');
       });
     }
diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart
index 3b07848..e170435 100644
--- a/packages/flutter/lib/src/rendering/binding.dart
+++ b/packages/flutter/lib/src/rendering/binding.dart
@@ -44,6 +44,9 @@
     assert(renderView != null);
     addPersistentFrameCallback(_handlePersistentFrameCallback);
     initMouseTracker();
+    if (kIsWeb) {
+      addPostFrameCallback(_handleWebFirstFrame);
+    }
   }
 
   /// The current [RendererBinding], if one has been created.
@@ -281,6 +284,12 @@
     }
   }
 
+  void _handleWebFirstFrame(Duration _) {
+    assert(kIsWeb);
+    const MethodChannel methodChannel = MethodChannel('flutter/service_worker');
+    methodChannel.invokeMethod<void>('first-frame');
+  }
+
   void _handleSemanticsAction(int id, SemanticsAction action, ByteData? args) {
     _pipelineOwner.semanticsOwner?.performAction(
       id,
diff --git a/packages/flutter/test/rendering/first_frame_test.dart b/packages/flutter/test/rendering/first_frame_test.dart
new file mode 100644
index 0000000..7e81473
--- /dev/null
+++ b/packages/flutter/test/rendering/first_frame_test.dart
@@ -0,0 +1,38 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// @dart = 2.8
+
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:flutter/services.dart';
+
+import '../flutter_test_alternative.dart';
+
+void main() {
+  test('Flutter dispatches first frame event on the web only', () async {
+    final Completer<void> completer = Completer<void>();
+    final TestRenderBinding binding = TestRenderBinding();
+    const MethodChannel firstFrameChannel = MethodChannel('flutter/service_worker');
+    firstFrameChannel.setMockMethodCallHandler((MethodCall methodCall) async {
+      completer.complete();
+    });
+
+    binding.handleBeginFrame(Duration.zero);
+    binding.handleDrawFrame();
+
+    await expectLater(completer.future, completes);
+  }, skip: !kIsWeb);
+}
+
+class TestRenderBinding extends BindingBase with SchedulerBinding, ServicesBinding, GestureBinding, SemanticsBinding, RendererBinding {
+  @override
+  void initInstances() {
+    super.initInstances();
+  }
+}
diff --git a/packages/flutter/test/rendering/mouse_tracking_test_utils.dart b/packages/flutter/test/rendering/mouse_tracking_test_utils.dart
index 650272c..423f36b 100644
--- a/packages/flutter/test/rendering/mouse_tracking_test_utils.dart
+++ b/packages/flutter/test/rendering/mouse_tracking_test_utils.dart
@@ -56,7 +56,7 @@
     _overridePhase = lastPhase;
   }
 
-  List<void Function(Duration)> postFrameCallbacks;
+  List<void Function(Duration)> postFrameCallbacks = <void Function(Duration)>[];
 
   // Proxy post-frame callbacks.
   @override
diff --git a/packages/flutter_tools/templates/app/web/index.html.tmpl b/packages/flutter_tools/templates/app/web/index.html.tmpl
index 0626c34..bc51a4a 100644
--- a/packages/flutter_tools/templates/app/web/index.html.tmpl
+++ b/packages/flutter_tools/templates/app/web/index.html.tmpl
@@ -23,7 +23,7 @@
        https://developers.google.com/web/fundamentals/primers/service-workers -->
   <script>
     if ('serviceWorker' in navigator) {
-      window.addEventListener('load', function () {
+      window.addEventListener('flutter-first-frame', function () {
         navigator.serviceWorker.register('flutter_service_worker.js');
       });
     }