[web] Avoid absolute positioning for base CanvasKit canvas (#188337)

fix for #176327
Partial fix for #181511

**Problem**

With CanvasKit and semantics enabled, scrolling regular Flutter Web
content on iPhone WebKit browsers can be very janky. The base CanvasKit
canvas was always styled with `position:absolute`, which is expensive
for WebKit to scroll together with the semantics DOM.

**Fix**

Only platform-view overlay canvases keep `position:absolute`. The normal
base CanvasKit canvas now uses regular positioning, which fixes the
plain Flutter scroll case while preserving `HtmlElementView` stacking.

One limitation: mixed Flutter and `HtmlElementView` scenes may still
show iOS WebKit scroll jank because platform-view overlays still require
`position:absolute`.

**Demo**

| Before | After |
|---|---|
| https://flutter-demo-09-before.web.app |
https://flutter-demo-09-after.web.app |

Steps: open the page on iPhone Chrome or Safari, keep semantics enabled,
then scroll the page continuously.

Before: the on-screen jank log shows severe scroll jank, including runs
over 100 janky frames per 5 seconds.

After: the same regular Flutter content stays smooth, with low jank
summaries around 1 to 10 janky frames per 5 seconds in validation runs.
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart
index f6b6d2f..ab26a7a 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart
@@ -392,6 +392,18 @@
   }
 
   @override
+  void setIsOverlay(bool isOverlay) {
+    final DomCSSStyleDeclaration style = (canvas as DomHTMLCanvasElement).style;
+    if (isOverlay) {
+      if (style.position != 'absolute') {
+        style.position = 'absolute';
+      }
+    } else if (style.position == 'absolute') {
+      style.removeProperty('position');
+    }
+  }
+
+  @override
   Future<void> triggerContextLoss() async {
     _handledContextLostEvent = Completer<void>();
     final WebGLContext gl = (canvas as DomHTMLCanvasElement).getGlContext(webGLVersion);
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/canvas_provider.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/canvas_provider.dart
index b9f55e0..f48fa59 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/canvas_provider.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/canvas_provider.dart
@@ -105,7 +105,6 @@
     final cssHeight = '${size.height / ratio}px';
     canvas.style
       ..width = cssWidth
-      ..height = cssHeight
-      ..position = 'absolute';
+      ..height = cssHeight;
   }
 }
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/rasterizer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/rasterizer.dart
index 8ee65ca..4a7f35c 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/rasterizer.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/rasterizer.dart
@@ -164,6 +164,9 @@
   /// Initialize the overlay.
   void initialize();
 
+  /// Marks whether this canvas is used as an overlay between platform views.
+  void setIsOverlay(bool isOverlay) {}
+
   /// Disposes this overlay.
   void dispose();
 }
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/embedder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/embedder.dart
index 7cdb059..dddbb02 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/embedder.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/embedder.dart
@@ -454,6 +454,19 @@
       // The composition has not changed, so no DOM manipulation is needed.
       return;
     }
+    // A canvas needs to be stacked as an overlay (and thus styled with
+    // `position: absolute`) whenever it shares the composition with a
+    // platform view, since the platform view's DOM element sits between
+    // canvases in the stacking order. It also needs overlay positioning
+    // whenever there's more than one canvas, even without platform views,
+    // since multiple canvases need to occupy the same area without
+    // affecting each other's layout. A single canvas with no platform
+    // views can use normal positioning, which is cheaper for the browser
+    // to scroll.
+    final bool hasPlatformViews = composition.entities.any(
+      (CompositionEntity entity) => entity is CompositionPlatformView,
+    );
+    final bool needsOverlayPositioning = hasPlatformViews || composition.canvases.length > 1;
     final List<int> indexMap = _getIndexMapFromPreviousComposition(_activeComposition, composition);
     final List<int> existingIndexMap = indexMap.where((int index) => index != -1).toList();
 
@@ -501,6 +514,7 @@
         // canvas needs a display canvas.
         canvas.displayCanvas = rasterizer.getOverlay();
       }
+      canvas.displayCanvas!.setIsOverlay(needsOverlayPositioning);
     }
 
     // At this point, the DOM contains the static elements and the elements from
diff --git a/engine/src/flutter/lib/web_ui/test/ui/platform_view_position_test.dart b/engine/src/flutter/lib/web_ui/test/ui/platform_view_position_test.dart
index f01ff23..170c4c7 100644
--- a/engine/src/flutter/lib/web_ui/test/ui/platform_view_position_test.dart
+++ b/engine/src/flutter/lib/web_ui/test/ui/platform_view_position_test.dart
@@ -22,7 +22,48 @@
 Future<void> testMain() async {
   setUpUnitTests(withImplicitView: true, setUpTestViewDimensions: false);
 
-  test('Onscreen canvas has position: absolute', () async {
+  test('base onscreen canvas is not positioned as an overlay', () async {
+    if (!isCanvasKit) {
+      return;
+    }
+
+    // Force multi-surface mode to ensure OnscreenCanvasProvider is used.
+    engine.debugOverrideJsConfiguration(
+      <String, Object?>{'canvasKitForceMultiSurfaceRasterizer': true}.jsify()
+          as engine.JsFlutterConfiguration?,
+    );
+    // Reset the renderer to ensure it is created with the new configuration.
+    engine.renderer.debugResetRasterizer();
+    engine.renderer.debugClear();
+
+    // Create a scene with only Flutter content. This uses the base canvas, not
+    // an overlay canvas interleaved with platform views.
+    final recorder = ui.PictureRecorder();
+    final canvas = ui.Canvas(recorder);
+    canvas.drawRect(
+      const ui.Rect.fromLTWH(0, 0, 100, 100),
+      ui.Paint()..color = const ui.Color(0xFFFF0000),
+    );
+
+    final sb = ui.SceneBuilder();
+    sb.addPicture(ui.Offset.zero, recorder.endRecording());
+
+    await renderScene(sb.build());
+
+    final DomElement canvasElement = (implicitView as engine.EngineFlutterView).dom.sceneHost
+        .querySelectorAll('canvas')
+        .single;
+
+    expect(
+      canvasElement.style.position,
+      isNot('absolute'),
+      reason: 'Base canvas should not be styled as a platform-view overlay.',
+    );
+
+    engine.debugOverrideJsConfiguration(null);
+  });
+
+  test('Onscreen overlay canvas has position: absolute', () async {
     // Force multi-surface mode to ensure OnscreenCanvasProvider is used.
     engine.debugOverrideJsConfiguration(
       <String, Object?>{'canvasKitForceMultiSurfaceRasterizer': true}.jsify()
@@ -57,16 +98,14 @@
 
     await renderScene(sb.build());
 
-    // Find the canvas element.
     final DomElement canvasElement = (implicitView as engine.EngineFlutterView).dom.sceneHost
         .querySelectorAll('canvas')
         .single;
 
-    // Verify position is absolute.
     expect(
       canvasElement.style.position,
       'absolute',
-      reason: 'Canvas should have position: absolute',
+      reason: 'Overlay canvas should have position: absolute.',
     );
 
     engine.debugOverrideJsConfiguration(null);