skip painting clipped out pictures (#18204)

diff --git a/lib/web_ui/lib/src/engine/surface/picture.dart b/lib/web_ui/lib/src/engine/surface/picture.dart
index 50d77b0..9ecb9cf 100644
--- a/lib/web_ui/lib/src/engine/surface/picture.dart
+++ b/lib/web_ui/lib/src/engine/surface/picture.dart
@@ -180,7 +180,11 @@
       return 1.0;
     } else {
       final BitmapCanvas oldCanvas = existingSurface._canvas;
-      if (!oldCanvas.doesFitBounds(_exactLocalCullRect)) {
+      if (oldCanvas == null) {
+        // We did not allocate a canvas last time. This can happen when the
+        // picture is completely clipped out of the view.
+        return 1.0;
+      } else if (!oldCanvas.doesFitBounds(_exactLocalCullRect)) {
         // The canvas needs to be resized before painting.
         return 1.0;
       } else {
@@ -547,7 +551,10 @@
 
   void _applyPaint(PersistedPicture oldSurface) {
     final EngineCanvas oldCanvas = oldSurface?._canvas;
-    if (!picture.recordingCanvas.didDraw) {
+    if (!picture.recordingCanvas.didDraw || _optimalLocalCullRect.isEmpty) {
+      // The picture is empty, or it has been completely clipped out. Skip
+      // painting. This removes all the setup work and scaffolding objects
+      // that won't be useful for anything anyway.
       _recycleCanvas(oldCanvas);
       domRenderer.clearDom(rootElement);
       return;
@@ -642,7 +649,7 @@
     super.debugValidate(validationErrors);
 
     if (picture.recordingCanvas.didDraw) {
-      if (debugCanvas == null) {
+      if (!_optimalLocalCullRect.isEmpty && debugCanvas == null) {
         validationErrors
             .add('$runtimeType has non-trivial picture but it has null canvas');
       }
diff --git a/lib/web_ui/test/compositing_test.dart b/lib/web_ui/test/engine/surface/scene_builder_test.dart
similarity index 92%
rename from lib/web_ui/test/compositing_test.dart
rename to lib/web_ui/test/engine/surface/scene_builder_test.dart
index 93925bd..6006b2f 100644
--- a/lib/web_ui/test/compositing_test.dart
+++ b/lib/web_ui/test/engine/surface/scene_builder_test.dart
@@ -10,7 +10,7 @@
 
 import 'package:test/test.dart';
 
-import 'matchers.dart';
+import '../../matchers.dart';
 
 void main() {
   group('SceneBuilder', () {
@@ -239,6 +239,31 @@
     });
   });
 
+  test('skips painting picture when picture fully clipped out', () async {
+    final Picture picture = _drawPicture();
+
+    // Picture not clipped out, so we should see a `<flt-canvas>`
+    {
+      final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
+      builder.pushOffset(0, 0);
+      builder.addPicture(Offset.zero, picture);
+      builder.pop();
+      html.HtmlElement content = builder.build().webOnlyRootElement;
+      expect(content.querySelectorAll('flt-picture').single.children, isNotEmpty);
+    }
+
+    // Picture fully clipped out, so we should not see a `<flt-canvas>`
+    {
+      final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
+      builder.pushOffset(0, 0);
+      builder.pushClipRect(const Rect.fromLTRB(1000, 1000, 2000, 2000));
+      builder.addPicture(Offset.zero, picture);
+      builder.pop();
+      builder.pop();
+      html.HtmlElement content = builder.build().webOnlyRootElement;
+      expect(content.querySelectorAll('flt-picture').single.children, isEmpty);
+    }
+  });
 }
 
 typedef TestLayerBuilder = EngineLayer Function(