Refactor flutter.js to do dart2wasm bootstrapping and CanvasKit/Skwasm preloading. (#49037)

This PR makes some major revisions to our flutter.js bootstrapper.
* Modularize flutter.js into multiple files to make it a little simpler to manage from source code. They are still bundled into a single .js file by esbuild.
* Added a `types.d.ts` file which contains declarations of the types of some of the objects used in the flutter.js API
* Deprecated the old `FlutterLoader.loadEntrypoint` API and added a new function simply called `FlutterLoader.load`, which has a few more capabilities:
  -  A build tool can inject a build config, that may describe multiple builds that `FlutterLoader.load` can attempt to use. It will use the first one that is compatible with the browser environment and the user's configuration.
  - It can also load wasm flutter apps.
  - It also pre-loads and instantiates CanvasKit (and Skwasm) as necessary depending on the build configuration.
  - `FlutterLoader.load` also immediately takes a flutter configuration object. If an `onEntrypointLoaded` callback is not provided by the user, it just does the expected thing and initializes the engine and immediately starts the app, passing the configuration along as needed.
 * `flutter.js` has the engine hash built into it now, which allows it to ascertain the correct CDN URLs for both CanvasKit and Skwasm.
diff --git a/ci/builders/linux_web_engine.json b/ci/builders/linux_web_engine.json
index 9dc023b..138e425 100644
--- a/ci/builders/linux_web_engine.json
+++ b/ci/builders/linux_web_engine.json
@@ -179,28 +179,6 @@
       }
     },
     {
-      "name": "web_tests/test_bundles/dart2js-skwasm-skwasm_stub",
-      "drone_dimensions": [
-        "device_type=none",
-        "os=Linux"
-      ],
-      "generators": {
-        "tasks": [
-          {
-            "name": "compile bundle dart2js-skwasm-skwasm_stub",
-            "parameters": [
-              "test",
-              "--compile",
-              "--bundle=dart2js-skwasm-skwasm_stub"
-            ],
-            "scripts": [
-              "flutter/lib/web_ui/dev/felt"
-            ]
-          }
-        ]
-      }
-    },
-    {
       "name": "web_tests/test_bundles/dart2wasm-html-engine",
       "drone_dimensions": [
         "device_type=none",
@@ -331,6 +309,28 @@
           }
         ]
       }
+    },
+    {
+      "name": "web_tests/test_bundles/fallbacks",
+      "drone_dimensions": [
+        "device_type=none",
+        "os=Linux"
+      ],
+      "generators": {
+        "tasks": [
+          {
+            "name": "compile bundle fallbacks",
+            "parameters": [
+              "test",
+              "--compile",
+              "--bundle=fallbacks"
+            ],
+            "scripts": [
+              "flutter/lib/web_ui/dev/felt"
+            ]
+          }
+        ]
+      }
     }
   ],
   "tests": [
@@ -515,42 +515,6 @@
       ]
     },
     {
-      "name": "Linux run chrome-dart2js-skwasm-skwasm_stub suite",
-      "recipe": "engine_v2/tester_engine",
-      "drone_dimensions": [
-        "device_type=none",
-        "os=Linux"
-      ],
-      "gclient_variables": {
-        "download_android_deps": false
-      },
-      "dependencies": [
-        "web_tests/artifacts",
-        "web_tests/test_bundles/dart2js-skwasm-skwasm_stub"
-      ],
-      "test_dependencies": [
-        {
-          "dependency": "goldctl",
-          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
-        },
-        {
-          "dependency": "chrome_and_driver",
-          "version": "119.0.6045.9"
-        }
-      ],
-      "tasks": [
-        {
-          "name": "run suite chrome-dart2js-skwasm-skwasm_stub",
-          "parameters": [
-            "test",
-            "--run",
-            "--suite=chrome-dart2js-skwasm-skwasm_stub"
-          ],
-          "script": "flutter/lib/web_ui/dev/felt"
-        }
-      ]
-    },
-    {
       "name": "Linux run chrome-full-dart2js-canvaskit-canvaskit suite",
       "recipe": "engine_v2/tester_engine",
       "drone_dimensions": [
@@ -1091,6 +1055,78 @@
       ]
     },
     {
+      "name": "Linux run chrome-fallbacks suite",
+      "recipe": "engine_v2/tester_engine",
+      "drone_dimensions": [
+        "device_type=none",
+        "os=Linux"
+      ],
+      "gclient_variables": {
+        "download_android_deps": false
+      },
+      "dependencies": [
+        "web_tests/artifacts",
+        "web_tests/test_bundles/fallbacks"
+      ],
+      "test_dependencies": [
+        {
+          "dependency": "goldctl",
+          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+        },
+        {
+          "dependency": "chrome_and_driver",
+          "version": "119.0.6045.9"
+        }
+      ],
+      "tasks": [
+        {
+          "name": "run suite chrome-fallbacks",
+          "parameters": [
+            "test",
+            "--run",
+            "--suite=chrome-fallbacks"
+          ],
+          "script": "flutter/lib/web_ui/dev/felt"
+        }
+      ]
+    },
+    {
+      "name": "Linux run firefox-fallbacks suite",
+      "recipe": "engine_v2/tester_engine",
+      "drone_dimensions": [
+        "device_type=none",
+        "os=Linux"
+      ],
+      "gclient_variables": {
+        "download_android_deps": false
+      },
+      "dependencies": [
+        "web_tests/artifacts",
+        "web_tests/test_bundles/fallbacks"
+      ],
+      "test_dependencies": [
+        {
+          "dependency": "goldctl",
+          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+        },
+        {
+          "dependency": "firefox",
+          "version": "version:106.0"
+        }
+      ],
+      "tasks": [
+        {
+          "name": "run suite firefox-fallbacks",
+          "parameters": [
+            "test",
+            "--run",
+            "--suite=firefox-fallbacks"
+          ],
+          "script": "flutter/lib/web_ui/dev/felt"
+        }
+      ]
+    },
+    {
       "name": "Mac run safari-dart2js-html-engine suite",
       "recipe": "engine_v2/tester_engine",
       "drone_dimensions": [
@@ -1256,6 +1292,39 @@
       ]
     },
     {
+      "name": "Mac run safari-fallbacks suite",
+      "recipe": "engine_v2/tester_engine",
+      "drone_dimensions": [
+        "device_type=none",
+        "os=Mac-13",
+        "cpu=arm64"
+      ],
+      "gclient_variables": {
+        "download_android_deps": false
+      },
+      "dependencies": [
+        "web_tests/artifacts",
+        "web_tests/test_bundles/fallbacks"
+      ],
+      "test_dependencies": [
+        {
+          "dependency": "goldctl",
+          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+        }
+      ],
+      "tasks": [
+        {
+          "name": "run suite safari-fallbacks",
+          "parameters": [
+            "test",
+            "--run",
+            "--suite=safari-fallbacks"
+          ],
+          "script": "flutter/lib/web_ui/dev/felt"
+        }
+      ]
+    },
+    {
       "name": "Windows run chrome-dart2js-html-engine suite",
       "recipe": "engine_v2/tester_engine",
       "drone_dimensions": [
@@ -1436,42 +1505,6 @@
       ]
     },
     {
-      "name": "Windows run chrome-dart2js-skwasm-skwasm_stub suite",
-      "recipe": "engine_v2/tester_engine",
-      "drone_dimensions": [
-        "device_type=none",
-        "os=Windows"
-      ],
-      "gclient_variables": {
-        "download_android_deps": false
-      },
-      "dependencies": [
-        "web_tests/artifacts",
-        "web_tests/test_bundles/dart2js-skwasm-skwasm_stub"
-      ],
-      "test_dependencies": [
-        {
-          "dependency": "goldctl",
-          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
-        },
-        {
-          "dependency": "chrome_and_driver",
-          "version": "119.0.6045.9"
-        }
-      ],
-      "tasks": [
-        {
-          "name": "run suite chrome-dart2js-skwasm-skwasm_stub",
-          "parameters": [
-            "test",
-            "--run",
-            "--suite=chrome-dart2js-skwasm-skwasm_stub"
-          ],
-          "script": "flutter/lib/web_ui/dev/felt"
-        }
-      ]
-    },
-    {
       "name": "Windows run chrome-full-dart2js-canvaskit-canvaskit suite",
       "recipe": "engine_v2/tester_engine",
       "drone_dimensions": [
@@ -1542,6 +1575,330 @@
           "script": "flutter/lib/web_ui/dev/felt"
         }
       ]
+    },
+    {
+      "name": "Windows run chrome-dart2wasm-html-engine suite",
+      "recipe": "engine_v2/tester_engine",
+      "drone_dimensions": [
+        "device_type=none",
+        "os=Windows"
+      ],
+      "gclient_variables": {
+        "download_android_deps": false
+      },
+      "dependencies": [
+        "web_tests/artifacts",
+        "web_tests/test_bundles/dart2wasm-html-engine"
+      ],
+      "test_dependencies": [
+        {
+          "dependency": "goldctl",
+          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+        },
+        {
+          "dependency": "chrome_and_driver",
+          "version": "119.0.6045.9"
+        }
+      ],
+      "tasks": [
+        {
+          "name": "run suite chrome-dart2wasm-html-engine",
+          "parameters": [
+            "test",
+            "--run",
+            "--suite=chrome-dart2wasm-html-engine"
+          ],
+          "script": "flutter/lib/web_ui/dev/felt"
+        }
+      ]
+    },
+    {
+      "name": "Windows run chrome-dart2wasm-html-html suite",
+      "recipe": "engine_v2/tester_engine",
+      "drone_dimensions": [
+        "device_type=none",
+        "os=Windows"
+      ],
+      "gclient_variables": {
+        "download_android_deps": false
+      },
+      "dependencies": [
+        "web_tests/artifacts",
+        "web_tests/test_bundles/dart2wasm-html-html"
+      ],
+      "test_dependencies": [
+        {
+          "dependency": "goldctl",
+          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+        },
+        {
+          "dependency": "chrome_and_driver",
+          "version": "119.0.6045.9"
+        }
+      ],
+      "tasks": [
+        {
+          "name": "run suite chrome-dart2wasm-html-html",
+          "parameters": [
+            "test",
+            "--run",
+            "--suite=chrome-dart2wasm-html-html"
+          ],
+          "script": "flutter/lib/web_ui/dev/felt"
+        }
+      ]
+    },
+    {
+      "name": "Windows run chrome-dart2wasm-html-ui suite",
+      "recipe": "engine_v2/tester_engine",
+      "drone_dimensions": [
+        "device_type=none",
+        "os=Windows"
+      ],
+      "gclient_variables": {
+        "download_android_deps": false
+      },
+      "dependencies": [
+        "web_tests/artifacts",
+        "web_tests/test_bundles/dart2wasm-html-ui"
+      ],
+      "test_dependencies": [
+        {
+          "dependency": "goldctl",
+          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+        },
+        {
+          "dependency": "chrome_and_driver",
+          "version": "119.0.6045.9"
+        }
+      ],
+      "tasks": [
+        {
+          "name": "run suite chrome-dart2wasm-html-ui",
+          "parameters": [
+            "test",
+            "--run",
+            "--suite=chrome-dart2wasm-html-ui"
+          ],
+          "script": "flutter/lib/web_ui/dev/felt"
+        }
+      ]
+    },
+    {
+      "name": "Windows run chrome-dart2wasm-canvaskit-canvaskit suite",
+      "recipe": "engine_v2/tester_engine",
+      "drone_dimensions": [
+        "device_type=none",
+        "os=Windows"
+      ],
+      "gclient_variables": {
+        "download_android_deps": false
+      },
+      "dependencies": [
+        "web_tests/artifacts",
+        "web_tests/test_bundles/dart2wasm-canvaskit-canvaskit"
+      ],
+      "test_dependencies": [
+        {
+          "dependency": "goldctl",
+          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+        },
+        {
+          "dependency": "chrome_and_driver",
+          "version": "119.0.6045.9"
+        }
+      ],
+      "tasks": [
+        {
+          "name": "run suite chrome-dart2wasm-canvaskit-canvaskit",
+          "parameters": [
+            "test",
+            "--run",
+            "--suite=chrome-dart2wasm-canvaskit-canvaskit"
+          ],
+          "script": "flutter/lib/web_ui/dev/felt"
+        }
+      ]
+    },
+    {
+      "name": "Windows run chrome-dart2wasm-canvaskit-ui suite",
+      "recipe": "engine_v2/tester_engine",
+      "drone_dimensions": [
+        "device_type=none",
+        "os=Windows"
+      ],
+      "gclient_variables": {
+        "download_android_deps": false
+      },
+      "dependencies": [
+        "web_tests/artifacts",
+        "web_tests/test_bundles/dart2wasm-canvaskit-ui"
+      ],
+      "test_dependencies": [
+        {
+          "dependency": "goldctl",
+          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+        },
+        {
+          "dependency": "chrome_and_driver",
+          "version": "119.0.6045.9"
+        }
+      ],
+      "tasks": [
+        {
+          "name": "run suite chrome-dart2wasm-canvaskit-ui",
+          "parameters": [
+            "test",
+            "--run",
+            "--suite=chrome-dart2wasm-canvaskit-ui"
+          ],
+          "script": "flutter/lib/web_ui/dev/felt"
+        }
+      ]
+    },
+    {
+      "name": "Windows run chrome-dart2wasm-skwasm-ui suite",
+      "recipe": "engine_v2/tester_engine",
+      "drone_dimensions": [
+        "device_type=none",
+        "os=Windows"
+      ],
+      "gclient_variables": {
+        "download_android_deps": false
+      },
+      "dependencies": [
+        "web_tests/artifacts",
+        "web_tests/test_bundles/dart2wasm-skwasm-ui"
+      ],
+      "test_dependencies": [
+        {
+          "dependency": "goldctl",
+          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+        },
+        {
+          "dependency": "chrome_and_driver",
+          "version": "119.0.6045.9"
+        }
+      ],
+      "tasks": [
+        {
+          "name": "run suite chrome-dart2wasm-skwasm-ui",
+          "parameters": [
+            "test",
+            "--run",
+            "--suite=chrome-dart2wasm-skwasm-ui"
+          ],
+          "script": "flutter/lib/web_ui/dev/felt"
+        }
+      ]
+    },
+    {
+      "name": "Windows run chrome-full-dart2wasm-canvaskit-canvaskit suite",
+      "recipe": "engine_v2/tester_engine",
+      "drone_dimensions": [
+        "device_type=none",
+        "os=Windows"
+      ],
+      "gclient_variables": {
+        "download_android_deps": false
+      },
+      "dependencies": [
+        "web_tests/artifacts",
+        "web_tests/test_bundles/dart2wasm-canvaskit-canvaskit"
+      ],
+      "test_dependencies": [
+        {
+          "dependency": "goldctl",
+          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+        },
+        {
+          "dependency": "chrome_and_driver",
+          "version": "119.0.6045.9"
+        }
+      ],
+      "tasks": [
+        {
+          "name": "run suite chrome-full-dart2wasm-canvaskit-canvaskit",
+          "parameters": [
+            "test",
+            "--run",
+            "--suite=chrome-full-dart2wasm-canvaskit-canvaskit"
+          ],
+          "script": "flutter/lib/web_ui/dev/felt"
+        }
+      ]
+    },
+    {
+      "name": "Windows run chrome-full-dart2wasm-canvaskit-ui suite",
+      "recipe": "engine_v2/tester_engine",
+      "drone_dimensions": [
+        "device_type=none",
+        "os=Windows"
+      ],
+      "gclient_variables": {
+        "download_android_deps": false
+      },
+      "dependencies": [
+        "web_tests/artifacts",
+        "web_tests/test_bundles/dart2wasm-canvaskit-ui"
+      ],
+      "test_dependencies": [
+        {
+          "dependency": "goldctl",
+          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+        },
+        {
+          "dependency": "chrome_and_driver",
+          "version": "119.0.6045.9"
+        }
+      ],
+      "tasks": [
+        {
+          "name": "run suite chrome-full-dart2wasm-canvaskit-ui",
+          "parameters": [
+            "test",
+            "--run",
+            "--suite=chrome-full-dart2wasm-canvaskit-ui"
+          ],
+          "script": "flutter/lib/web_ui/dev/felt"
+        }
+      ]
+    },
+    {
+      "name": "Windows run chrome-fallbacks suite",
+      "recipe": "engine_v2/tester_engine",
+      "drone_dimensions": [
+        "device_type=none",
+        "os=Windows"
+      ],
+      "gclient_variables": {
+        "download_android_deps": false
+      },
+      "dependencies": [
+        "web_tests/artifacts",
+        "web_tests/test_bundles/fallbacks"
+      ],
+      "test_dependencies": [
+        {
+          "dependency": "goldctl",
+          "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd"
+        },
+        {
+          "dependency": "chrome_and_driver",
+          "version": "119.0.6045.9"
+        }
+      ],
+      "tasks": [
+        {
+          "name": "run suite chrome-fallbacks",
+          "parameters": [
+            "test",
+            "--run",
+            "--suite=chrome-fallbacks"
+          ],
+          "script": "flutter/lib/web_ui/dev/felt"
+        }
+      ]
     }
   ]
 }
\ No newline at end of file
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 7a9bb3f..8ab8045 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -5877,7 +5877,16 @@
 ORIGIN: ../../../flutter/lib/ui/window/pointer_data_packet_converter.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/ui/window/viewport_metrics.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/ui/window/viewport_metrics.h + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/base_uri.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/browser_environment.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/canvaskit_loader.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/entrypoint_loader.js + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/flutter.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/instantiate_wasm.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/loader.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/service_worker_loader.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/skwasm_loader.js + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/trusted_types.js + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/lib/annotations.dart + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/lib/canvas.dart + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/lib/channel_buffers.dart + ../../../flutter/LICENSE
@@ -8700,7 +8709,17 @@
 FILE: ../../../flutter/lib/ui/window/pointer_data_packet_converter.h
 FILE: ../../../flutter/lib/ui/window/viewport_metrics.cc
 FILE: ../../../flutter/lib/ui/window/viewport_metrics.h
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/base_uri.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/browser_environment.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/canvaskit_loader.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/entrypoint_loader.js
 FILE: ../../../flutter/lib/web_ui/flutter_js/src/flutter.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/instantiate_wasm.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/loader.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/service_worker_loader.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/skwasm_loader.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/trusted_types.js
+FILE: ../../../flutter/lib/web_ui/flutter_js/src/types.d.ts
 FILE: ../../../flutter/lib/web_ui/lib/annotations.dart
 FILE: ../../../flutter/lib/web_ui/lib/canvas.dart
 FILE: ../../../flutter/lib/web_ui/lib/channel_buffers.dart
diff --git a/lib/web_ui/dev/chrome.dart b/lib/web_ui/dev/chrome.dart
index 374d4d6..b209ddf 100644
--- a/lib/web_ui/dev/chrome.dart
+++ b/lib/web_ui/dev/chrome.dart
@@ -23,14 +23,11 @@
 /// Provides an environment for desktop Chrome.
 class ChromeEnvironment implements BrowserEnvironment {
   ChromeEnvironment({
-    required bool enableWasmGC,
     required bool useDwarf,
-  }) : _enableWasmGC = enableWasmGC,
-       _useDwarf = useDwarf;
+  }) : _useDwarf = useDwarf;
 
   late final BrowserInstallation _installation;
 
-  final bool _enableWasmGC;
   final bool _useDwarf;
 
   @override
@@ -42,7 +39,6 @@
       url,
       _installation,
       debug: debug,
-      enableWasmGC: _enableWasmGC,
       useDwarf: _useDwarf
     );
   }
@@ -83,7 +79,6 @@
     Uri url,
     BrowserInstallation installation, {
     required bool debug,
-    required bool enableWasmGC,
     required bool useDwarf,
   }) {
     final Completer<Uri> remoteDebuggerCompleter = Completer<Uri>.sync();
@@ -101,13 +96,7 @@
       final bool isChromeNoSandbox =
           Platform.environment['CHROME_NO_SANDBOX'] == 'true';
       final String dir = await generateUserDirectory(installation, useDwarf);
-      final String jsFlags = enableWasmGC ? <String>[
-        '--experimental-wasm-gc',
-        '--experimental-wasm-stack-switching',
-        '--experimental-wasm-type-reflection',
-      ].join(' ') : '';
       final List<String> args = <String>[
-        if (jsFlags.isNotEmpty) '--js-flags=$jsFlags',
         '--user-data-dir=$dir',
         url.toString(),
         if (!debug)
diff --git a/lib/web_ui/dev/common.dart b/lib/web_ui/dev/common.dart
index 301b9bc..0eabc3b 100644
--- a/lib/web_ui/dev/common.dart
+++ b/lib/web_ui/dev/common.dart
@@ -240,12 +240,11 @@
 /// The [browserName] matches the browser name passed as the `--browser` option.
 BrowserEnvironment getBrowserEnvironment(
   BrowserName browserName, {
-  required bool enableWasmGC,
   required bool useDwarf,
 }) {
   switch (browserName) {
     case BrowserName.chrome:
-      return ChromeEnvironment(enableWasmGC: enableWasmGC, useDwarf: useDwarf);
+      return ChromeEnvironment(useDwarf: useDwarf);
     case BrowserName.edge:
       return EdgeEnvironment();
     case BrowserName.firefox:
diff --git a/lib/web_ui/dev/felt_config.dart b/lib/web_ui/dev/felt_config.dart
index 6e42e50..31f8739 100644
--- a/lib/web_ui/dev/felt_config.dart
+++ b/lib/web_ui/dev/felt_config.dart
@@ -32,11 +32,11 @@
 }
 
 class TestBundle {
-  TestBundle(this.name, this.testSet, this.compileConfig);
+  TestBundle(this.name, this.testSet, this.compileConfigs);
 
   final String name;
   final TestSet testSet;
-  final CompileConfiguration compileConfig;
+  final List<CompileConfiguration> compileConfigs;
 }
 
 enum CanvasKitVariant {
@@ -157,12 +157,16 @@
       if (testSet == null) {
         throw AssertionError('Test set not found with name: `$testSetName` (referenced by test bundle: `$name`)');
       }
-      final String compileConfigName = testBundleYaml['compile-config'] as String;
-      final CompileConfiguration? compileConfig = compileConfigsByName[compileConfigName];
-      if (compileConfig == null) {
-        throw AssertionError('Compile config not found with name: `$compileConfigName` (referenced by test bundle: `$name`)');
+      final dynamic compileConfigsValue = testBundleYaml['compile-configs'];
+      final List<CompileConfiguration> compileConfigs;
+      if (compileConfigsValue is String) {
+        compileConfigs = <CompileConfiguration>[compileConfigsByName[compileConfigsValue]!];
+      } else {
+        compileConfigs = (compileConfigsValue as List<dynamic>).map(
+          (dynamic configName) => compileConfigsByName[configName as String]!
+        ).toList();
       }
-      final TestBundle bundle = TestBundle(name, testSet, compileConfig);
+      final TestBundle bundle = TestBundle(name, testSet, compileConfigs);
       testBundles.add(bundle);
       if (testBundlesByName.containsKey(name)) {
         throw AssertionError('Duplicate test bundle name: $name');
@@ -202,11 +206,6 @@
       if (runConfig == null) {
         throw AssertionError('Run config not found with name: `$runConfigName` (referenced by test suite: `$name`)');
       }
-      if (bundle.compileConfig.renderer == Renderer.canvaskit && runConfig.variant == null) {
-        throw AssertionError(
-            'Run config `$runConfigName` was used with a CanvasKit test bundle `$testBundleName` '
-            'but did not specify a CanvasKit variant (referenced by test suite: `$name`)');
-      }
       bool canvasKit = false;
       bool canvasKitChromium = false;
       bool skwasm = false;
diff --git a/lib/web_ui/dev/generate_builder_json.dart b/lib/web_ui/dev/generate_builder_json.dart
index fe1e6f6..b2d9a5a 100644
--- a/lib/web_ui/dev/generate_builder_json.dart
+++ b/lib/web_ui/dev/generate_builder_json.dart
@@ -120,11 +120,7 @@
       suite.runConfig.browser == BrowserName.safari
     ),
     ..._getTestStepsForPlatform(suites, 'Windows', (TestSuite suite) =>
-      suite.runConfig.browser == BrowserName.chrome &&
-
-      // TODO(jacksongardner): Enable dart2wasm tests on Windows
-      // https://github.com/flutter/flutter/issues/124082
-      suite.testBundle.compileConfig.compiler != Compiler.dart2wasm
+      suite.runConfig.browser == BrowserName.chrome
     ),
   ];
 }
diff --git a/lib/web_ui/dev/steps/compile_bundle_step.dart b/lib/web_ui/dev/steps/compile_bundle_step.dart
index 6601e2f..b1d86ce 100644
--- a/lib/web_ui/dev/steps/compile_bundle_step.dart
+++ b/lib/web_ui/dev/steps/compile_bundle_step.dart
@@ -61,20 +61,20 @@
       .toList();
   }
 
-  TestCompiler _createCompiler() {
-    switch (bundle.compileConfig.compiler) {
+  TestCompiler _createCompiler(CompileConfiguration config) {
+    switch (config.compiler) {
       case Compiler.dart2js:
         return Dart2JSCompiler(
           testSetDirectory,
           outputBundleDirectory,
-          renderer: bundle.compileConfig.renderer,
+          renderer: config.renderer,
           isVerbose: isVerbose,
         );
       case Compiler.dart2wasm:
         return Dart2WasmCompiler(
           testSetDirectory,
           outputBundleDirectory,
-          renderer: bundle.compileConfig.renderer,
+          renderer: config.renderer,
           isVerbose: isVerbose,
         );
     }
@@ -84,7 +84,9 @@
   Future<void> run() async {
     print('Compiling test bundle ${bundle.name.ansiMagenta}...');
     final List<FilePath> allTests = _findTestFiles();
-    final TestCompiler compiler = _createCompiler();
+    final List<TestCompiler> compilers = bundle.compileConfigs.map(
+      (CompileConfiguration config) => _createCompiler(config)
+    ).toList();
     final Stopwatch stopwatch = Stopwatch()..start();
     final String testSetDirectoryPath = testSetDirectory.path;
 
@@ -94,26 +96,28 @@
     }
 
     final List<Future<MapEntry<String, CompileResult>>> pendingResults = <Future<MapEntry<String, CompileResult>>>[];
-    for (final FilePath testFile in allTests) {
-      final String relativePath = pathlib.relative(
-        testFile.absolute,
-        from: testSetDirectoryPath);
-      final Future<MapEntry<String, CompileResult>> result = compilePool.withResource(() async {
-        if (testFiles != null && !testFiles!.contains(testFile)) {
-          return MapEntry<String, CompileResult>(relativePath, CompileResult.filtered);
-        }
-        final bool success = await compiler.compileTest(testFile);
-        const int maxTestNameLength = 80;
-        final String truncatedPath = relativePath.length > maxTestNameLength
-          ? relativePath.replaceRange(maxTestNameLength - 3, relativePath.length, '...')
-          : relativePath;
-        final String expandedPath = truncatedPath.padRight(maxTestNameLength);
-        io.stdout.write('\r  ${success ? expandedPath.ansiGreen : expandedPath.ansiRed}');
-        return success
-          ? MapEntry<String, CompileResult>(relativePath, CompileResult.success)
-          : MapEntry<String, CompileResult>(relativePath, CompileResult.compilationFailure);
-      });
-      pendingResults.add(result);
+    for (final TestCompiler compiler in compilers) {
+      for (final FilePath testFile in allTests) {
+        final String relativePath = pathlib.relative(
+          testFile.absolute,
+          from: testSetDirectoryPath);
+        final Future<MapEntry<String, CompileResult>> result = compilePool.withResource(() async {
+          if (testFiles != null && !testFiles!.contains(testFile)) {
+            return MapEntry<String, CompileResult>(relativePath, CompileResult.filtered);
+          }
+          final bool success = await compiler.compileTest(testFile);
+          const int maxTestNameLength = 80;
+          final String truncatedPath = relativePath.length > maxTestNameLength
+            ? relativePath.replaceRange(maxTestNameLength - 3, relativePath.length, '...')
+            : relativePath;
+          final String expandedPath = truncatedPath.padRight(maxTestNameLength);
+          io.stdout.write('\r  ${success ? expandedPath.ansiGreen : expandedPath.ansiRed}');
+          return success
+            ? MapEntry<String, CompileResult>(relativePath, CompileResult.success)
+            : MapEntry<String, CompileResult>(relativePath, CompileResult.compilationFailure);
+        });
+        pendingResults.add(result);
+      }
     }
     final Map<String, CompileResult> results = Map<String, CompileResult>.fromEntries(await Future.wait(pendingResults));
     stopwatch.stop();
@@ -121,8 +125,11 @@
     final String resultsJson = const JsonEncoder.withIndent('  ').convert(<String, dynamic>{
       'name': bundle.name,
       'directory': bundle.testSet.directory,
-      'compiler': bundle.compileConfig.compiler.name,
-      'renderer': bundle.compileConfig.renderer.name,
+      'builds': bundle.compileConfigs.map(
+        (CompileConfiguration config) => <String, dynamic>{
+          'compiler': config.compiler.name,
+          'renderer': config.renderer.name,
+        }).toList(),
       'compileTimeInMs': stopwatch.elapsedMilliseconds,
       'results': results.map((String k, CompileResult v) => MapEntry<String, String>(k, v.name)),
     });
diff --git a/lib/web_ui/dev/steps/copy_artifacts_step.dart b/lib/web_ui/dev/steps/copy_artifacts_step.dart
index fa63d91..b02f6f4 100644
--- a/lib/web_ui/dev/steps/copy_artifacts_step.dart
+++ b/lib/web_ui/dev/steps/copy_artifacts_step.dart
@@ -33,7 +33,6 @@
   @override
   Future<void> run() async {
     await environment.webTestsArtifactsDir.create(recursive: true);
-    await copyTestBootstrapScripts();
     await buildHostPage();
     await copyTestFonts();
     await copySkiaTestImages();
@@ -52,23 +51,6 @@
     }
   }
 
-  Future<void> copyTestBootstrapScripts() async {
-    for (final String filename in <String>[
-      'test_dart2js.js',
-      'test_dart2wasm.js',
-    ]) {
-      final io.File sourceFile = io.File(pathlib.join(
-        environment.webUiDevDir.path,
-        filename,
-      ));
-      final io.File targetFile = io.File(pathlib.join(
-        environment.webTestsArtifactsDir.path,
-        filename,
-      ));
-      await sourceFile.copy(targetFile.path);
-    }
-  }
-
   Future<void> copyTestFonts() async {
     const Map<String, String> testFonts = <String, String>{
       'Ahem': 'ahem.ttf',
diff --git a/lib/web_ui/dev/steps/run_suite_step.dart b/lib/web_ui/dev/steps/run_suite_step.dart
index adb9a12..a19abdc 100644
--- a/lib/web_ui/dev/steps/run_suite_step.dart
+++ b/lib/web_ui/dev/steps/run_suite_step.dart
@@ -49,8 +49,6 @@
   /// Require Skia Gold to be available and reachable.
   final bool requireSkiaGold;
 
-  bool get isWasm => suite.testBundle.compileConfig.compiler == Compiler.dart2wasm;
-
   @override
   String get description => 'run_suite';
 
@@ -65,7 +63,6 @@
     _prepareTestResultsDirectory();
     final BrowserEnvironment browserEnvironment = getBrowserEnvironment(
       suite.runConfig.browser,
-      enableWasmGC: isWasm,
       useDwarf: useDwarf,
     );
     await browserEnvironment.prepare();
@@ -177,12 +174,18 @@
   }
 
   Future<SkiaGoldClient?> _createSkiaClient() async {
-    final Renderer renderer = suite.testBundle.compileConfig.renderer;
+    if (suite.testBundle.compileConfigs.length > 1) {
+      // Multiple compile configs are only used for our fallback tests, which
+      // do not collect goldens.
+      return null;
+    }
+    final Renderer renderer = suite.testBundle.compileConfigs.first.renderer;
     final CanvasKitVariant? variant = suite.runConfig.variant;
     final io.Directory workDirectory = getSkiaGoldDirectoryForSuite(suite);
     if (workDirectory.existsSync()) {
       workDirectory.deleteSync(recursive: true);
     }
+    final bool isWasm = suite.testBundle.compileConfigs.first.compiler == Compiler.dart2wasm;
     final SkiaGoldClient skiaClient = SkiaGoldClient(
       workDirectory,
       dimensions: <String, String> {
diff --git a/lib/web_ui/dev/suite_filter.dart b/lib/web_ui/dev/suite_filter.dart
index 25b566a..91f2661 100644
--- a/lib/web_ui/dev/suite_filter.dart
+++ b/lib/web_ui/dev/suite_filter.dart
@@ -70,18 +70,28 @@
   }
 }
 
-class CompilerFilter extends AllowListSuiteFilter<Compiler> {
-  CompilerFilter({required super.allowList});
+class CompilerFilter extends SuiteFilter {
+  CompilerFilter({required this.allowList});
+
+  final Set<Compiler> allowList;
 
   @override
-  Compiler getAttributeForSuite(TestSuite suite) => suite.testBundle.compileConfig.compiler;
+  SuiteFilterResult filterSuite(TestSuite suite) => suite.testBundle.compileConfigs.any(
+    (CompileConfiguration config) => allowList.contains(config.compiler)
+  ) ? SuiteFilterResult.accepted()
+    : SuiteFilterResult.rejected('Selected compilers not used in suite.');
 }
 
-class RendererFilter extends AllowListSuiteFilter<Renderer> {
-  RendererFilter({required super.allowList});
+class RendererFilter extends SuiteFilter {
+  RendererFilter({required this.allowList});
+
+  final Set<Renderer> allowList;
 
   @override
-  Renderer getAttributeForSuite(TestSuite suite) => suite.testBundle.compileConfig.renderer;
+  SuiteFilterResult filterSuite(TestSuite suite) => suite.testBundle.compileConfigs.any(
+    (CompileConfiguration config) => allowList.contains(config.renderer)
+  ) ? SuiteFilterResult.accepted()
+    : SuiteFilterResult.rejected('Selected renderers not used in suite.');
 }
 
 class CanvasKitVariantFilter extends AllowListSuiteFilter<CanvasKitVariant> {
diff --git a/lib/web_ui/dev/test_dart2js.js b/lib/web_ui/dev/test_dart2js.js
deleted file mode 100644
index 22f55b5..0000000
--- a/lib/web_ui/dev/test_dart2js.js
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2013 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.
-
-// This script runs in HTML files and loads and instantiates unit tests
-// for the flutter web engine that are compiled to dart2js. It is based
-// off of the `test/dart.js` script from the `test` dart package.
-
-export const runTest = (configuration) => {
-  // Sends an error message to the server indicating that the script failed to
-  // load.
-  //
-  // This mimics a MultiChannel-formatted message.
-  var sendLoadException = function (message) {
-    window.parent.postMessage({
-      "href": window.location.href,
-      "data": [0, { "type": "loadException", "message": message }],
-      "exception": true,
-    }, window.location.origin);
-  }
-
-  // Listen for dartLoadException events and forward to the server.
-  window.addEventListener('dartLoadException', function (e) {
-    sendLoadException(e.detail);
-  });
-
-  // The basename of the current page.
-  var name = window.location.href.replace(/.*\//, '').replace(/#.*/, '');
-
-  // Find <link rel="x-dart-test">.
-  var links = document.getElementsByTagName("link");
-  var testLinks = [];
-  var length = links.length;
-  for (var i = 0; i < length; ++i) {
-    if (links[i].rel == "x-dart-test") testLinks.push(links[i]);
-  }
-
-  if (testLinks.length != 1) {
-    sendLoadException(
-      'Expected exactly 1 <link rel="x-dart-test"> in ' + name + ', found ' +
-      testLinks.length + '.');
-    return;
-  }
-
-  var link = testLinks[0];
-
-  if (link.href == '') {
-    sendLoadException(
-      'Expected <link rel="x-dart-test"> in ' + name + ' to have an "href" ' +
-      'attribute.');
-    return;
-  }
-
-  try {
-    window._flutter.loader.loadEntrypoint({
-      entrypointUrl: link.href + '.browser_test.dart.js',
-      onEntrypointLoaded: function(engineInitializer) {
-        engineInitializer.initializeEngine(configuration).then(function(appRunner) {
-          appRunner.runApp();
-        });
-      }
-    });
-  } catch (exception) {
-    const message = `Failed to bootstrap unit test: ${exception}`;
-    sendLoadException(message);
-  }
-};
diff --git a/lib/web_ui/dev/test_dart2wasm.js b/lib/web_ui/dev/test_dart2wasm.js
deleted file mode 100644
index e1d1c32..0000000
--- a/lib/web_ui/dev/test_dart2wasm.js
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright 2013 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.
-
-// This script runs in HTML files and loads and instantiates dart unit tests
-// that are compiled to WebAssembly. It is based off of the `test/dart.js`
-// script from the `test` dart package.
-
-window.onload = async function () {
-  // Sends an error message to the server indicating that the script failed to
-  // load.
-  //
-  // This mimics a MultiChannel-formatted message.
-  var sendLoadException = function (message) {
-    window.parent.postMessage({
-      "href": window.location.href,
-      "data": [0, { "type": "loadException", "message": message }],
-      "exception": true,
-    }, window.location.origin);
-  }
-
-  // Listen for dartLoadException events and forward to the server.
-  window.addEventListener('dartLoadException', function (e) {
-    sendLoadException(e.detail);
-  });
-
-  // The basename of the current page.
-  var name = window.location.href.replace(/.*\//, '').replace(/#.*/, '');
-
-  // Find <link rel="x-dart-test">.
-  var links = document.getElementsByTagName("link");
-  var testLinks = [];
-  var length = links.length;
-  for (var i = 0; i < length; ++i) {
-    if (links[i].rel == "x-dart-test") testLinks.push(links[i]);
-  }
-
-  if (testLinks.length != 1) {
-    sendLoadException(
-      'Expected exactly 1 <link rel="x-dart-test"> in ' + name + ', found ' +
-      testLinks.length + '.');
-    return;
-  }
-
-  var link = testLinks[0];
-
-  if (link.href == '') {
-    sendLoadException(
-      'Expected <link rel="x-dart-test"> in ' + name + ' to have an "href" ' +
-      'attribute.');
-    return;
-  }
-
-  let dart2wasm_runtime;
-  let moduleInstance;
-  try {
-    const isSkwasm = link.hasAttribute('skwasm');
-    const imports = isSkwasm ? new Promise((resolve) => {
-      const skwasmScript = document.createElement('script');
-      skwasmScript.src = '/canvaskit/skwasm.js';
-
-      document.body.appendChild(skwasmScript);
-      skwasmScript.addEventListener('load', async () => {
-        const skwasmInstance = await skwasm();
-        window._flutter_skwasmInstance = skwasmInstance;
-        resolve({
-          "skwasm": skwasmInstance.wasmExports,
-          "skwasmWrapper": skwasmInstance,
-          "ffi": {
-            "memory": skwasmInstance.wasmMemory,
-          }
-        });
-      });
-    }) : {};
-
-    let baseName = link.href + '.browser_test.dart';
-    dart2wasm_runtime = await import(baseName + '.mjs');
-    const dartModulePromise = WebAssembly.compileStreaming(fetch(baseName + '.wasm'));
-    moduleInstance = await dart2wasm_runtime.instantiate(dartModulePromise, imports);
-  } catch (exception) {
-    const message = `Failed to fetch and instantiate wasm module: ${exception}`;
-    sendLoadException(message);
-  }
-
-  if (moduleInstance) {
-    try {
-      await dart2wasm_runtime.invoke(moduleInstance);
-    } catch (exception) {
-      const message = `Exception while invoking test: ${exception}`;
-      sendLoadException(message);
-    }
-  }
-};
diff --git a/lib/web_ui/dev/test_platform.dart b/lib/web_ui/dev/test_platform.dart
index 90d3172..b91c354 100644
--- a/lib/web_ui/dev/test_platform.dart
+++ b/lib/web_ui/dev/test_platform.dart
@@ -155,8 +155,9 @@
   /// The URL for this server.
   Uri get url => server.url.resolve('/');
 
-  bool get isWasm => suite.testBundle.compileConfig.compiler == Compiler.dart2wasm;
-  bool get needsCrossOriginIsolated => isWasm && suite.testBundle.compileConfig.renderer == Renderer.skwasm;
+  bool get needsCrossOriginIsolated => suite.testBundle.compileConfigs.any(
+    (CompileConfiguration config) => config.renderer == Renderer.skwasm
+  );
 
   /// A [OneOffHandler] for servicing WebSocket connections for
   /// [BrowserManager]s.
@@ -526,47 +527,58 @@
     }
   }
 
+  String _makeBuildConfigString(String scriptBase, CompileConfiguration config) {
+    return config.compiler == Compiler.dart2wasm ? '''
+      {
+        compileTarget: "${config.compiler.name}",
+        renderer: "${config.renderer.name}",
+        mainWasmPath: "$scriptBase.browser_test.dart.wasm",
+        jsSupportRuntimePath: "$scriptBase.browser_test.dart.mjs",
+      }
+''' : '''
+      {
+        compileTarget: "${config.compiler.name}",
+        renderer: "${config.renderer.name}",
+        mainJsPath: "$scriptBase.browser_test.dart.js",
+      }
+''';
+  }
+
   /// Serves the HTML file that bootstraps the test.
   shelf.Response _testBootstrapHandler(shelf.Request request) {
     final String path = p.fromUri(request.url);
 
     if (path.endsWith('.html')) {
       final String test = '${p.withoutExtension(path)}.dart';
-
-      final bool linkSkwasm = suite.testBundle.compileConfig.renderer == Renderer.skwasm;
-      // Link to the Dart wrapper.
       final String scriptBase = htmlEscape.convert(p.basename(test));
-      final String link = '<link rel="x-dart-test" href="$scriptBase"${linkSkwasm ? " skwasm" : ""}>';
 
-      final String bootstrapScript = isWasm ? '''
-<script>
-  window.flutterConfiguration = {
-    canvasKitBaseUrl: "/canvaskit/",
-    // Some of our tests rely on color emoji
-    useColorEmoji: true,
-    canvasKitVariant: "${getCanvasKitVariant()}",
-  };
-</script>
-<script src="/test_dart2wasm.js"></script>
-      ''' : '''
+      final String buildConfigsString = suite.testBundle.compileConfigs.map(
+        (CompileConfiguration config) => _makeBuildConfigString(scriptBase, config)
+      ).join(',\n');
+      final String bootstrapScript = '''
 <script src="/flutter_js/flutter.js"></script>
-<script type="module">
-  import { runTest } from "/test_dart2js.js";
-
-  runTest({
-    canvasKitBaseUrl: "/canvaskit/",
-    // Some of our tests rely on color emoji
-    useColorEmoji: true,
-    canvasKitVariant: "${getCanvasKitVariant()}",
+<script>
+  _flutter.buildConfig = {
+    builds: [
+      $buildConfigsString
+    ]
+  };
+  _flutter.loader.load({
+    config: {
+      canvasKitBaseUrl: "/canvaskit/",
+      // Some of our tests rely on color emoji
+      useColorEmoji: true,
+      canvasKitVariant: "${getCanvasKitVariant()}",
+    },
   });
 </script>
 ''';
+
       return shelf.Response.ok('''
         <!DOCTYPE html>
         <html>
         <head>
           <meta name="assetBase" content="/">
-          $link
           $bootstrapScript
         </head>
         </html>
@@ -643,13 +655,16 @@
           'debug': isDebug.toString()
         });
 
+    final bool hasSourceMaps = suite.testBundle.compileConfigs.any(
+      (CompileConfiguration config) => config.compiler == Compiler.dart2js
+    );
     final Future<BrowserManager?> future = BrowserManager.start(
       browserEnvironment: browserEnvironment,
       url: hostUrl,
       future: completer.future,
       packageConfig: packageConfig,
       debug: isDebug,
-      sourceMapDirectory: isWasm ? null : getBundleBuildDirectory(suite.testBundle),
+      sourceMapDirectory: hasSourceMaps ? getBundleBuildDirectory(suite.testBundle) : null,
     );
 
     // Store null values for browsers that error out so we know not to load them
diff --git a/lib/web_ui/flutter_js/BUILD.gn b/lib/web_ui/flutter_js/BUILD.gn
index c49893f..255cd2c 100644
--- a/lib/web_ui/flutter_js/BUILD.gn
+++ b/lib/web_ui/flutter_js/BUILD.gn
@@ -3,6 +3,7 @@
 # found in the LICENSE file.
 
 import("//flutter/build/esbuild/esbuild.gni")
+import("//flutter/shell/version/version.gni")
 import("sources.gni")
 
 group("flutter_js") {
diff --git a/lib/web_ui/flutter_js/sources.gni b/lib/web_ui/flutter_js/sources.gni
index ee00c55..11da02b 100644
--- a/lib/web_ui/flutter_js/sources.gni
+++ b/lib/web_ui/flutter_js/sources.gni
@@ -2,4 +2,17 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-flutter_js_source_list = [ "src/flutter.js" ]
+flutter_js_source_list = [
+  "src/base_uri.js",
+  "src/browser_environment.js",
+  "src/canvaskit_loader.js",
+  "src/entrypoint_loader.js",
+  "src/flutter.js",
+  "src/instantiate_wasm.js",
+  "src/loader.js",
+  "src/service_worker_loader.js",
+  "src/skwasm_loader.js",
+  "src/trusted_types.js",
+
+  "src/types.d.ts",
+]
diff --git a/lib/web_ui/flutter_js/src/base_uri.js b/lib/web_ui/flutter_js/src/base_uri.js
new file mode 100644
index 0000000..bfca8d3
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/base_uri.js
@@ -0,0 +1,17 @@
+// Copyright 2013 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.
+
+export const baseUri = ensureTrailingSlash(getBaseURI());
+
+function getBaseURI() {
+  const base = document.querySelector("base");
+  return (base && base.getAttribute("href")) || "";
+}
+
+function ensureTrailingSlash(uri) {
+  if (uri === "") {
+    return uri;
+  }
+  return uri.endsWith("/") ? uri : `${uri}/`;
+}
diff --git a/lib/web_ui/flutter_js/src/browser_environment.js b/lib/web_ui/flutter_js/src/browser_environment.js
new file mode 100644
index 0000000..64baabc
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/browser_environment.js
@@ -0,0 +1,46 @@
+// Copyright 2013 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.
+
+const isBlink = () => {
+  return (navigator.vendor === 'Google Inc.') ||
+    (navigator.agent === 'Edg/');
+}
+
+const hasImageCodecs = () => {
+  if (typeof ImageDecoder === 'undefined') {
+    return false;
+  }
+  // TODO(yjbanov): https://github.com/flutter/flutter/issues/122761
+  // Frequently, when a browser launches an API that other browsers already
+  // support, there are subtle incompatibilities that may cause apps to crash if,
+  // we blindly adopt the new implementation. This check prevents us from picking
+  // up potentially incompatible implementations of ImagdeDecoder API. Instead,
+  // when a new browser engine launches the API, we'll evaluate it and enable it
+  // explicitly.
+  return isBlink();
+}
+
+const hasChromiumBreakIterators = () => {
+  return (typeof Intl.v8BreakIterator !== "undefined") &&
+    (typeof Intl.Segmenter !== "undefined");
+}
+
+const supportsWasmGC = () => {
+  // This attempts to instantiate a wasm module that only will validate if the
+  // final WasmGC spec is implemented in the browser.
+  //
+  // Copied from https://github.com/GoogleChromeLabs/wasm-feature-detect/blob/main/src/detectors/gc/index.js
+  const bytes = [0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 95, 1, 120, 0];
+  return WebAssembly.validate(new Uint8Array(bytes));
+}
+
+/**
+ * @returns {import("./types").BrowserEnvironment}
+ */
+export const browserEnvironment = {
+  hasImageCodecs: hasImageCodecs(),
+  hasChromiumBreakIterators: hasChromiumBreakIterators(),
+  supportsWasmGC: supportsWasmGC(),
+  crossOriginIsolated: window.crossOriginIsolated,
+};
diff --git a/lib/web_ui/flutter_js/src/canvaskit_loader.js b/lib/web_ui/flutter_js/src/canvaskit_loader.js
new file mode 100644
index 0000000..cf88c3c
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/canvaskit_loader.js
@@ -0,0 +1,47 @@
+// Copyright 2013 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.
+
+import { createWasmInstantiator } from "./instantiate_wasm.js";
+
+export const loadCanvasKit = (deps, config, browserEnvironment, engineRevision) => {
+  if (window.flutterCanvasKit) {
+    // The user has set this global variable ahead of time, so we just return that.
+    return Promise.resolve(window.flutterCanvasKit);
+  }
+  window.flutterCanvasKitLoaded = new Promise((resolve, reject) => {
+    const supportsChromiumCanvasKit = browserEnvironment.hasChromiumBreakIterators && browserEnvironment.hasImageCodecs;
+    if (!supportsChromiumCanvasKit && config.canvasKitVariant == "chromium") {
+      throw "Chromium CanvasKit variant specifically requested, but unsupported in this browser";
+    }
+    const useChromiumCanvasKit = supportsChromiumCanvasKit && (config.canvasKitVariant !== "full");
+    let baseUrl = config.canvasKitBaseUrl ?? `https://www.gstatic.com/flutter-canvaskit/${engineRevision}/`;
+    if (useChromiumCanvasKit) {
+      baseUrl = `${baseUrl}/chromium/`;
+    }
+    let canvasKitUrl = `${baseUrl}canvaskit.js`;
+    if (deps.flutterTT.policy) {
+      canvasKitUrl = deps.flutterTT.policy.createScriptURL(canvasKitUrl);
+    }
+    const wasmInstantiator = createWasmInstantiator(`${baseUrl}canvaskit.wasm`);
+    const script = document.createElement("script");
+    script.src = canvasKitUrl;
+    if (config.nonce) {
+      script.nonce = config.nonce;
+    }
+    script.addEventListener('load', async () => {
+      try {
+        const canvasKit = await CanvasKitInit({
+          instantiateWasm: wasmInstantiator,
+        });
+        window.flutterCanvasKit = canvasKit;
+        resolve(canvasKit);  
+      } catch (e) {
+        reject(e);
+      }
+    });
+    script.addEventListener('error', reject);
+    document.head.appendChild(script);
+  });
+  return window.flutterCanvasKitLoaded;
+}
diff --git a/lib/web_ui/flutter_js/src/entrypoint_loader.js b/lib/web_ui/flutter_js/src/entrypoint_loader.js
new file mode 100644
index 0000000..db8b331
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/entrypoint_loader.js
@@ -0,0 +1,198 @@
+// Copyright 2013 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.
+
+import { baseUri } from "./base_uri.js";
+
+/**
+ * Handles injecting the main Flutter web entrypoint (main.dart.js), and notifying
+ * the user when Flutter is ready, through `didCreateEngineInitializer`.
+ *
+ * @see https://docs.flutter.dev/development/platform-integration/web/initialization
+ */
+export class FlutterEntrypointLoader {
+  /**
+   * Creates a FlutterEntrypointLoader.
+   */
+  constructor() {
+    // Watchdog to prevent injecting the main entrypoint multiple times.
+    this._scriptLoaded = false;
+  }
+  /**
+   * Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
+   * @param {TrustedTypesPolicy | undefined} policy
+   */
+  setTrustedTypesPolicy(policy) {
+    this._ttPolicy = policy;
+  }
+  /**
+   * @deprecated
+   * Loads flutter main entrypoint, specified by `entrypointUrl`, and calls a
+   * user-specified `onEntrypointLoaded` callback with an EngineInitializer
+   * object when it's done.
+   *
+   * @param {*} options
+   * @returns {Promise | undefined} that will eventually resolve with an
+   * EngineInitializer, or will be rejected with the error caused by the loader.
+   * Returns undefined when an `onEntrypointLoaded` callback is supplied in `options`.
+   */
+  async loadEntrypoint(options) {
+    const { entrypointUrl = `${baseUri}main.dart.js`, onEntrypointLoaded, nonce } =
+      options || {};
+    return this._loadJSEntrypoint(entrypointUrl, onEntrypointLoaded, nonce);
+  }
+
+  /**
+   * Loads the entry point for a flutter application.
+   * @param {import("./types").ApplicationBuild} build
+   *   Information about the specific build that is to be loaded
+   * @param {*} deps
+   *   External dependencies that may be needed to load the app.
+   * @param {import("./types").FlutterConfiguration} config
+   *   The application configuration. If no callback is specified, this will be
+   *   passed along to engine when initializing it.
+   * @param {string} nonce
+   *   A nonce to apply to the main application script tag, if necessary.
+   * @param {import("./types").OnEntrypointLoadedCallback?} onEntrypointLoaded
+   *   An optional callback to invoke when the entrypoint is loaded. If no
+   *   callback is supplied, the engine initializer and app runner will be
+   *   automatically invoked on load, passing along the supplied flutter
+   *   configuration.
+   */
+  async load(build, deps, config, nonce, onEntrypointLoaded) {
+    onEntrypointLoaded ??= (engineInitializer) => {
+      engineInitializer.initializeEngine(config).then((appRunner) => appRunner.runApp())
+    };
+    if (build.compileTarget === "dart2wasm") {
+      return this._loadWasmEntrypoint(build, deps, onEntrypointLoaded);
+    } else {
+      const mainPath = build.mainJsPath ?? "main.dart.js";
+      const entrypointUrl = `${baseUri}${mainPath}`;
+      return this._loadJSEntrypoint(entrypointUrl, onEntrypointLoaded, nonce);
+    }
+  }
+
+  /**
+   * Resolves the promise created by loadEntrypoint, and calls the `onEntrypointLoaded`
+   * function supplied by the user (if needed).
+   *
+   * Called by Flutter through `_flutter.loader.didCreateEngineInitializer` method,
+   * which is bound to the correct instance of the FlutterEntrypointLoader by
+   * the FlutterLoader object.
+   *
+   * @param {Function} engineInitializer @see https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/js_interop/js_loader.dart#L42
+   */
+  didCreateEngineInitializer(engineInitializer) {
+    if (typeof this._didCreateEngineInitializerResolve === "function") {
+      this._didCreateEngineInitializerResolve(engineInitializer);
+      // Remove the resolver after the first time, so Flutter Web can hot restart.
+      this._didCreateEngineInitializerResolve = null;
+      // Make the engine revert to "auto" initialization on hot restart.
+      delete _flutter.loader.didCreateEngineInitializer;
+    }
+    if (typeof this._onEntrypointLoaded === "function") {
+      this._onEntrypointLoaded(engineInitializer);
+    }
+  }
+  /**
+   * Injects a script tag into the DOM, and configures this loader to be able to
+   * handle the "entrypoint loaded" notifications received from Flutter web.
+   *
+   * @param {string} entrypointUrl the URL of the script that will initialize
+   *                 Flutter.
+   * @param {Function} onEntrypointLoaded a callback that will be called when
+   *                   Flutter web notifies this object that the entrypoint is
+   *                   loaded.
+   * @returns {Promise | undefined} a Promise that resolves when the entrypoint
+   *                                is loaded, or undefined if `onEntrypointLoaded`
+   *                                is a function.
+   */
+  _loadJSEntrypoint(entrypointUrl, onEntrypointLoaded, nonce) {
+    const useCallback = typeof onEntrypointLoaded === "function";
+    if (!this._scriptLoaded) {
+      this._scriptLoaded = true;
+      const scriptTag = this._createScriptTag(entrypointUrl, nonce);
+      if (useCallback) {
+        // Just inject the script tag, and return nothing; Flutter will call
+        // `didCreateEngineInitializer` when it's done.
+        console.debug("Injecting <script> tag. Using callback.");
+        this._onEntrypointLoaded = onEntrypointLoaded;
+        document.head.append(scriptTag);
+      } else {
+        // Inject the script tag and return a promise that will get resolved
+        // with the EngineInitializer object from Flutter when it calls
+        // `didCreateEngineInitializer` later.
+        return new Promise((resolve, reject) => {
+          console.debug(
+            "Injecting <script> tag. Using Promises. Use the callback approach instead!"
+          );
+          this._didCreateEngineInitializerResolve = resolve;
+          scriptTag.addEventListener("error", reject);
+          document.head.append(scriptTag);
+        });
+      }
+    }
+  }
+
+  /**
+   *
+   * @param {import("./types").WasmApplicationBuild} build
+   * @param {*} deps
+   * @param {import("./types").OnEntrypointLoadedCallback} onEntrypointLoaded
+   */
+  async _loadWasmEntrypoint(build, deps, onEntrypointLoaded) {
+    if (!this._scriptLoaded) {
+      this._scriptLoaded = true;
+
+      this._onEntrypointLoaded = onEntrypointLoaded;
+      const { mainWasmPath, jsSupportRuntimePath } = build;
+      const moduleUri = `${baseUri}${mainWasmPath}`;
+      let jsSupportRuntimeUri = `${baseUri}${jsSupportRuntimePath}`;
+      if (this._ttPolicy != null) {
+        jsSupportRuntimeUri = this._ttPolicy.createScriptURL(jsSupportRuntimeUri);
+      }  
+      const dartModulePromise = WebAssembly.compileStreaming(fetch(moduleUri));
+
+      const jsSupportRuntime = await import(jsSupportRuntimeUri);
+
+      let imports;
+      if (build.renderer === "skwasm") {
+        imports = (async () => {
+          const skwasmInstance = await deps.skwasm;
+          window._flutter_skwasmInstance = skwasmInstance;
+          return {
+            skwasm: skwasmInstance.wasmExports,
+            skwasmWrapper: skwasmInstance,
+            ffi: {
+              memory: skwasmInstance.wasmMemory,
+            },
+          };
+        })();
+      } else {
+        imports = {};
+      }
+      const moduleInstance = await jsSupportRuntime.instantiate(dartModulePromise, imports);
+      await jsSupportRuntime.invoke(moduleInstance);
+    }
+  }
+
+  /**
+   * Creates a script tag for the given URL.
+   * @param {string} url
+   * @returns {HTMLScriptElement}
+   */
+  _createScriptTag(url, nonce) {
+    const scriptTag = document.createElement("script");
+    scriptTag.type = "application/javascript";
+    if (nonce) {
+      scriptTag.nonce = nonce;
+    }
+    // Apply TrustedTypes validation, if available.
+    let trustedUrl = url;
+    if (this._ttPolicy != null) {
+      trustedUrl = this._ttPolicy.createScriptURL(url);
+    }
+    scriptTag.src = trustedUrl;
+    return scriptTag;
+  }
+}
diff --git a/lib/web_ui/flutter_js/src/flutter.js b/lib/web_ui/flutter_js/src/flutter.js
index fcdc9c3..57208c7 100644
--- a/lib/web_ui/flutter_js/src/flutter.js
+++ b/lib/web_ui/flutter_js/src/flutter.js
@@ -2,380 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-var _flutter = window._flutter;
-if (!_flutter) {
-  _flutter = window._flutter = {};
+import { FlutterLoader } from './loader.js';
+
+if (!window._flutter) {
+  window._flutter = {
+    loader: new FlutterLoader()
+  };
 }
-_flutter.loader = null;
-
-(function () {
-  "use strict";
-
-  const baseUri = ensureTrailingSlash(getBaseURI());
-
-  function getBaseURI() {
-    const base = document.querySelector("base");
-    return (base && base.getAttribute("href")) || "";
-  }
-
-  function ensureTrailingSlash(uri) {
-    if (uri == "") {
-      return uri;
-    }
-    return uri.endsWith("/") ? uri : `${uri}/`;
-  }
-
-  /**
-   * Wraps `promise` in a timeout of the given `duration` in ms.
-   *
-   * Resolves/rejects with whatever the original `promises` does, or rejects
-   * if `promise` takes longer to complete than `duration`. In that case,
-   * `debugName` is used to compose a legible error message.
-   *
-   * If `duration` is < 0, the original `promise` is returned unchanged.
-   * @param {Promise} promise
-   * @param {number} duration
-   * @param {string} debugName
-   * @returns {Promise} a wrapped promise.
-   */
-  async function timeout(promise, duration, debugName) {
-    if (duration < 0) {
-      return promise;
-    }
-    let timeoutId;
-    const _clock = new Promise((_, reject) => {
-      timeoutId = setTimeout(() => {
-        reject(
-          new Error(
-            `${debugName} took more than ${duration}ms to resolve. Moving on.`,
-            {
-              cause: timeout,
-            }
-          )
-        );
-      }, duration);
-    });
-
-    return Promise.race([promise, _clock]).finally(() => {
-      clearTimeout(timeoutId);
-    });
-  }
-
-  /**
-   * Handles the creation of a TrustedTypes `policy` that validates URLs based
-   * on an (optional) incoming array of RegExes.
-   */
-  class FlutterTrustedTypesPolicy {
-    /**
-     * Constructs the policy.
-     * @param {[RegExp]} validPatterns the patterns to test URLs
-     * @param {String} policyName the policy name (optional)
-     */
-    constructor(validPatterns, policyName = "flutter-js") {
-      const patterns = validPatterns || [
-        /\.js$/,
-      ];
-      if (window.trustedTypes) {
-        this.policy = trustedTypes.createPolicy(policyName, {
-          createScriptURL: function(url) {
-            const parsed = new URL(url, window.location);
-            const file = parsed.pathname.split("/").pop();
-            const matches = patterns.some((pattern) => pattern.test(file));
-            if (matches) {
-              return parsed.toString();
-            }
-            console.error(
-              "URL rejected by TrustedTypes policy",
-              policyName, ":", url, "(download prevented)");
-          }
-        });
-      }
-    }
-  }
-
-  /**
-   * Handles loading/reloading Flutter's service worker, if configured.
-   *
-   * @see: https://developers.google.com/web/fundamentals/primers/service-workers
-   */
-  class FlutterServiceWorkerLoader {
-    /**
-     * Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
-     * @param {TrustedTypesPolicy | undefined} policy
-     */
-    setTrustedTypesPolicy(policy) {
-      this._ttPolicy = policy;
-    }
-
-    /**
-     * Returns a Promise that resolves when the latest Flutter service worker,
-     * configured by `settings` has been loaded and activated.
-     *
-     * Otherwise, the promise is rejected with an error message.
-     * @param {*} settings Service worker settings
-     * @returns {Promise} that resolves when the latest serviceWorker is ready.
-     */
-    loadServiceWorker(settings) {
-      if (settings == null) {
-        // In the future, settings = null -> uninstall service worker?
-        console.debug("Null serviceWorker configuration. Skipping.");
-        return Promise.resolve();
-      }
-      if (!("serviceWorker" in navigator)) {
-        let errorMessage = "Service Worker API unavailable.";
-        if (!window.isSecureContext) {
-          errorMessage += "\nThe current context is NOT secure."
-          errorMessage += "\nRead more: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts";
-        }
-        return Promise.reject(
-          new Error(errorMessage)
-        );
-      }
-      const {
-        serviceWorkerVersion,
-        serviceWorkerUrl = `${baseUri}flutter_service_worker.js?v=${serviceWorkerVersion}`,
-        timeoutMillis = 4000,
-      } = settings;
-
-      // Apply the TrustedTypes policy, if present.
-      let url = serviceWorkerUrl;
-      if (this._ttPolicy != null) {
-        url = this._ttPolicy.createScriptURL(url);
-      }
-
-      const serviceWorkerActivation = navigator.serviceWorker
-        .register(url)
-        .then((serviceWorkerRegistration) => this._getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion))
-        .then(this._waitForServiceWorkerActivation);
-
-      // Timeout race promise
-      return timeout(
-        serviceWorkerActivation,
-        timeoutMillis,
-        "prepareServiceWorker"
-      );
-    }
-
-    /**
-     * Returns the latest service worker for the given `serviceWorkerRegistration`.
-     *
-     * This might return the current service worker, if there's no new service worker
-     * awaiting to be installed/updated.
-     *
-     * @param {ServiceWorkerRegistration} serviceWorkerRegistration
-     * @param {String} serviceWorkerVersion
-     * @returns {Promise<ServiceWorker>}
-     */
-    async _getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion) {
-      if (!serviceWorkerRegistration.active && (serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting)) {
-        // No active web worker and we have installed or are installing
-        // one for the first time. Simply wait for it to activate.
-        console.debug("Installing/Activating first service worker.");
-        return serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting;
-      } else if (!serviceWorkerRegistration.active.scriptURL.endsWith(serviceWorkerVersion)) {
-        // When the app updates the serviceWorkerVersion changes, so we
-        // need to ask the service worker to update.
-        const newRegistration = await serviceWorkerRegistration.update();
-        console.debug("Updating service worker.");
-        return newRegistration.installing || newRegistration.waiting || newRegistration.active;
-      } else {
-        console.debug("Loading from existing service worker.");
-        return serviceWorkerRegistration.active;
-      }
-    }
-
-    /**
-     * Returns a Promise that resolves when the `serviceWorker` changes its
-     * state to "activated".
-     *
-     * @param {ServiceWorker} serviceWorker
-     * @returns {Promise<void>}
-     */
-    async _waitForServiceWorkerActivation(serviceWorker) {
-      if (!serviceWorker || serviceWorker.state == "activated") {
-        if (!serviceWorker) {
-          throw new Error("Cannot activate a null service worker!");
-        } else {
-          console.debug("Service worker already active.");
-          return;
-        }
-      }
-      return new Promise((resolve, _) => {
-        serviceWorker.addEventListener("statechange", () => {
-          if (serviceWorker.state == "activated") {
-            console.debug("Activated new service worker.");
-            resolve();
-          }
-        });
-      });
-    }
-  }
-
-  /**
-   * Handles injecting the main Flutter web entrypoint (main.dart.js), and notifying
-   * the user when Flutter is ready, through `didCreateEngineInitializer`.
-   *
-   * @see https://docs.flutter.dev/development/platform-integration/web/initialization
-   */
-  class FlutterEntrypointLoader {
-    /**
-     * Creates a FlutterEntrypointLoader.
-     */
-    constructor() {
-      // Watchdog to prevent injecting the main entrypoint multiple times.
-      this._scriptLoaded = false;
-    }
-
-    /**
-     * Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
-     * @param {TrustedTypesPolicy | undefined} policy
-     */
-    setTrustedTypesPolicy(policy) {
-      this._ttPolicy = policy;
-    }
-
-    /**
-     * Loads flutter main entrypoint, specified by `entrypointUrl`, and calls a
-     * user-specified `onEntrypointLoaded` callback with an EngineInitializer
-     * object when it's done.
-     *
-     * @param {*} options
-     * @returns {Promise | undefined} that will eventually resolve with an
-     * EngineInitializer, or will be rejected with the error caused by the loader.
-     * Returns undefined when an `onEntrypointLoaded` callback is supplied in `options`.
-     */
-    async loadEntrypoint(options) {
-      const { entrypointUrl = `${baseUri}main.dart.js`, onEntrypointLoaded, nonce } =
-        options || {};
-
-      return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded, nonce);
-    }
-
-    /**
-     * Resolves the promise created by loadEntrypoint, and calls the `onEntrypointLoaded`
-     * function supplied by the user (if needed).
-     *
-     * Called by Flutter through `_flutter.loader.didCreateEngineInitializer` method,
-     * which is bound to the correct instance of the FlutterEntrypointLoader by
-     * the FlutterLoader object.
-     *
-     * @param {Function} engineInitializer @see https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/js_interop/js_loader.dart#L42
-     */
-    didCreateEngineInitializer(engineInitializer) {
-      if (typeof this._didCreateEngineInitializerResolve === "function") {
-        this._didCreateEngineInitializerResolve(engineInitializer);
-        // Remove the resolver after the first time, so Flutter Web can hot restart.
-        this._didCreateEngineInitializerResolve = null;
-        // Make the engine revert to "auto" initialization on hot restart.
-        delete _flutter.loader.didCreateEngineInitializer;
-      }
-      if (typeof this._onEntrypointLoaded === "function") {
-        this._onEntrypointLoaded(engineInitializer);
-      }
-    }
-
-    /**
-     * Injects a script tag into the DOM, and configures this loader to be able to
-     * handle the "entrypoint loaded" notifications received from Flutter web.
-     *
-     * @param {string} entrypointUrl the URL of the script that will initialize
-     *                 Flutter.
-     * @param {Function} onEntrypointLoaded a callback that will be called when
-     *                   Flutter web notifies this object that the entrypoint is
-     *                   loaded.
-     * @returns {Promise | undefined} a Promise that resolves when the entrypoint
-     *                                is loaded, or undefined if `onEntrypointLoaded`
-     *                                is a function.
-     */
-    _loadEntrypoint(entrypointUrl, onEntrypointLoaded, nonce) {
-      const useCallback = typeof onEntrypointLoaded === "function";
-
-      if (!this._scriptLoaded) {
-        this._scriptLoaded = true;
-        const scriptTag = this._createScriptTag(entrypointUrl, nonce);
-        if (useCallback) {
-          // Just inject the script tag, and return nothing; Flutter will call
-          // `didCreateEngineInitializer` when it's done.
-          console.debug("Injecting <script> tag. Using callback.");
-          this._onEntrypointLoaded = onEntrypointLoaded;
-          document.body.append(scriptTag);
-        } else {
-          // Inject the script tag and return a promise that will get resolved
-          // with the EngineInitializer object from Flutter when it calls
-          // `didCreateEngineInitializer` later.
-          return new Promise((resolve, reject) => {
-            console.debug(
-              "Injecting <script> tag. Using Promises. Use the callback approach instead!"
-            );
-            this._didCreateEngineInitializerResolve = resolve;
-            scriptTag.addEventListener("error", reject);
-            document.body.append(scriptTag);
-          });
-        }
-      }
-    }
-
-    /**
-     * Creates a script tag for the given URL.
-     * @param {string} url
-     * @returns {HTMLScriptElement}
-     */
-    _createScriptTag(url, nonce) {
-      const scriptTag = document.createElement("script");
-      scriptTag.type = "application/javascript";
-      if (nonce) {
-        scriptTag.nonce = nonce;
-      }
-      // Apply TrustedTypes validation, if available.
-      let trustedUrl = url;
-      if (this._ttPolicy != null) {
-        trustedUrl = this._ttPolicy.createScriptURL(url);
-      }
-      scriptTag.src = trustedUrl;
-      return scriptTag;
-    }
-  }
-
-  /**
-   * The public interface of _flutter.loader. Exposes two methods:
-   * * loadEntrypoint (which coordinates the default Flutter web loading procedure)
-   * * didCreateEngineInitializer (which is called by Flutter to notify that its
-   *                              Engine is ready to be initialized)
-   */
-  class FlutterLoader {
-    /**
-     * Initializes the Flutter web app.
-     * @param {*} options
-     * @returns {Promise?} a (Deprecated) Promise that will eventually resolve
-     *                     with an EngineInitializer, or will be rejected with
-     *                     any error caused by the loader. Or Null, if the user
-     *                     supplies an `onEntrypointLoaded` Function as an option.
-     */
-    async loadEntrypoint(options) {
-      const { serviceWorker, ...entrypoint } = options || {};
-
-      // A Trusted Types policy that is going to be used by the loader.
-      const flutterTT = new FlutterTrustedTypesPolicy();
-
-      // The FlutterServiceWorkerLoader instance could be injected as a dependency
-      // (and dynamically imported from a module if not present).
-      const serviceWorkerLoader = new FlutterServiceWorkerLoader();
-      serviceWorkerLoader.setTrustedTypesPolicy(flutterTT.policy);
-      await serviceWorkerLoader.loadServiceWorker(serviceWorker).catch(e => {
-        // Regardless of what happens with the injection of the SW, the show must go on
-        console.warn("Exception while loading service worker:", e);
-      });
-
-      // The FlutterEntrypointLoader instance could be injected as a dependency
-      // (and dynamically imported from a module if not present).
-      const entrypointLoader = new FlutterEntrypointLoader();
-      entrypointLoader.setTrustedTypesPolicy(flutterTT.policy);
-      // Install the `didCreateEngineInitializer` listener where Flutter web expects it to be.
-      this.didCreateEngineInitializer =
-        entrypointLoader.didCreateEngineInitializer.bind(entrypointLoader);
-      return entrypointLoader.loadEntrypoint(entrypoint);
-    }
-  }
-
-  _flutter.loader = new FlutterLoader();
-})();
diff --git a/lib/web_ui/flutter_js/src/instantiate_wasm.js b/lib/web_ui/flutter_js/src/instantiate_wasm.js
new file mode 100644
index 0000000..29827f6
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/instantiate_wasm.js
@@ -0,0 +1,17 @@
+// Copyright 2013 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.
+
+// This is a little helper function that helps us start the fetch and compilation
+// of an emscripten wasm module in parallel with the fetch of its script.
+export const createWasmInstantiator = (url) => {
+  const modulePromise = WebAssembly.compileStreaming(fetch(url));
+  return (imports, successCallback) => {
+    (async () => {
+      const module = await modulePromise;
+      const instance = await WebAssembly.instantiate(module, imports);
+      successCallback(instance, module);
+    })();
+    return {};
+  };
+}
diff --git a/lib/web_ui/flutter_js/src/loader.js b/lib/web_ui/flutter_js/src/loader.js
new file mode 100644
index 0000000..3d5fb4e
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/loader.js
@@ -0,0 +1,130 @@
+// Copyright 2013 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.
+
+import { browserEnvironment } from './browser_environment.js';
+import { FlutterEntrypointLoader } from './entrypoint_loader.js';
+import { FlutterServiceWorkerLoader } from './service_worker_loader.js';
+import { FlutterTrustedTypesPolicy } from './trusted_types.js';
+import { loadCanvasKit } from './canvaskit_loader.js';
+import { loadSkwasm } from './skwasm_loader.js';
+
+/**
+ * The public interface of _flutter.loader. Exposes two methods:
+ * * loadEntrypoint (which coordinates the default Flutter web loading procedure)
+ * * didCreateEngineInitializer (which is called by Flutter to notify that its
+ *                              Engine is ready to be initialized)
+ */
+export class FlutterLoader {
+  /**
+   * @deprecated Use `load` instead.
+   * Initializes the Flutter web app.
+   * @param {*} options
+   * @returns {Promise?} a (Deprecated) Promise that will eventually resolve
+   *                     with an EngineInitializer, or will be rejected with
+   *                     any error caused by the loader. Or Null, if the user
+   *                     supplies an `onEntrypointLoaded` Function as an option.
+   */
+  async loadEntrypoint(options) {
+    const { serviceWorker, ...entrypoint } = options || {};
+    // A Trusted Types policy that is going to be used by the loader.
+    const flutterTT = new FlutterTrustedTypesPolicy();
+    // The FlutterServiceWorkerLoader instance could be injected as a dependency
+    // (and dynamically imported from a module if not present).
+    const serviceWorkerLoader = new FlutterServiceWorkerLoader();
+    serviceWorkerLoader.setTrustedTypesPolicy(flutterTT.policy);
+    await serviceWorkerLoader.loadServiceWorker(serviceWorker).catch(e => {
+      // Regardless of what happens with the injection of the SW, the show must go on
+      console.warn("Exception while loading service worker:", e);
+    });
+    // The FlutterEntrypointLoader instance could be injected as a dependency
+    // (and dynamically imported from a module if not present).
+    const entrypointLoader = new FlutterEntrypointLoader();
+    entrypointLoader.setTrustedTypesPolicy(flutterTT.policy);
+    // Install the `didCreateEngineInitializer` listener where Flutter web expects it to be.
+    this.didCreateEngineInitializer =
+      entrypointLoader.didCreateEngineInitializer.bind(entrypointLoader);
+    return entrypointLoader.loadEntrypoint(entrypoint);
+  }
+
+  /**
+   * Loads and initializes a flutter application.
+   * @param {Object} options
+   * @param {import("/.types".ServiceWorkerSettings?)} options.serviceWorkerSettings
+   *   Settings for the service worker to be loaded. Can pass `undefined` or
+   *   `null` to not launch a service worker at all.
+   * @param {import("/.types".OnEntryPointLoadedCallback)} options.onEntrypointLoaded
+   *   An optional callback to invoke 
+   * @param {string} options.nonce
+   *   A nonce to be applied to the main JS script when loading it, which may
+   *   be required by the sites Content-Security-Policy.
+   *   For more details, see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src here}.
+   * @param {import("./types".FlutterConfiguration)} arg.config
+   */
+  async load({
+    serviceWorkerSettings,
+    onEntrypointLoaded,
+    nonce,
+    config,
+  } = {}) {
+    config ??= {};
+
+    /** @type {import("./types").BuildConfig} */
+    const buildConfig = _flutter.buildConfig;
+    if (!buildConfig) {
+      throw "FlutterLoader.load requires _flutter.buildConfig to be set";
+    }
+
+    const rendererIsCompatible = (renderer) => {
+      switch (renderer) {
+        case "skwasm":
+          return browserEnvironment.crossOriginIsolated
+            && browserEnvironment.hasChromiumBreakIterators
+            && browserEnvironment.hasImageCodecs
+            && browserEnvironment.supportsWasmGC;
+        default:
+          return true;
+      }
+    }
+
+    const buildIsCompatible = (build) => {
+      if (build.compileTarget === "dart2wasm" && !browserEnvironment.supportsWasmGC) {
+        return false;
+      }
+      if (config.renderer && config.renderer != build.renderer) {
+        return false;
+      }
+      return rendererIsCompatible(build.renderer);
+    };
+    const build = buildConfig.builds.find(buildIsCompatible);
+    if (!build) {
+      throw "FlutterLoader could not find a build compatible with configuration and environment.";
+    }
+
+    const deps = {};
+    deps.flutterTT = new FlutterTrustedTypesPolicy();
+    if (serviceWorkerSettings) {
+      deps.serviceWorkerLoader = new FlutterServiceWorkerLoader();
+      deps.serviceWorkerLoader.setTrustedTypesPolicy(deps.flutterTT.policy);
+      await deps.serviceWorkerLoader.loadServiceWorker(serviceWorkerSettings).catch(e => {
+        // Regardless of what happens with the injection of the SW, the show must go on
+        console.warn("Exception while loading service worker:", e);
+      });
+    }
+
+    if (build.renderer === "canvaskit") {
+      deps.canvasKit = loadCanvasKit(deps, config, browserEnvironment, buildConfig.engineRevision);
+    } else if (build.renderer === "skwasm") {
+      deps.skwasm = loadSkwasm(deps, config, browserEnvironment, buildConfig.engineRevision);
+    }
+
+    // The FlutterEntrypointLoader instance could be injected as a dependency
+    // (and dynamically imported from a module if not present).
+    const entrypointLoader = new FlutterEntrypointLoader();
+    entrypointLoader.setTrustedTypesPolicy(deps.flutterTT.policy);
+    // Install the `didCreateEngineInitializer` listener where Flutter web expects it to be.
+    this.didCreateEngineInitializer =
+      entrypointLoader.didCreateEngineInitializer.bind(entrypointLoader);
+    return entrypointLoader.load(build, deps, config, nonce, onEntrypointLoaded);
+  }
+}
diff --git a/lib/web_ui/flutter_js/src/service_worker_loader.js b/lib/web_ui/flutter_js/src/service_worker_loader.js
new file mode 100644
index 0000000..7c4ff3e
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/service_worker_loader.js
@@ -0,0 +1,152 @@
+// Copyright 2013 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.
+
+import { baseUri } from "./base_uri.js";
+
+/**
+ * Wraps `promise` in a timeout of the given `duration` in ms.
+ *
+ * Resolves/rejects with whatever the original `promises` does, or rejects
+ * if `promise` takes longer to complete than `duration`. In that case,
+ * `debugName` is used to compose a legible error message.
+ *
+ * If `duration` is < 0, the original `promise` is returned unchanged.
+ * @param {Promise} promise
+ * @param {number} duration
+ * @param {string} debugName
+ * @returns {Promise} a wrapped promise.
+ */
+async function timeout(promise, duration, debugName) {
+  if (duration < 0) {
+    return promise;
+  }
+  let timeoutId;
+  const _clock = new Promise((_, reject) => {
+    timeoutId = setTimeout(() => {
+      reject(
+        new Error(
+          `${debugName} took more than ${duration}ms to resolve. Moving on.`,
+          {
+            cause: timeout,
+          }
+        )
+      );
+    }, duration);
+  });
+  return Promise.race([promise, _clock]).finally(() => {
+    clearTimeout(timeoutId);
+  });
+}
+
+/**
+ * Handles loading/reloading Flutter's service worker, if configured.
+ *
+ * @see: https://developers.google.com/web/fundamentals/primers/service-workers
+ */
+export class FlutterServiceWorkerLoader {
+  /**
+   * Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
+   * @param {TrustedTypesPolicy | undefined} policy
+   */
+  setTrustedTypesPolicy(policy) {
+    this._ttPolicy = policy;
+  }
+  /**
+   * Returns a Promise that resolves when the latest Flutter service worker,
+   * configured by `settings` has been loaded and activated.
+   *
+   * Otherwise, the promise is rejected with an error message.
+   * @param {import("./types").ServiceWorkerSettings} settings Service worker settings
+   * @returns {Promise} that resolves when the latest serviceWorker is ready.
+   */
+  loadServiceWorker(settings) {
+    if (!settings) {
+      // In the future, settings = null -> uninstall service worker?
+      console.debug("Null serviceWorker configuration. Skipping.");
+      return Promise.resolve();
+    }
+    if (!("serviceWorker" in navigator)) {
+      let errorMessage = "Service Worker API unavailable.";
+      if (!window.isSecureContext) {
+        errorMessage += "\nThe current context is NOT secure."
+        errorMessage += "\nRead more: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts";
+      }
+      return Promise.reject(
+        new Error(errorMessage)
+      );
+    }
+    const {
+      serviceWorkerVersion,
+      serviceWorkerUrl = `${baseUri}flutter_service_worker.js?v=${serviceWorkerVersion}`,
+      timeoutMillis = 4000,
+    } = settings;
+    // Apply the TrustedTypes policy, if present.
+    let url = serviceWorkerUrl;
+    if (this._ttPolicy != null) {
+      url = this._ttPolicy.createScriptURL(url);
+    }
+    const serviceWorkerActivation = navigator.serviceWorker
+      .register(url)
+      .then((serviceWorkerRegistration) => this._getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion))
+      .then(this._waitForServiceWorkerActivation);
+    // Timeout race promise
+    return timeout(
+      serviceWorkerActivation,
+      timeoutMillis,
+      "prepareServiceWorker"
+    );
+  }
+  /**
+   * Returns the latest service worker for the given `serviceWorkerRegistration`.
+   *
+   * This might return the current service worker, if there's no new service worker
+   * awaiting to be installed/updated.
+   *
+   * @param {ServiceWorkerRegistration} serviceWorkerRegistration
+   * @param {string} serviceWorkerVersion
+   * @returns {Promise<ServiceWorker>}
+   */
+  async _getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion) {
+    if (!serviceWorkerRegistration.active && (serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting)) {
+      // No active web worker and we have installed or are installing
+      // one for the first time. Simply wait for it to activate.
+      console.debug("Installing/Activating first service worker.");
+      return serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting;
+    } else if (!serviceWorkerRegistration.active.scriptURL.endsWith(serviceWorkerVersion)) {
+      // When the app updates the serviceWorkerVersion changes, so we
+      // need to ask the service worker to update.
+      const newRegistration = await serviceWorkerRegistration.update();
+      console.debug("Updating service worker.");
+      return newRegistration.installing || newRegistration.waiting || newRegistration.active;
+    } else {
+      console.debug("Loading from existing service worker.");
+      return serviceWorkerRegistration.active;
+    }
+  }
+  /**
+   * Returns a Promise that resolves when the `serviceWorker` changes its
+   * state to "activated".
+   *
+   * @param {ServiceWorker} serviceWorker
+   * @returns {Promise<void>}
+   */
+  async _waitForServiceWorkerActivation(serviceWorker) {
+    if (!serviceWorker || serviceWorker.state === "activated") {
+      if (!serviceWorker) {
+        throw new Error("Cannot activate a null service worker!");
+      } else {
+        console.debug("Service worker already active.");
+        return;
+      }
+    }
+    return new Promise((resolve, _) => {
+      serviceWorker.addEventListener("statechange", () => {
+        if (serviceWorker.state === "activated") {
+          console.debug("Activated new service worker.");
+          resolve();
+        }
+      });
+    });
+  }
+}
diff --git a/lib/web_ui/flutter_js/src/skwasm_loader.js b/lib/web_ui/flutter_js/src/skwasm_loader.js
new file mode 100644
index 0000000..86e1b52
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/skwasm_loader.js
@@ -0,0 +1,47 @@
+// Copyright 2013 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.
+
+import { createWasmInstantiator } from "./instantiate_wasm.js";
+
+export const loadSkwasm = (deps, config, browserEnvironment, engineRevision) => {
+  return new Promise((resolve, reject) => {
+    const baseUrl = config.canvasKitBaseUrl ?? `https://www.gstatic.com/flutter-canvaskit/${engineRevision}/`;
+    let skwasmUrl = `${baseUrl}skwasm.js`;
+    if (deps.flutterTT.policy) {
+      skwasmUrl = deps.flutterTT.policy.createScriptURL(skwasmUrl);
+    }
+    const wasmInstantiator = createWasmInstantiator(`${baseUrl}skwasm.wasm`);
+    const script = document.createElement("script");
+    script.src = skwasmUrl;
+    if (config.nonce) {
+      script.nonce = config.nonce;
+    }
+    script.addEventListener('load', async () => {
+      try {
+        const skwasmInstance = await skwasm({
+          instantiateWasm: wasmInstantiator,
+          locateFile: (fileName, scriptDirectory) => {
+            // When hosted via a CDN or some other url that is not the same
+            // origin as the main script of the page, we will fail to create
+            // a web worker with the .worker.js script. This workaround will
+            // make sure that the worker JS can be loaded regardless of where
+            // it is hosted.
+            const url = scriptDirectory + fileName;
+            if (url.endsWith('.worker.js')) {
+              return URL.createObjectURL(new Blob(
+                [`importScripts('${url}');`],
+                { 'type': 'application/javascript' }));
+            }
+            return url;
+          }
+        });
+        resolve(skwasmInstance);
+      } catch (e) {
+        reject(e);
+      }
+    });
+    script.addEventListener('error', reject);
+    document.head.appendChild(script);
+  });
+}
diff --git a/lib/web_ui/flutter_js/src/trusted_types.js b/lib/web_ui/flutter_js/src/trusted_types.js
new file mode 100644
index 0000000..76db604
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/trusted_types.js
@@ -0,0 +1,36 @@
+// Copyright 2013 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.
+
+/**
+ * Handles the creation of a TrustedTypes `policy` that validates URLs based
+ * on an (optional) incoming array of RegExes.
+ */
+export class FlutterTrustedTypesPolicy {
+  /**
+   * Constructs the policy.
+   * @param {[RegExp]} validPatterns the patterns to test URLs
+   * @param {String} policyName the policy name (optional)
+   */
+  constructor(validPatterns, policyName = "flutter-js") {
+    const patterns = validPatterns || [
+      /\.js$/,
+      /\.mjs$/,
+    ];
+    if (window.trustedTypes) {
+      this.policy = trustedTypes.createPolicy(policyName, {
+        createScriptURL: function (url) {
+          const parsed = new URL(url, window.location);
+          const file = parsed.pathname.split("/").pop();
+          const matches = patterns.some((pattern) => pattern.test(file));
+          if (matches) {
+            return parsed.toString();
+          }
+          console.error(
+            "URL rejected by TrustedTypes policy",
+            policyName, ":", url, "(download prevented)");
+        }
+      });
+    }
+  }
+}
diff --git a/lib/web_ui/flutter_js/src/types.d.ts b/lib/web_ui/flutter_js/src/types.d.ts
new file mode 100644
index 0000000..9cd1f7a
--- /dev/null
+++ b/lib/web_ui/flutter_js/src/types.d.ts
@@ -0,0 +1,73 @@
+// Copyright 2013 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.
+
+type JSCompileTarget = "dart2js" | "dartdevc";
+type WasmCompileTarget = "dart2wasm";
+
+export type CompileTarget = JSCompileTarget | WasmCompileTarget;
+
+export type WebRenderer =
+  "html" |
+  "canvaskit" |
+  "skwasm";
+
+interface ApplicationBuildBase {
+  renderer: WebRenderer;
+}
+
+export interface JSApplicationBuild extends ApplicationBuildBase {
+  compileTarget: JSCompileTarget;
+  mainJsPath: string;
+}
+
+export interface WasmApplicationBuild extends ApplicationBuildBase {
+  compileTarget: WasmCompileTarget;
+  mainWasmPath: string;
+  jsSupportRuntimePath: string;
+}
+
+export type ApplicationBuild = JSApplicationBuild | WasmApplicationBuild;
+
+export interface BuildConfig {
+  serviceWorkerVersion: string;
+  engineRevision: string;
+  builds: ApplicationBuild[];
+}
+
+export interface BrowserEnvironment {
+  hasImageCodecs: boolean;
+  hasChromiumBreakIterators: boolean;
+  supportsWasmGC: boolean;
+  crossOriginIsolated: boolean;
+}
+
+type CanvasKitVariant =
+  "auto" |
+  "full" |
+  "chromium";
+
+export interface FlutterConfiguration {
+  assetBase: string?;
+  canvasKitBaseUrl: string?;
+  canvasKitVariant: CanvasKitVariant?;
+  renderer: WebRenderer?;
+  hostElement: HtmlElement?;
+}
+
+export interface ServiceWorkerSettings {
+  serviceWorkerVersion: string;
+  serviceWorkerUrl: string?;
+  timeoutMillis: number?;
+}
+
+export interface AppRunner {
+  runApp: () => void;
+}
+
+export interface EngineInitializer {
+  initializeEngine: () => Promise<AppRunner>;
+}
+
+export type OnEntrypointLoadedCallback =
+  (initializer: EngineInitializer) => void;
diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
index a0de371..39608d2 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
@@ -43,6 +43,9 @@
 @JS('window.flutterCanvasKit')
 external CanvasKit? get windowFlutterCanvasKit;
 
+@JS('window.flutterCanvasKitLoaded')
+external JSPromise<JSAny>? get windowFlutterCanvasKitLoaded;
+
 @JS()
 @anonymous
 @staticInterop
diff --git a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart
index deb63b4..b3a8b3b 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart
@@ -65,6 +65,9 @@
     _initialized ??= () async {
       if (windowFlutterCanvasKit != null) {
         canvasKit = windowFlutterCanvasKit!;
+      } else if (windowFlutterCanvasKitLoaded != null) {
+        // CanvasKit is being preloaded by flutter.js. Wait for it to complete.
+        canvasKit = await promiseToFuture<CanvasKit>(windowFlutterCanvasKitLoaded!);
       } else {
         canvasKit = await downloadCanvasKit();
         windowFlutterCanvasKit = canvasKit;
diff --git a/lib/web_ui/test/canvaskit/hot_restart_test.dart b/lib/web_ui/test/canvaskit/hot_restart_test.dart
index b032f53..3ee6903 100644
--- a/lib/web_ui/test/canvaskit/hot_restart_test.dart
+++ b/lib/web_ui/test/canvaskit/hot_restart_test.dart
@@ -12,8 +12,6 @@
 
 void testMain() {
   test('CanvasKit reuses the instance already set on `window`', () async {
-    expect(windowFlutterCanvasKit, isNull);
-
     // First initialization should make CanvasKit available through `window`.
     await renderer.initialize();
     expect(windowFlutterCanvasKit, isNotNull);
diff --git a/lib/web_ui/test/canvaskit/initialization/services_vs_ui_test.dart b/lib/web_ui/test/canvaskit/initialization/services_vs_ui_test.dart
index 0cfd55a..709b88a 100644
--- a/lib/web_ui/test/canvaskit/initialization/services_vs_ui_test.dart
+++ b/lib/web_ui/test/canvaskit/initialization/services_vs_ui_test.dart
@@ -16,7 +16,6 @@
   test('services are initalized separately from UI', () async {
     final JsFlutterConfiguration? config = await bootstrapAndExtractConfig();
     expect(scheduleFrameCallback, isNull);
-    expect(windowFlutterCanvasKit, isNull);
 
     expect(findGlassPane(), isNull);
     expect(RawKeyboard.instance, isNull);
diff --git a/lib/web_ui/test/fallbacks/fallbacks_test.dart b/lib/web_ui/test/fallbacks/fallbacks_test.dart
new file mode 100644
index 0000000..da9ac9a
--- /dev/null
+++ b/lib/web_ui/test/fallbacks/fallbacks_test.dart
@@ -0,0 +1,31 @@
+// Copyright 2013 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.
+
+
+import 'package:test/bootstrap/browser.dart';
+import 'package:test/test.dart';
+import 'package:ui/src/engine.dart';
+
+import '../common/test_initialization.dart';
+import '../ui/utils.dart';
+
+void main() {
+  internalBootstrapBrowserTest(() => testMain);
+}
+
+Future<void> testMain() async {
+  setUpUnitTests(
+    setUpTestViewDimensions: false,
+  );
+
+  test('bootstrapper selects correct builds', () {
+    if (browserEngine == BrowserEngine.blink) {
+      expect(isWasm, isTrue);
+      expect(isSkwasm, isTrue);
+    } else {
+      expect(isWasm, isFalse);
+      expect(isCanvasKit, isTrue);
+    }
+  });
+}
diff --git a/lib/web_ui/test/felt_config.yaml b/lib/web_ui/test/felt_config.yaml
index 6cc145b..2e20a35 100644
--- a/lib/web_ui/test/felt_config.yaml
+++ b/lib/web_ui/test/felt_config.yaml
@@ -42,59 +42,60 @@
   - name: ui
     directory: ui
 
-  # This just has a single test that makes sure the skwasm stub renderer is
-  # included when compiling to JS
-  - name: skwasm_stub
-    directory: skwasm_stub
+  # Tests for fallback functionality between build variants
+  - name: fallbacks
+    directory: fallbacks
 
 test-bundles:
   - name: dart2js-html-engine
     test-set: engine
-    compile-config: dart2js-html
+    compile-configs: dart2js-html
 
   - name: dart2js-html-html
     test-set: html
-    compile-config: dart2js-html
+    compile-configs: dart2js-html
 
   - name: dart2js-html-ui
     test-set: ui
-    compile-config: dart2js-html
+    compile-configs: dart2js-html
 
   - name: dart2js-canvaskit-canvaskit
     test-set: canvaskit
-    compile-config: dart2js-canvaskit
+    compile-configs: dart2js-canvaskit
 
   - name: dart2js-canvaskit-ui
     test-set: ui
-    compile-config: dart2js-canvaskit
-
-  - name: dart2js-skwasm-skwasm_stub
-    test-set: skwasm_stub
-    compile-config: dart2js-skwasm
+    compile-configs: dart2js-canvaskit
 
   - name: dart2wasm-html-engine
     test-set: engine
-    compile-config: dart2wasm-html
+    compile-configs: dart2wasm-html
 
   - name: dart2wasm-html-html
     test-set: html
-    compile-config: dart2wasm-html
+    compile-configs: dart2wasm-html
 
   - name: dart2wasm-html-ui
     test-set: ui
-    compile-config: dart2wasm-html
+    compile-configs: dart2wasm-html
 
   - name: dart2wasm-canvaskit-canvaskit
     test-set: canvaskit
-    compile-config: dart2wasm-canvaskit
+    compile-configs: dart2wasm-canvaskit
 
   - name: dart2wasm-canvaskit-ui
     test-set: ui
-    compile-config: dart2wasm-canvaskit
+    compile-configs: dart2wasm-canvaskit
 
   - name: dart2wasm-skwasm-ui
     test-set: ui
-    compile-config: dart2wasm-skwasm
+    compile-configs: dart2wasm-skwasm
+
+  - name: fallbacks
+    test-set: fallbacks
+    compile-configs:
+      - dart2wasm-skwasm
+      - dart2js-canvaskit
 
 run-configs:
   - name: chrome
@@ -144,10 +145,6 @@
     run-config: chrome
     artifact-deps: [ canvaskit_chromium ]
 
-  - name: chrome-dart2js-skwasm-skwasm_stub
-    test-bundle: dart2js-skwasm-skwasm_stub
-    run-config: chrome
-
   - name: chrome-full-dart2js-canvaskit-canvaskit
     test-bundle: dart2js-canvaskit-canvaskit
     run-config: chrome-full
@@ -270,3 +267,18 @@
     test-bundle: dart2wasm-canvaskit-ui
     run-config: chrome-full
     artifact-deps: [ canvaskit ]
+
+  - name: chrome-fallbacks
+    test-bundle: fallbacks
+    run-config: chrome
+    artifact-deps: [ canvaskit, skwasm ]
+
+  - name: firefox-fallbacks
+    test-bundle: fallbacks
+    run-config: firefox
+    artifact-deps: [ canvaskit ]
+
+  - name: safari-fallbacks
+    test-bundle: fallbacks
+    run-config: safari
+    artifact-deps: [ canvaskit ]
diff --git a/lib/web_ui/test/skwasm_stub/smoke_test.dart b/lib/web_ui/test/skwasm_stub/smoke_test.dart
deleted file mode 100644
index 7361d9c..0000000
--- a/lib/web_ui/test/skwasm_stub/smoke_test.dart
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright 2013 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.
-
-@TestOn('chrome || safari || firefox')
-library;
-
-import 'dart:async';
-
-import 'package:test/bootstrap/browser.dart';
-import 'package:test/test.dart';
-import 'package:ui/src/engine/renderer.dart';
-import 'package:ui/src/engine/skwasm/skwasm_stub/renderer.dart';
-
-void main() {
-  internalBootstrapBrowserTest(() => testMain);
-}
-
-Future<void> testMain() async {
-  group('Skwasm stub tests', () {
-    test('Skwasm stub renderer throws', () {
-      expect(renderer, isA<SkwasmRenderer>());
-      expect(() {
-        renderer.initialize();
-      }, throwsUnimplementedError);
-    });
-  });
-}