Make large jumpTo recommend deferred loading (#64271)

diff --git a/packages/flutter/lib/src/widgets/scroll_position.dart b/packages/flutter/lib/src/widgets/scroll_position.dart
index f58d83a..3f805cc 100644
--- a/packages/flutter/lib/src/widgets/scroll_position.dart
+++ b/packages/flutter/lib/src/widgets/scroll_position.dart
@@ -145,6 +145,24 @@
   double get maxScrollExtent => _maxScrollExtent;
   double _maxScrollExtent;
 
+  /// The additional velocity added for a [forcePixels] change in a single
+  /// frame.
+  ///
+  /// This value is used by [recommendDeferredLoading] in addition to the
+  /// [activity]'s [ScrollActivity.velocity] to ask the [physics] whether or
+  /// not to defer loading. It accounts for the fact that a [forcePixels] call
+  /// may involve a [ScrollActivity] with 0 velocity, but the scrollable is
+  /// still instantaneously moving from its current position to a potentially
+  /// very far position, and which is of interest to callers of
+  /// [recommendDeferredLoading].
+  ///
+  /// For example, if a scrollable is currently at 5000 pixels, and we [jumpTo]
+  /// 0 to get back to the top of the list, we would have an implied velocity of
+  /// -5000 and an `activity.velocity` of 0. The jump may be going past a
+  /// number of resource intensive widgets which should avoid doing work if the
+  /// position jumps past them.
+  double _impliedVelocity = 0;
+
   @override
   double get pixels => _pixels;
   double _pixels;
@@ -343,8 +361,13 @@
   @protected
   void forcePixels(double value) {
     assert(pixels != null);
+    assert(value != null);
+    _impliedVelocity = value - _pixels;
     _pixels = value;
     notifyListeners();
+    SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
+      _impliedVelocity = 0;
+    });
   }
 
   /// Called whenever scrolling ends, to store the current scroll offset in a
@@ -834,7 +857,12 @@
     assert(context != null);
     assert(activity != null);
     assert(activity.velocity != null);
-    return physics.recommendDeferredLoading(activity.velocity, copyWith(), context);
+    assert(_impliedVelocity != null);
+    return physics.recommendDeferredLoading(
+      activity.velocity + _impliedVelocity,
+      copyWith(),
+      context,
+    );
   }
 
   @override
diff --git a/packages/flutter/test/widgets/scroll_position_test.dart b/packages/flutter/test/widgets/scroll_position_test.dart
index 11aa00a..f536e9f 100644
--- a/packages/flutter/test/widgets/scroll_position_test.dart
+++ b/packages/flutter/test/widgets/scroll_position_test.dart
@@ -229,4 +229,41 @@
     );
     expect(controller.position.pixels, equals(0.0));
   });
+
+  testWidgets('jumpTo recomends deferred loading', (WidgetTester tester) async {
+    int loadedWithDeferral = 0;
+    int buildCount = 0;
+    const double height = 500;
+    await tester.pumpWidget(MaterialApp(
+      home: ListView.builder(
+        itemBuilder: (BuildContext context, int index) {
+          buildCount += 1;
+          if (Scrollable.recommendDeferredLoadingForContext(context)) {
+            loadedWithDeferral += 1;
+          }
+          return const SizedBox(height: height);
+        },
+      ),
+    ));
+
+    // The two visible on screen should have loaded without deferral.
+    expect(buildCount, 2);
+    expect(loadedWithDeferral, 0);
+
+    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
+    position.jumpTo(height * 100);
+    await tester.pump();
+
+    // All but the first two that were loaded normally should have gotten a
+    // recommendation to defer.
+    expect(buildCount, 102);
+    expect(loadedWithDeferral, 100);
+
+    position.jumpTo(height * 102);
+    await tester.pump();
+
+    // The smaller jump should not have recommended deferral.
+    expect(buildCount, 104);
+    expect(loadedWithDeferral, 100);
+  });
 }