Revert "Remove the fast reassemble / single widget reload feature (#132255)"

This reverts commit a2e2574941982e3de3eb746f0a00ca90f6d28f04.
diff --git a/packages/flutter/lib/src/foundation/binding.dart b/packages/flutter/lib/src/foundation/binding.dart
index e898b80..83204bd 100644
--- a/packages/flutter/lib/src/foundation/binding.dart
+++ b/packages/flutter/lib/src/foundation/binding.dart
@@ -168,6 +168,13 @@
   static Type? _debugInitializedType;
   static bool _debugServiceExtensionsRegistered = false;
 
+  /// Additional configuration used by the framework during hot reload.
+  ///
+  /// See also:
+  ///
+  ///  * [DebugReassembleConfig], which describes the configuration.
+  static DebugReassembleConfig? debugReassembleConfig;
+
   /// Deprecated. Will be removed in a future version of Flutter.
   ///
   /// This property has been deprecated to prepare for Flutter's upcoming
@@ -982,3 +989,23 @@
 Future<void> _exitApplication() async {
   exit(0);
 }
+
+/// Additional configuration used for hot reload reassemble optimizations.
+///
+/// Do not extend, implement, or mixin this class. This may only be instantiated
+/// in debug mode.
+class DebugReassembleConfig {
+  /// Create a new [DebugReassembleConfig].
+  ///
+  /// Throws a [FlutterError] if this is called in profile or release mode.
+  DebugReassembleConfig({
+    this.widgetName,
+  }) {
+    if (!kDebugMode) {
+      throw FlutterError('Cannot instantiate DebugReassembleConfig in profile or release mode.');
+    }
+  }
+
+  /// The name of the widget that was modified, or `null` if the change was elsewhere.
+  final String? widgetName;
+}
diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart
index bb9bd4f..ab6adac 100644
--- a/packages/flutter/lib/src/rendering/binding.dart
+++ b/packages/flutter/lib/src/rendering/binding.dart
@@ -608,16 +608,18 @@
   @override
   Future<void> performReassemble() async {
     await super.performReassemble();
-    if (!kReleaseMode) {
-      FlutterTimeline.startSync('Preparing Hot Reload (layout)');
-    }
-    try {
-      for (final RenderView renderView in renderViews) {
-        renderView.reassemble();
-      }
-    } finally {
+    if (BindingBase.debugReassembleConfig?.widgetName == null) {
       if (!kReleaseMode) {
-        FlutterTimeline.finishSync();
+        FlutterTimeline.startSync('Preparing Hot Reload (layout)');
+      }
+      try {
+        for (final RenderView renderView in renderViews) {
+          renderView.reassemble();
+        }
+      } finally {
+        if (!kReleaseMode) {
+          FlutterTimeline.finishSync();
+        }
       }
     }
     scheduleWarmUpFrame();
diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart
index ca8347e..900d34f 100644
--- a/packages/flutter/lib/src/widgets/binding.dart
+++ b/packages/flutter/lib/src/widgets/binding.dart
@@ -505,6 +505,23 @@
         },
       );
 
+      registerServiceExtension(
+        name: WidgetsServiceExtensions.fastReassemble.name,
+        callback: (Map<String, Object> params) async {
+          // This mirrors the implementation of the 'reassemble' callback registration
+          // in lib/src/foundation/binding.dart, but with the extra binding config used
+          // to skip some reassemble work.
+          final String? className = params['className'] as String?;
+          BindingBase.debugReassembleConfig = DebugReassembleConfig(widgetName: className);
+          try {
+            await reassembleApplication();
+          } finally {
+            BindingBase.debugReassembleConfig = null;
+          }
+          return <String, String>{'type': 'Success'};
+        },
+      );
+
       // Expose the ability to send Widget rebuilds as [Timeline] events.
       registerBoolServiceExtension(
         name: WidgetsServiceExtensions.profileWidgetBuilds.name,
@@ -543,7 +560,7 @@
 
   Future<void> _forceRebuild() {
     if (rootElement != null) {
-      buildOwner!.reassemble(rootElement!);
+      buildOwner!.reassemble(rootElement!, null);
       return endOfFrame;
     }
     return Future<void>.value();
@@ -1073,7 +1090,7 @@
     }());
 
     if (rootElement != null) {
-      buildOwner!.reassemble(rootElement!);
+      buildOwner!.reassemble(rootElement!, BindingBase.debugReassembleConfig);
     }
     return super.performReassemble();
   }
diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index c1c45fc..6c5c231 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -3252,13 +3252,14 @@
   /// changed implementations.
   ///
   /// This is expensive and should not be called except during development.
-  void reassemble(Element root) {
+  void reassemble(Element root, DebugReassembleConfig? reassembleConfig) {
     if (!kReleaseMode) {
       FlutterTimeline.startSync('Preparing Hot Reload (widgets)');
     }
     try {
       assert(root._parent == null);
       assert(root.owner == this);
+      root._debugReassembleConfig = reassembleConfig;
       root.reassemble();
     } finally {
       if (!kReleaseMode) {
@@ -3373,6 +3374,7 @@
   }
 
   Element? _parent;
+  DebugReassembleConfig? _debugReassembleConfig;
   _NotificationNode? _notificationTree;
 
   /// Compare two widgets for equality.
@@ -3524,10 +3526,15 @@
   @mustCallSuper
   @protected
   void reassemble() {
-    markNeedsBuild();
+    if (_debugShouldReassemble(_debugReassembleConfig, _widget)) {
+      markNeedsBuild();
+      _debugReassembleConfig = null;
+    }
     visitChildren((Element child) {
+      child._debugReassembleConfig = _debugReassembleConfig;
       child.reassemble();
     });
+    _debugReassembleConfig = null;
   }
 
   bool _debugIsInScope(Element target) {
@@ -5578,7 +5585,9 @@
 
   @override
   void reassemble() {
-    state.reassemble();
+    if (_debugShouldReassemble(_debugReassembleConfig, _widget)) {
+      state.reassemble();
+    }
     super.reassemble();
   }
 
@@ -6943,3 +6952,9 @@
   @override
   Element createElement() => throw UnimplementedError();
 }
+
+// Whether a [DebugReassembleConfig] indicates that an element holding [widget] can skip
+// a reassemble.
+bool _debugShouldReassemble(DebugReassembleConfig? config, Widget? widget) {
+  return config == null || config.widgetName == null || widget?.runtimeType.toString() == config.widgetName;
+}
diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart
index 5873aaa..7c56bed 100644
--- a/packages/flutter/lib/src/widgets/widget_inspector.dart
+++ b/packages/flutter/lib/src/widgets/widget_inspector.dart
@@ -970,7 +970,7 @@
   Future<void> forceRebuild() {
     final WidgetsBinding binding = WidgetsBinding.instance;
     if (binding.rootElement != null) {
-      binding.buildOwner!.reassemble(binding.rootElement!);
+      binding.buildOwner!.reassemble(binding.rootElement!, null);
       return binding.endOfFrame;
     }
     return Future<void>.value();
diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart
index 7b6d36a..b97a70a 100644
--- a/packages/flutter/test/foundation/service_extensions_test.dart
+++ b/packages/flutter/test/foundation/service_extensions_test.dart
@@ -116,7 +116,6 @@
 }
 
 void main() {
-  final Set<String> testedExtensions = <String>{}; // Add the name of an extension to this set in the test where it is tested.
   final List<String?> console = <String?>[];
   late PipelineOwner owner;
 
@@ -154,9 +153,6 @@
 
     expect(binding.frameScheduled, isFalse);
 
-    testedExtensions.add(WidgetsServiceExtensions.didSendFirstFrameEvent.name);
-    testedExtensions.add(WidgetsServiceExtensions.didSendFirstFrameRasterizedEvent.name);
-
     expect(debugPrint, equals(debugPrintThrottled));
     debugPrint = (String? message, { int? wrapWidth }) {
       console.add(message);
@@ -166,13 +162,12 @@
   tearDownAll(() async {
     // See widget_inspector_test.dart for tests of the ext.flutter.inspector
     // service extensions included in this count.
-    int widgetInspectorExtensionCount = 28;
+    int widgetInspectorExtensionCount = 20;
     if (WidgetInspectorService.instance.isWidgetCreationTracked()) {
       // Some inspector extensions are only exposed if widget creation locations
       // are tracked.
       widgetInspectorExtensionCount += 2;
     }
-    expect(binding.extensions.keys.where((String name) => name.startsWith('inspector.')), hasLength(widgetInspectorExtensionCount));
 
     // The following service extensions are disabled in web:
     // 1. exit
@@ -180,13 +175,12 @@
     const int disabledExtensions = kIsWeb ? 2 : 0;
 
     // The expected number of registered service extensions in the Flutter
-    // framework, excluding any that are for the widget inspector (see
-    // widget_inspector_test.dart for tests of the ext.flutter.inspector service
-    // extensions). Any test counted here must be tested in this file!
-    const int serviceExtensionCount = 29;
+    // framework, excluding any that are for the widget inspector
+    // (see widget_inspector_test.dart for tests of the ext.flutter.inspector
+    // service extensions).
+    const int serviceExtensionCount = 38;
 
     expect(binding.extensions.length, serviceExtensionCount + widgetInspectorExtensionCount - disabledExtensions);
-    expect(testedExtensions, hasLength(serviceExtensionCount));
 
     expect(console, isEmpty);
     debugPrint = debugPrintThrottled;
@@ -219,8 +213,6 @@
     expect(result, <String, String>{'enabled': 'true'});
     expect(WidgetsApp.debugAllowBannerOverride, true);
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(WidgetsServiceExtensions.debugAllowBanner.name);
   });
 
   test('Service extensions - debugDumpApp', () async {
@@ -229,8 +221,6 @@
     expect(result, <String, dynamic>{
       'data': matches('TestServiceExtensionsBinding - DEBUG MODE\n<no tree currently mounted>'),
     });
-
-    testedExtensions.add(WidgetsServiceExtensions.debugDumpApp.name);
   });
 
   test('Service extensions - debugDumpFocusTree', () async {
@@ -244,8 +234,6 @@
         r'$',
       ),
     });
-
-    testedExtensions.add(WidgetsServiceExtensions.debugDumpFocusTree.name);
   });
 
   test('Service extensions - debugDumpRenderTree', () async {
@@ -263,8 +251,6 @@
         r'$',
       ),
     });
-
-    testedExtensions.add(RenderingServiceExtensions.debugDumpRenderTree.name);
   });
 
   test('Service extensions - debugDumpLayerTree', () async {
@@ -288,8 +274,6 @@
         r'$',
       ),
     });
-
-    testedExtensions.add(RenderingServiceExtensions.debugDumpLayerTree.name);
   });
 
   test('Service extensions - debugDumpSemanticsTreeInTraversalOrder', () async {
@@ -304,8 +288,6 @@
         r'To generate semantics, try turning on an assistive technology \(like VoiceOver or TalkBack\) on your device.'
       )
     });
-
-    testedExtensions.add(RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name);
   });
 
   test('Service extensions - debugDumpSemanticsTreeInInverseHitTestOrder', () async {
@@ -320,12 +302,10 @@
         r'To generate semantics, try turning on an assistive technology \(like VoiceOver or TalkBack\) on your device.'
       )
     });
-
-    testedExtensions.add(RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name);
   });
 
   test('Service extensions - debugPaint', () async {
-    final Iterable<Map<String, dynamic>> extensionChangedEvents = binding.getServiceExtensionStateChangedEvents('ext.flutter.${RenderingServiceExtensions.debugPaint.name}');
+    final Iterable<Map<String, dynamic>> extensionChangedEvents = binding.getServiceExtensionStateChangedEvents('ext.flutter.debugPaint');
     Map<String, dynamic> extensionChangedEvent;
     Map<String, dynamic> result;
     Future<Map<String, dynamic>> pendingResult;
@@ -377,8 +357,6 @@
     expect(debugPaintSizeEnabled, false);
     expect(extensionChangedEvents.length, 2);
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(RenderingServiceExtensions.debugPaint.name);
   });
 
   test('Service extensions - debugPaintBaselinesEnabled', () async {
@@ -421,8 +399,6 @@
     expect(result, <String, String>{'enabled': 'false'});
     expect(debugPaintBaselinesEnabled, false);
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(RenderingServiceExtensions.debugPaintBaselinesEnabled.name);
   });
 
   test('Service extensions - invertOversizedImages', () async {
@@ -469,8 +445,6 @@
     expect(result, <String, String>{'enabled': 'false'});
     expect(debugInvertOversizedImages, false);
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(RenderingServiceExtensions.invertOversizedImages.name);
   });
 
   test('Service extensions - profileWidgetBuilds', () async {
@@ -500,8 +474,6 @@
     expect(debugProfileBuildsEnabled, false);
 
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(WidgetsServiceExtensions.profileWidgetBuilds.name);
   });
 
   test('Service extensions - profileUserWidgetBuilds', () async {
@@ -531,8 +503,6 @@
     expect(debugProfileBuildsEnabledUserWidgets, false);
 
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(WidgetsServiceExtensions.profileUserWidgetBuilds.name);
   });
 
   test('Service extensions - profileRenderObjectPaints', () async {
@@ -562,8 +532,6 @@
     expect(debugProfilePaintsEnabled, false);
 
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(RenderingServiceExtensions.profileRenderObjectPaints.name);
   });
 
   test('Service extensions - profileRenderObjectLayouts', () async {
@@ -593,8 +561,6 @@
     expect(debugProfileLayoutsEnabled, false);
 
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(RenderingServiceExtensions.profileRenderObjectLayouts.name);
   });
 
   test('Service extensions - evict', () async {
@@ -630,16 +596,12 @@
     expect(data, isFalse);
     expect(completed, isTrue);
     TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMessageHandler('flutter/assets', null);
-
-    testedExtensions.add(ServicesServiceExtensions.evict.name);
   });
 
   test('Service extensions - exit', () async {
     // no test for _calling_ 'exit', because that should terminate the process!
     // Not expecting extension to be available for web platform.
     expect(binding.extensions.containsKey(FoundationServiceExtensions.exit.name), !isBrowser);
-
-    testedExtensions.add(FoundationServiceExtensions.exit.name);
   });
 
   test('Service extensions - platformOverride', () async {
@@ -726,8 +688,6 @@
     expect(extensionChangedEvent['extension'], 'ext.flutter.platformOverride');
     expect(extensionChangedEvent['value'], 'android');
     binding.reassembled = 0;
-
-    testedExtensions.add(FoundationServiceExtensions.platformOverride.name);
   });
 
   test('Service extensions - repaintRainbow', () async {
@@ -771,8 +731,6 @@
     expect(result, <String, String>{'enabled': 'false'});
     expect(debugRepaintRainbowEnabled, false);
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(RenderingServiceExtensions.repaintRainbow.name);
   });
 
   test('Service extensions - debugDisableClipLayers', () async {
@@ -815,8 +773,6 @@
     expect(result, <String, String>{'enabled': 'false'});
     expect(debugDisableClipLayers, false);
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(RenderingServiceExtensions.debugDisableClipLayers.name);
   });
 
   test('Service extensions - debugDisablePhysicalShapeLayers', () async {
@@ -859,8 +815,6 @@
     expect(result, <String, String>{'enabled': 'false'});
     expect(debugDisablePhysicalShapeLayers, false);
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(RenderingServiceExtensions.debugDisablePhysicalShapeLayers.name);
   });
 
   test('Service extensions - debugDisableOpacityLayers', () async {
@@ -903,8 +857,6 @@
     expect(result, <String, String>{'enabled': 'false'});
     expect(debugDisableOpacityLayers, false);
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(RenderingServiceExtensions.debugDisableOpacityLayers.name);
   });
 
   test('Service extensions - reassemble', () async {
@@ -928,8 +880,6 @@
     expect(result, <String, String>{});
     expect(binding.reassembled, 1);
     binding.reassembled = 0;
-
-    testedExtensions.add(FoundationServiceExtensions.reassemble.name);
   });
 
   test('Service extensions - showPerformanceOverlay', () async {
@@ -938,7 +888,6 @@
     // The performance overlay service extension is disabled on the web.
     if (kIsWeb) {
       expect(binding.extensions.containsKey(WidgetsServiceExtensions.showPerformanceOverlay.name), isFalse);
-      testedExtensions.add(WidgetsServiceExtensions.showPerformanceOverlay.name);
       return;
     }
 
@@ -960,8 +909,6 @@
     expect(result, <String, String>{'enabled': 'false'});
     expect(WidgetsApp.showPerformanceOverlayOverride, false);
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(WidgetsServiceExtensions.showPerformanceOverlay.name);
   });
 
   test('Service extensions - timeDilation', () async {
@@ -998,8 +945,6 @@
     expect(timeDilation, 1.0);
     expect(extensionChangedEvents.length, 2);
     expect(binding.frameScheduled, isFalse);
-
-    testedExtensions.add(SchedulerServiceExtensions.timeDilation.name);
   });
 
   test('Service extensions - brightnessOverride', () async {
@@ -1008,8 +953,6 @@
     final String brightnessValue = result['value'] as String;
 
     expect(brightnessValue, 'Brightness.light');
-
-    testedExtensions.add(FoundationServiceExtensions.brightnessOverride.name);
   });
 
   test('Service extensions - activeDevToolsServerAddress', () async {
@@ -1023,8 +966,6 @@
     result = await binding.testExtension(FoundationServiceExtensions.activeDevToolsServerAddress.name, <String, String>{'value': 'http://127.0.0.1:9102'});
     serverAddress = result['value'] as String;
     expect(serverAddress, 'http://127.0.0.1:9102');
-
-    testedExtensions.add(FoundationServiceExtensions.activeDevToolsServerAddress.name);
   });
 
   test('Service extensions - connectedVmServiceUri', () async {
@@ -1038,7 +979,5 @@
     result = await binding.testExtension(FoundationServiceExtensions.connectedVmServiceUri.name, <String, String>{'value': 'http://127.0.0.1:54000/kMUMseKAnog=/'});
     serverAddress = result['value'] as String;
     expect(serverAddress, 'http://127.0.0.1:54000/kMUMseKAnog=/');
-
-    testedExtensions.add(FoundationServiceExtensions.connectedVmServiceUri.name);
   });
 }
diff --git a/packages/flutter/test/widgets/fast_reassemble_test.dart b/packages/flutter/test/widgets/fast_reassemble_test.dart
new file mode 100644
index 0000000..1269992
--- /dev/null
+++ b/packages/flutter/test/widgets/fast_reassemble_test.dart
@@ -0,0 +1,123 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  testWidgets('reassemble with a className only marks subtrees from the first matching element as dirty', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const Foo(Bar(Fizz(SizedBox())))
+    );
+
+    expect(Foo.count, 0);
+    expect(Bar.count, 0);
+    expect(Fizz.count, 0);
+
+    DebugReassembleConfig config = DebugReassembleConfig(widgetName: 'Bar');
+    WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.rootElement!, config);
+
+    expect(Foo.count, 0);
+    expect(Bar.count, 1);
+    expect(Fizz.count, 1);
+
+    config = DebugReassembleConfig(widgetName: 'Fizz');
+    WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.rootElement!, config);
+
+    expect(Foo.count, 0);
+    expect(Bar.count, 1);
+    expect(Fizz.count, 2);
+
+    config = DebugReassembleConfig(widgetName: 'NoMatch');
+    WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.rootElement!, config);
+
+    expect(Foo.count, 0);
+    expect(Bar.count, 1);
+    expect(Fizz.count, 2);
+
+    config = DebugReassembleConfig();
+    WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.rootElement!, config);
+
+    expect(Foo.count, 1);
+    expect(Bar.count, 2);
+    expect(Fizz.count, 3);
+
+    WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.rootElement!, null);
+
+    expect(Foo.count, 2);
+    expect(Bar.count, 3);
+    expect(Fizz.count, 4);
+  });
+}
+
+class Foo extends StatefulWidget {
+  const Foo(this.child, {super.key});
+
+  final Widget child;
+  static int count = 0;
+
+  @override
+  State<Foo> createState() => _FooState();
+}
+
+class _FooState extends State<Foo> {
+  @override
+  void reassemble() {
+    Foo.count += 1;
+    super.reassemble();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return widget.child;
+  }
+}
+
+
+class Bar extends StatefulWidget {
+  const Bar(this.child, {super.key});
+
+  final Widget child;
+  static int count = 0;
+
+  @override
+  State<Bar> createState() => _BarState();
+}
+
+class _BarState extends State<Bar> {
+  @override
+  void reassemble() {
+    Bar.count += 1;
+    super.reassemble();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return widget.child;
+  }
+}
+
+class Fizz extends StatefulWidget {
+  const Fizz(this.child, {super.key});
+
+  final Widget child;
+  static int count = 0;
+
+  @override
+  State<Fizz> createState() => _FizzState();
+}
+
+class _FizzState extends State<Fizz> {
+  @override
+  void reassemble() {
+    Fizz.count += 1;
+    super.reassemble();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return widget.child;
+  }
+}
diff --git a/packages/flutter/test/widgets/widget_inspector_test_utils.dart b/packages/flutter/test/widgets/widget_inspector_test_utils.dart
index 19370bf..843b76e 100644
--- a/packages/flutter/test/widgets/widget_inspector_test_utils.dart
+++ b/packages/flutter/test/widgets/widget_inspector_test_utils.dart
@@ -115,7 +115,7 @@
     final WidgetsBinding binding = WidgetsBinding.instance;
 
     if (binding.rootElement != null) {
-      binding.buildOwner!.reassemble(binding.rootElement!);
+      binding.buildOwner!.reassemble(binding.rootElement!, null);
     }
   }
 
diff --git a/packages/flutter_tools/lib/src/bundle.dart b/packages/flutter_tools/lib/src/bundle.dart
index ef7bfdb..6eed94e 100644
--- a/packages/flutter_tools/lib/src/bundle.dart
+++ b/packages/flutter_tools/lib/src/bundle.dart
@@ -33,7 +33,7 @@
 }) {
   final StringBuffer buffer = StringBuffer();
    final List<String> cacheFrontEndOptions = extraFrontEndOptions.toList()
-     ..removeWhere((String arg) => arg.startsWith('--enable-experiment='));
+     ..removeWhere((String arg) => arg.startsWith('--enable-experiment=') || arg == '--flutter-widget-cache');
   buffer.writeAll(dartDefines);
   buffer.writeAll(cacheFrontEndOptions);
   String buildPrefix = '';
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
index 93e0f3a..4136071 100644
--- a/packages/flutter_tools/lib/src/devfs.dart
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -401,6 +401,7 @@
     bool success = false,
     int invalidatedSourcesCount = 0,
     int syncedBytes = 0,
+    this.fastReassembleClassName,
     int scannedSourcesCount = 0,
     Duration compileDuration = Duration.zero,
     Duration transferDuration = Duration.zero,
@@ -422,6 +423,7 @@
   Duration get findInvalidatedDuration => _findInvalidatedDuration;
 
   bool _success;
+  String? fastReassembleClassName;
   int _invalidatedSourcesCount;
   int _syncedBytes;
   int _scannedSourcesCount;
@@ -433,6 +435,7 @@
     if (!report._success) {
       _success = false;
     }
+    fastReassembleClassName ??= report.fastReassembleClassName;
     _invalidatedSourcesCount += report._invalidatedSourcesCount;
     _syncedBytes += report._syncedBytes;
     _scannedSourcesCount += report._scannedSourcesCount;
@@ -492,6 +495,7 @@
   DateTime? lastCompiled;
   DateTime? _previousCompiled;
   PackageConfig? lastPackageConfig;
+  File? _widgetCacheOutputFile;
 
   Uri? _baseUri;
   Uri? get baseUri => _baseUri;
@@ -551,6 +555,22 @@
     lastCompiled = _previousCompiled;
   }
 
+
+  /// If the build method of a single widget was modified, return the widget name.
+  ///
+  /// If any other changes were made, or there is an error scanning the file,
+  /// return `null`.
+  String? _checkIfSingleWidgetReloadApplied() {
+    final File? widgetCacheOutputFile = _widgetCacheOutputFile;
+    if (widgetCacheOutputFile != null && widgetCacheOutputFile.existsSync()) {
+      final String widget = widgetCacheOutputFile.readAsStringSync().trim();
+      if (widget.isNotEmpty) {
+        return widget;
+      }
+    }
+    return null;
+  }
+
   /// Updates files on the device.
   ///
   /// Returns the number of bytes synced.
@@ -576,6 +596,7 @@
     final DateTime candidateCompileTime = DateTime.now();
     didUpdateFontManifest = false;
     lastPackageConfig = packageConfig;
+    _widgetCacheOutputFile = _fileSystem.file('$dillOutputPath.incremental.dill.widget_cache');
 
     // Update modified files
     final Map<Uri, DevFSContent> dirtyEntries = <Uri, DevFSContent>{};
@@ -720,6 +741,7 @@
       success: true,
       syncedBytes: syncedBytes,
       invalidatedSourcesCount: invalidatedFiles.length,
+      fastReassembleClassName: _checkIfSingleWidgetReloadApplied(),
       compileDuration: compileTimer.elapsed,
       transferDuration: transferTimer.elapsed,
     );
diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart
index 2c5a1b3..97be411 100644
--- a/packages/flutter_tools/lib/src/features.dart
+++ b/packages/flutter_tools/lib/src/features.dart
@@ -44,6 +44,9 @@
   /// Whether custom devices are enabled.
   bool get areCustomDevicesEnabled => false;
 
+  /// Whether fast single widget reloads are enabled.
+  bool get isSingleWidgetReloadEnabled => false;
+
   /// Whether WebAssembly compilation for Flutter Web is enabled.
   bool get isFlutterWebWasmEnabled => false;
 
@@ -59,6 +62,7 @@
   flutterLinuxDesktopFeature,
   flutterMacOSDesktopFeature,
   flutterWindowsDesktopFeature,
+  singleWidgetReload,
   flutterAndroidFeature,
   flutterIOSFeature,
   flutterFuchsiaFeature,
@@ -136,6 +140,20 @@
   ),
 );
 
+/// The fast hot reload feature for https://github.com/flutter/flutter/issues/61407.
+const Feature singleWidgetReload = Feature(
+  name: 'Hot reload optimization for changes to class body of a single widget',
+  configSetting: 'single-widget-reload-optimization',
+  environmentOverride: 'FLUTTER_SINGLE_WIDGET_RELOAD',
+  master: FeatureChannelSetting(
+    available: true,
+    enabledByDefault: true,
+  ),
+  beta: FeatureChannelSetting(
+    available: true,
+  ),
+);
+
 /// Enabling WebAssembly compilation from `flutter build web`
 const Feature flutterWebWasm = Feature(
   name: 'WebAssembly compilation from flutter build web',
diff --git a/packages/flutter_tools/lib/src/flutter_features.dart b/packages/flutter_tools/lib/src/flutter_features.dart
index e4102bd..672a818 100644
--- a/packages/flutter_tools/lib/src/flutter_features.dart
+++ b/packages/flutter_tools/lib/src/flutter_features.dart
@@ -45,6 +45,9 @@
   bool get areCustomDevicesEnabled => isEnabled(flutterCustomDevicesFeature);
 
   @override
+  bool get isSingleWidgetReloadEnabled => isEnabled(singleWidgetReload);
+
+  @override
   bool get isFlutterWebWasmEnabled => isEnabled(flutterWebWasm);
 
   @override
diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
index 2c81eb2..a44b803 100644
--- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
+++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
@@ -446,6 +446,7 @@
         fullRestart: true,
         reason: reason,
         overallTimeInMs: elapsed.inMilliseconds,
+        fastReassemble: false,
       ).send();
     }
     return OperationResult.ok;
diff --git a/packages/flutter_tools/lib/src/reporting/custom_dimensions.dart b/packages/flutter_tools/lib/src/reporting/custom_dimensions.dart
index 4ee8c9e..9a31fcd 100644
--- a/packages/flutter_tools/lib/src/reporting/custom_dimensions.dart
+++ b/packages/flutter_tools/lib/src/reporting/custom_dimensions.dart
@@ -58,6 +58,7 @@
     this.commandRunAndroidEmbeddingVersion,
     this.commandPackagesAndroidEmbeddingVersion,
     this.nullSafety,
+    this.fastReassemble,
     this.nullSafeMigratedLibraries,
     this.nullSafeTotalLibraries,
     this.hotEventCompileTimeInMs,
@@ -117,17 +118,17 @@
   final String? commandRunAndroidEmbeddingVersion;  // cd45
   final String? commandPackagesAndroidEmbeddingVersion;  // cd46
   final bool? nullSafety;  // cd47
-  // cd48 was fastReassemble but that feature was removed
+  final bool? fastReassemble;  // cd48
   final int? nullSafeMigratedLibraries;  // cd49
   final int? nullSafeTotalLibraries;  // cd50
-  final int? hotEventCompileTimeInMs;  // cd51
-  final int? hotEventFindInvalidatedTimeInMs;  // cd52
-  final int? hotEventScannedSourcesCount;  // cd53
-  final int? hotEventReassembleTimeInMs;  // cd54
-  final int? hotEventReloadVMTimeInMs;  // cd55
-  final bool? commandRunEnableImpeller;  // cd56
-  final String? commandRunIOSInterfaceType; // cd57
-  final bool? commandRunIsTest; // cd58
+  final int? hotEventCompileTimeInMs;  // cd 51
+  final int? hotEventFindInvalidatedTimeInMs;  // cd 52
+  final int? hotEventScannedSourcesCount;  // cd 53
+  final int? hotEventReassembleTimeInMs;  // cd 54
+  final int? hotEventReloadVMTimeInMs;  // cd 55
+  final bool? commandRunEnableImpeller;  // cd 56
+  final String? commandRunIOSInterfaceType; // cd 57
+  final bool? commandRunIsTest; // cd 58
 
   /// Convert to a map that will be used to upload to the analytics backend.
   Map<String, String> toMap() => <String, String>{
@@ -178,6 +179,7 @@
       if (commandRunAndroidEmbeddingVersion != null) CustomDimensionsEnum.commandRunAndroidEmbeddingVersion.cdKey: commandRunAndroidEmbeddingVersion.toString(),
       if (commandPackagesAndroidEmbeddingVersion != null) CustomDimensionsEnum.commandPackagesAndroidEmbeddingVersion.cdKey: commandPackagesAndroidEmbeddingVersion.toString(),
       if (nullSafety != null) CustomDimensionsEnum.nullSafety.cdKey: nullSafety.toString(),
+      if (fastReassemble != null) CustomDimensionsEnum.fastReassemble.cdKey: fastReassemble.toString(),
       if (nullSafeMigratedLibraries != null) CustomDimensionsEnum.nullSafeMigratedLibraries.cdKey: nullSafeMigratedLibraries.toString(),
       if (nullSafeTotalLibraries != null) CustomDimensionsEnum.nullSafeTotalLibraries.cdKey: nullSafeTotalLibraries.toString(),
       if (hotEventCompileTimeInMs != null) CustomDimensionsEnum.hotEventCompileTimeInMs.cdKey: hotEventCompileTimeInMs.toString(),
@@ -245,6 +247,7 @@
       commandRunAndroidEmbeddingVersion: other.commandRunAndroidEmbeddingVersion ?? commandRunAndroidEmbeddingVersion,
       commandPackagesAndroidEmbeddingVersion: other.commandPackagesAndroidEmbeddingVersion ?? commandPackagesAndroidEmbeddingVersion,
       nullSafety: other.nullSafety ?? nullSafety,
+      fastReassemble: other.fastReassemble ?? fastReassemble,
       nullSafeMigratedLibraries: other.nullSafeMigratedLibraries ?? nullSafeMigratedLibraries,
       nullSafeTotalLibraries: other.nullSafeTotalLibraries ?? nullSafeTotalLibraries,
       hotEventCompileTimeInMs: other.hotEventCompileTimeInMs ?? hotEventCompileTimeInMs,
@@ -306,6 +309,7 @@
       commandRunAndroidEmbeddingVersion: _extractString(map, CustomDimensionsEnum.commandRunAndroidEmbeddingVersion),
       commandPackagesAndroidEmbeddingVersion: _extractString(map, CustomDimensionsEnum.commandPackagesAndroidEmbeddingVersion),
       nullSafety: _extractBool(map, CustomDimensionsEnum.nullSafety),
+      fastReassemble: _extractBool(map, CustomDimensionsEnum.fastReassemble),
       nullSafeMigratedLibraries: _extractInt(map, CustomDimensionsEnum.nullSafeMigratedLibraries),
       nullSafeTotalLibraries: _extractInt(map, CustomDimensionsEnum.nullSafeTotalLibraries),
       hotEventCompileTimeInMs: _extractInt(map, CustomDimensionsEnum.hotEventCompileTimeInMs),
@@ -393,7 +397,7 @@
   commandRunAndroidEmbeddingVersion,  // cd45
   commandPackagesAndroidEmbeddingVersion,  // cd46
   nullSafety,  // cd47
-  obsolete1,  // cd48 (was fastReassemble)
+  fastReassemble,  // cd48
   nullSafeMigratedLibraries,  // cd49
   nullSafeTotalLibraries,  // cd50
   hotEventCompileTimeInMs,  // cd51
diff --git a/packages/flutter_tools/lib/src/reporting/events.dart b/packages/flutter_tools/lib/src/reporting/events.dart
index ec3933e..ae0028f 100644
--- a/packages/flutter_tools/lib/src/reporting/events.dart
+++ b/packages/flutter_tools/lib/src/reporting/events.dart
@@ -39,6 +39,7 @@
     required this.sdkName,
     required this.emulator,
     required this.fullRestart,
+    required this.fastReassemble,
     this.reason,
     this.finalLibraryCount,
     this.syncedLibraryCount,
@@ -62,6 +63,7 @@
   final String sdkName;
   final bool emulator;
   final bool fullRestart;
+  final bool fastReassemble;
   final int? finalLibraryCount;
   final int? syncedLibraryCount;
   final int? syncedClassesCount;
@@ -92,6 +94,7 @@
       hotEventInvalidatedSourcesCount: invalidatedSourcesCount,
       hotEventTransferTimeInMs: transferTimeInMs,
       hotEventOverallTimeInMs: overallTimeInMs,
+      fastReassemble: fastReassemble,
       hotEventCompileTimeInMs: compileTimeInMs,
       hotEventFindInvalidatedTimeInMs: findInvalidatedTimeInMs,
       hotEventScannedSourcesCount: scannedSourcesCount,
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index a3de310..1d55e0d 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -34,6 +34,7 @@
 import 'convert.dart';
 import 'devfs.dart';
 import 'device.dart';
+import 'features.dart';
 import 'globals.dart' as globals;
 import 'ios/application_package.dart';
 import 'ios/devices.dart';
@@ -168,8 +169,11 @@
         platform: platform,
       );
     } else {
+      // The flutter-widget-cache feature only applies to run mode.
       List<String> extraFrontEndOptions = buildInfo.extraFrontEndOptions;
       extraFrontEndOptions = <String>[
+        if (featureFlags.isSingleWidgetReloadEnabled)
+         '--flutter-widget-cache',
         '--enable-experiment=alternative-invalidation-strategy',
         ...extraFrontEndOptions,
       ];
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index fdebdca..d687012 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -20,6 +20,7 @@
 import 'dart/package_map.dart';
 import 'devfs.dart';
 import 'device.dart';
+import 'features.dart';
 import 'globals.dart' as globals;
 import 'project.dart';
 import 'reporting/reporting.dart';
@@ -414,6 +415,7 @@
       sdkName: _sdkName!,
       emulator: _emulator!,
       fullRestart: false,
+      fastReassemble: false,
       overallTimeInMs: appStartedTimer.elapsed.inMilliseconds,
       compileTimeInMs: totalCompileTime.inMilliseconds,
       transferTimeInMs: totalLaunchAppTime.inMilliseconds,
@@ -800,6 +802,7 @@
           emulator: emulator!,
           fullRestart: true,
           reason: reason,
+          fastReassemble: false,
           overallTimeInMs: restartTimer.elapsed.inMilliseconds,
           syncedBytes: result.updateFSReport?.syncedBytes,
           invalidatedSourcesCount: result.updateFSReport?.invalidatedSourcesCount,
@@ -825,6 +828,7 @@
           emulator: emulator!,
           fullRestart: true,
           reason: reason,
+          fastReassemble: false,
         ).send();
       }
       status?.cancel();
@@ -874,6 +878,7 @@
           emulator: emulator!,
           fullRestart: false,
           reason: reason,
+          fastReassemble: false,
         ).send();
       } else {
         HotEvent('exception',
@@ -882,6 +887,7 @@
           emulator: emulator!,
           fullRestart: false,
           reason: reason,
+          fastReassemble: false,
         ).send();
       }
       return OperationResult(errorCode, errorMessage, fatal: true);
@@ -965,6 +971,7 @@
       viewCache,
       onSlow,
       reloadMessage,
+      updatedDevFS.fastReassembleClassName,
     );
     shouldReportReloadTime = reassembleResult.shouldReportReloadTime;
     if (reassembleResult.reassembleViews.isEmpty) {
@@ -998,6 +1005,7 @@
       syncedBytes: updatedDevFS.syncedBytes,
       invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
       transferTimeInMs: updatedDevFS.transferDuration.inMilliseconds,
+      fastReassemble: featureFlags.isSingleWidgetReloadEnabled && updatedDevFS.fastReassembleClassName != null,
       compileTimeInMs: updatedDevFS.compileDuration.inMilliseconds,
       findInvalidatedTimeInMs: updatedDevFS.findInvalidatedDuration.inMilliseconds,
       scannedSourcesCount: updatedDevFS.scannedSourcesCount,
@@ -1217,6 +1225,7 @@
       emulator: emulator!,
       fullRestart: false,
       reason: reason,
+      fastReassemble: false,
       usage: usage,
     ).send();
     // Reset devFS lastCompileTime to ensure the file will still be marked
@@ -1279,6 +1288,7 @@
   Map<FlutterDevice?, List<FlutterView>> viewCache,
   void Function(String message)? onSlow,
   String reloadMessage,
+  String? fastReassembleClassName,
 );
 
 Future<ReassembleResult> _defaultReassembleHelper(
@@ -1286,6 +1296,7 @@
   Map<FlutterDevice?, List<FlutterView>> viewCache,
   void Function(String message)? onSlow,
   String reloadMessage,
+  String? fastReassembleClassName,
 ) async {
   // Check if any isolates are paused and reassemble those that aren't.
   final Map<FlutterView, FlutterVmService?> reassembleViews = <FlutterView, FlutterVmService?>{};
@@ -1314,9 +1325,17 @@
         reassembleViews[view] = device.vmService;
         // If the tool identified a change in a single widget, do a fast instead
         // of a full reassemble.
-        final Future<void> reassembleWork = device.vmService!.flutterReassemble(
-          isolateId: view.uiIsolate!.id!,
-        );
+        Future<void> reassembleWork;
+        if (fastReassembleClassName != null) {
+          reassembleWork = device.vmService!.flutterFastReassemble(
+            isolateId: view.uiIsolate!.id!,
+            className: fastReassembleClassName,
+          );
+        } else {
+          reassembleWork = device.vmService!.flutterReassemble(
+            isolateId: view.uiIsolate!.id!,
+          );
+        }
         reassembleFutures.add(reassembleWork.then(
           (Object? obj) => obj,
           onError: (Object error, StackTrace stackTrace) {
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart
index 57228a9..6da8494 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -809,6 +809,19 @@
     );
   }
 
+  Future<Map<String, Object?>?> flutterFastReassemble({
+   required String isolateId,
+   required String className,
+  }) {
+    return invokeFlutterExtensionRpcRaw(
+      'ext.flutter.fastReassemble',
+      isolateId: isolateId,
+      args: <String, Object>{
+        'className': className,
+      },
+    );
+  }
+
   Future<bool> flutterAlreadyPaintedFirstUsefulFrame({
     required String isolateId,
   }) async {
diff --git a/packages/flutter_tools/test/general.shard/bundle_builder_test.dart b/packages/flutter_tools/test/general.shard/bundle_builder_test.dart
index 3353540..190400a 100644
--- a/packages/flutter_tools/test/general.shard/bundle_builder_test.dart
+++ b/packages/flutter_tools/test/general.shard/bundle_builder_test.dart
@@ -118,14 +118,14 @@
     ProcessManager: () => FakeProcessManager.any(),
   });
 
-  testWithoutContext('--enable-experiment is removed from getDefaultCachedKernelPath hash', () {
+  testWithoutContext('--flutter-widget-cache and --enable-experiment are removed from getDefaultCachedKernelPath hash', () {
     final FileSystem fileSystem = MemoryFileSystem.test();
     final Config config = Config.test();
 
     expect(getDefaultCachedKernelPath(
       trackWidgetCreation: true,
       dartDefines: <String>[],
-      extraFrontEndOptions: <String>['--enable-experiment=foo'],
+      extraFrontEndOptions: <String>['--enable-experiment=foo', '--flutter-widget-cache'],
       fileSystem: fileSystem,
       config: config,
     ), 'build/cache.dill.track.dill');
@@ -133,7 +133,7 @@
     expect(getDefaultCachedKernelPath(
       trackWidgetCreation: true,
       dartDefines: <String>['foo=bar'],
-      extraFrontEndOptions: <String>['--enable-experiment=foo'],
+      extraFrontEndOptions: <String>['--enable-experiment=foo', '--flutter-widget-cache'],
       fileSystem: fileSystem,
       config: config,
     ), 'build/06ad47d8e64bd28de537b62ff85357c4.cache.dill.track.dill');
@@ -141,7 +141,7 @@
     expect(getDefaultCachedKernelPath(
       trackWidgetCreation: false,
       dartDefines: <String>[],
-      extraFrontEndOptions: <String>['--enable-experiment=foo'],
+      extraFrontEndOptions: <String>['--enable-experiment=foo', '--flutter-widget-cache'],
       fileSystem: fileSystem,
       config: config,
     ), 'build/cache.dill');
@@ -149,7 +149,7 @@
     expect(getDefaultCachedKernelPath(
       trackWidgetCreation: true,
       dartDefines: <String>[],
-      extraFrontEndOptions: <String>['--enable-experiment=foo', '--foo=bar'],
+      extraFrontEndOptions: <String>['--enable-experiment=foo', '--flutter-widget-cache', '--foo=bar'],
       fileSystem: fileSystem,
       config: config,
     ), 'build/95b595cca01caa5f0ca0a690339dd7f6.cache.dill.track.dill');
diff --git a/packages/flutter_tools/test/general.shard/hot_test.dart b/packages/flutter_tools/test/general.shard/hot_test.dart
index b8be90f..110b59a 100644
--- a/packages/flutter_tools/test/general.shard/hot_test.dart
+++ b/packages/flutter_tools/test/general.shard/hot_test.dart
@@ -178,6 +178,7 @@
             Map<FlutterDevice?, List<FlutterView>> viewCache,
             void Function(String message)? onSlow,
             String reloadMessage,
+            String? fastReassembleClassName,
           ) async => ReassembleResult(
               <FlutterView?, FlutterVmService?>{null: null},
               false,
@@ -295,6 +296,7 @@
             hotEventSdkName: 'Tester',
             hotEventEmulator: false,
             hotEventFullRestart: true,
+            fastReassemble: false,
             hotEventOverallTimeInMs: 64000,
             hotEventSyncedBytes: 4,
             hotEventInvalidatedSourcesCount: 2,
@@ -377,6 +379,7 @@
             Map<FlutterDevice?, List<FlutterView>> viewCache,
             void Function(String message)? onSlow,
             String reloadMessage,
+            String? fastReassembleClassName,
           ) async => ReassembleResult(
               <FlutterView?, FlutterVmService?>{null: null},
               false,
@@ -399,6 +402,7 @@
             hotEventSdkName: 'Tester',
             hotEventEmulator: false,
             hotEventFullRestart: false,
+            fastReassemble: false,
             hotEventCompileTimeInMs: 16000,
             hotEventFindInvalidatedTimeInMs: 64000,
             hotEventScannedSourcesCount: 16,
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
index 6723487..35970ba 100644
--- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -25,6 +25,7 @@
 import 'package:flutter_tools/src/devfs.dart';
 import 'package:flutter_tools/src/device.dart';
 import 'package:flutter_tools/src/device_port_forwarder.dart';
+import 'package:flutter_tools/src/features.dart';
 import 'package:flutter_tools/src/globals.dart' as globals;
 import 'package:flutter_tools/src/project.dart';
 import 'package:flutter_tools/src/reporting/reporting.dart';
@@ -423,6 +424,7 @@
         hotEventSdkName: 'Android',
         hotEventEmulator: false,
         hotEventFullRestart: false,
+        fastReassemble: false,
       )),
     ));
     expect(fakeVmServiceHost?.hasRemainingExpectations, false);
@@ -478,6 +480,7 @@
         hotEventSdkName: 'Android',
         hotEventEmulator: false,
         hotEventFullRestart: false,
+        fastReassemble: false,
       )),
     ));
     expect(fakeVmServiceHost?.hasRemainingExpectations, false);
@@ -524,6 +527,7 @@
         hotEventSdkName: 'Android',
         hotEventEmulator: false,
         hotEventFullRestart: false,
+        fastReassemble: false,
       )),
     ));
     expect(fakeVmServiceHost?.hasRemainingExpectations, false);
@@ -762,6 +766,96 @@
     Usage: () => TestUsage(),
   }));
 
+  testUsingContext('ResidentRunner can perform fast reassemble', () => testbed.run(() async {
+    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
+      listViews,
+      FakeVmServiceRequest(
+        method: 'getVM',
+        jsonResponse: fakeVM.toJson(),
+      ),
+      listViews,
+      listViews,
+      FakeVmServiceRequest(
+        method: 'getVM',
+        jsonResponse: fakeVM.toJson(),
+      ),
+      const FakeVmServiceRequest(
+        method: kReloadSourcesServiceName,
+        args: <String, Object>{
+          'isolateId': '1',
+          'pause': false,
+          'rootLibUri': 'main.dart.incremental.dill',
+        },
+        jsonResponse: <String, Object>{
+          'type': 'ReloadReport',
+          'success': true,
+          'details': <String, Object>{
+            'loadedLibraryCount': 1,
+          },
+        },
+      ),
+      FakeVmServiceRequest(
+        method: 'getIsolatePauseEvent',
+        args: <String, Object>{
+          'isolateId': '1',
+        },
+        jsonResponse: fakeUnpausedEvent.toJson(),
+      ),
+      FakeVmServiceRequest(
+        method: 'ext.flutter.fastReassemble',
+        args: <String, Object?>{
+          'isolateId': fakeUnpausedIsolate.id,
+          'className': 'FOO',
+        },
+      ),
+    ]);
+    final FakeDelegateFlutterDevice flutterDevice = FakeDelegateFlutterDevice(
+      device,
+      BuildInfo.debug,
+      FakeResidentCompiler(),
+      devFS,
+    )..vmService = fakeVmServiceHost!.vmService;
+    residentRunner = HotRunner(
+      <FlutterDevice>[
+        flutterDevice,
+      ],
+      stayResident: false,
+      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+      target: 'main.dart',
+      devtoolsHandler: createNoOpHandler,
+    );
+    devFS.nextUpdateReport = UpdateFSReport(
+      success: true,
+      fastReassembleClassName: 'FOO',
+      invalidatedSourcesCount: 1,
+    );
+
+    final Completer<DebugConnectionInfo> futureConnectionInfo = Completer<DebugConnectionInfo>.sync();
+    final Completer<void> futureAppStart = Completer<void>.sync();
+    unawaited(residentRunner.attach(
+      appStartedCompleter: futureAppStart,
+      connectionInfoCompleter: futureConnectionInfo,
+      enableDevTools: true,
+    ));
+
+    await futureAppStart.future;
+    final OperationResult result = await residentRunner.restart();
+
+    expect(result.fatal, false);
+    expect(result.code, 0);
+
+    final TestUsageEvent event = (globals.flutterUsage as TestUsage).events.first;
+    expect(event.category, 'hot');
+    expect(event.parameter, 'reload');
+    expect(event.parameters?.fastReassemble, true);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    Platform: () => FakePlatform(),
+    ProjectFileInvalidator: () => FakeProjectFileInvalidator(),
+    Usage: () => TestUsage(),
+    FeatureFlags: () => TestFeatureFlags(isSingleWidgetReloadEnabled: true),
+  }));
+
   testUsingContext('ResidentRunner reports hot reload time details', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       listViews,
@@ -799,9 +893,10 @@
         jsonResponse: fakeUnpausedEvent.toJson(),
       ),
       FakeVmServiceRequest(
-        method: 'ext.flutter.reassemble',
+        method: 'ext.flutter.fastReassemble',
         args: <String, Object?>{
           'isolateId': fakeUnpausedIsolate.id,
+          'className': 'FOO',
         },
       ),
     ]);
@@ -822,6 +917,7 @@
     );
     devFS.nextUpdateReport = UpdateFSReport(
       success: true,
+      fastReassembleClassName: 'FOO',
       invalidatedSourcesCount: 1,
     );
 
@@ -846,6 +942,7 @@
     Platform: () => FakePlatform(),
     ProjectFileInvalidator: () => FakeProjectFileInvalidator(),
     Usage: () => TestUsage(),
+    FeatureFlags: () => TestFeatureFlags(isSingleWidgetReloadEnabled: true),
   }));
 
   testUsingContext('ResidentRunner can send target platform to analytics from full restart', () => testbed.run(() async {
@@ -1128,6 +1225,7 @@
         hotEventSdkName: 'Android',
         hotEventEmulator: false,
         hotEventFullRestart: true,
+        fastReassemble: false,
       )),
     ));
     expect(fakeVmServiceHost?.hasRemainingExpectations, false);
@@ -1886,7 +1984,31 @@
     ProcessManager: () => FakeProcessManager.any(),
   });
 
-  testUsingContext('FlutterDevice passes alternative-invalidation-strategy flag', () async {
+  testUsingContext('FlutterDevice passes flutter-widget-cache flag when feature is enabled', () async {
+    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
+    final FakeDevice device = FakeDevice();
+
+    final DefaultResidentCompiler? residentCompiler = (await FlutterDevice.create(
+      device,
+      buildInfo: const BuildInfo(
+        BuildMode.debug,
+        '',
+        treeShakeIcons: false,
+        extraFrontEndOptions: <String>[],
+      ),
+      target: null, platform: FakePlatform(),
+    )).generator as DefaultResidentCompiler?;
+
+    expect(residentCompiler!.extraFrontEndOptions,
+      contains('--flutter-widget-cache'));
+  }, overrides: <Type, Generator>{
+    Artifacts: () => Artifacts.test(),
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+    FeatureFlags: () => TestFeatureFlags(isSingleWidgetReloadEnabled: true),
+  });
+
+   testUsingContext('FlutterDevice passes alternative-invalidation-strategy flag', () async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
     final FakeDevice device = FakeDevice();
 
@@ -1910,7 +2032,7 @@
     ProcessManager: () => FakeProcessManager.any(),
   });
 
-  testUsingContext('FlutterDevice passes initializeFromDill parameter if specified', () async {
+   testUsingContext('FlutterDevice passes initializeFromDill parameter if specified', () async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
     final FakeDevice device = FakeDevice();
 
diff --git a/packages/flutter_tools/test/integration.shard/single_widget_reload_test.dart b/packages/flutter_tools/test/integration.shard/single_widget_reload_test.dart
new file mode 100644
index 0000000..85ec32c
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/single_widget_reload_test.dart
@@ -0,0 +1,63 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter_tools/src/base/file_system.dart';
+
+import '../src/common.dart';
+import 'test_data/single_widget_reload_project.dart';
+import 'test_driver.dart';
+import 'test_utils.dart';
+
+void main() {
+  late Directory tempDir;
+  final SingleWidgetReloadProject project = SingleWidgetReloadProject();
+  late FlutterRunTestDriver flutter;
+
+  setUp(() async {
+    tempDir = createResolvedTempDirectorySync('hot_reload_test.');
+    await project.setUpIn(tempDir);
+    flutter = FlutterRunTestDriver(tempDir);
+  });
+
+  tearDown(() async {
+    await flutter.stop();
+    tryToDelete(tempDir);
+  });
+
+  testWithoutContext('newly added code executes during hot reload with single widget reloads, but only invalidated widget', () async {
+    final StringBuffer stdout = StringBuffer();
+    final StreamSubscription<String> subscription = flutter.stdout.listen(stdout.writeln);
+    await flutter.run(singleWidgetReloads: true);
+    project.uncommentHotReloadPrint();
+    try {
+      await flutter.hotReload();
+      expect(stdout.toString(), allOf(
+        contains('(((TICK 1)))'),
+        contains('(((((RELOAD WORKED)))))'),
+        // Does not invalidate parent widget, so second tick is not output.
+        isNot(contains('(((TICK 2)))'),
+      )));
+    } finally {
+      await subscription.cancel();
+    }
+  });
+
+  testWithoutContext('changes outside of the class body triggers a full reload', () async {
+    final StringBuffer stdout = StringBuffer();
+    final StreamSubscription<String> subscription = flutter.stdout.listen(stdout.writeln);
+    await flutter.run(singleWidgetReloads: true);
+    project.modifyFunction();
+    try {
+      await flutter.hotReload();
+      expect(stdout.toString(), allOf(
+        contains('(((TICK 1)))'),
+        contains('(((TICK 2)))'),
+      ));
+    } finally {
+      await subscription.cancel();
+    }
+  });
+}
diff --git a/packages/flutter_tools/test/integration.shard/test_data/single_widget_reload_project.dart b/packages/flutter_tools/test/integration.shard/test_data/single_widget_reload_project.dart
new file mode 100644
index 0000000..2a48a09
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/test_data/single_widget_reload_project.dart
@@ -0,0 +1,94 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../test_utils.dart';
+import 'project.dart';
+
+class SingleWidgetReloadProject extends Project {
+  @override
+  final String pubspec = '''
+  name: test
+  environment:
+    sdk: '>=3.0.0-0 <4.0.0'
+
+  dependencies:
+    flutter:
+      sdk: flutter
+  ''';
+
+  @override
+  final String main = r'''
+  import 'package:flutter/material.dart';
+  import 'package:flutter/scheduler.dart';
+  import 'package:flutter/services.dart';
+  import 'package:flutter/widgets.dart';
+
+  void main() async {
+    WidgetsFlutterBinding.ensureInitialized();
+    final ByteData message = const StringCodec().encodeMessage('AppLifecycleState.resumed')!;
+    await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage('flutter/lifecycle', message, (_) { });
+    runApp(MyApp());
+  }
+
+  int count = 1;
+
+  class MyApp extends StatelessWidget {
+    @override
+    Widget build(BuildContext context) {
+      // PARENT WIDGET
+
+      print('((((TICK $count))))');
+      count += 1;
+
+      return MaterialApp(
+        title: 'Flutter Demo',
+        home: SecondWidget(),
+      );
+    }
+  }
+
+  class SecondWidget extends StatelessWidget {
+    @override
+    Widget build(BuildContext context) {
+      // Do not remove the next line, it's uncommented by a test to verify that
+      // hot reloading worked:
+      // printHotReloadWorked();
+      return Container();
+    }
+  }
+
+  void printHotReloadWorked() {
+    // The call to this function is uncommented by a test to verify that hot
+    // reloading worked.
+    print('(((((RELOAD WORKED)))))');
+  }
+  ''';
+
+  Uri get parentWidgetUri => mainDart;
+  int get parentWidgetLine => lineContaining(main, '// PARENT WIDGET');
+
+  void uncommentHotReloadPrint() {
+    final String newMainContents = main.replaceAll(
+      '// printHotReloadWorked();',
+      'printHotReloadWorked();',
+    );
+    writeFile(
+      fileSystem.path.join(dir.path, 'lib', 'main.dart'),
+      newMainContents,
+      writeFutureModifiedDate: true,
+    );
+  }
+
+  void modifyFunction() {
+    final String newMainContents = main.replaceAll(
+      '(((((RELOAD WORKED)))))',
+      '(((((RELOAD WORKED 2)))))',
+    );
+    writeFile(
+      fileSystem.path.join(dir.path, 'lib', 'main.dart'),
+      newMainContents,
+      writeFutureModifiedDate: true,
+    );
+  }
+}
diff --git a/packages/flutter_tools/test/integration.shard/test_driver.dart b/packages/flutter_tools/test/integration.shard/test_driver.dart
index 6be229d..24a8e59 100644
--- a/packages/flutter_tools/test/integration.shard/test_driver.dart
+++ b/packages/flutter_tools/test/integration.shard/test_driver.dart
@@ -90,6 +90,7 @@
     List<String> arguments, {
     String? script,
     bool withDebugger = false,
+    bool singleWidgetReloads = false,
   }) async {
     final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');
     if (withDebugger) {
@@ -113,6 +114,8 @@
       environment: <String, String>{
         'FLUTTER_TEST': 'true',
         'FLUTTER_WEB': 'true',
+        if (singleWidgetReloads)
+          'FLUTTER_SINGLE_WIDGET_RELOAD': 'true',
       },
     );
 
@@ -508,6 +511,7 @@
     bool chrome = false,
     bool expressionEvaluation = true,
     bool structuredErrors = false,
+    bool singleWidgetReloads = false,
     bool serveObservatory = false,
     String? script,
     List<String>? additionalCommandArgs,
@@ -538,6 +542,7 @@
       startPaused: startPaused,
       pauseOnExceptions: pauseOnExceptions,
       script: script,
+      singleWidgetReloads: singleWidgetReloads,
     );
   }
 
@@ -546,6 +551,7 @@
     bool withDebugger = false,
     bool startPaused = false,
     bool pauseOnExceptions = false,
+    bool singleWidgetReloads = false,
     bool serveObservatory = false,
     List<String>? additionalCommandArgs,
   }) async {
@@ -567,6 +573,7 @@
       withDebugger: withDebugger,
       startPaused: startPaused,
       pauseOnExceptions: pauseOnExceptions,
+      singleWidgetReloads: singleWidgetReloads,
       attachPort: port,
     );
   }
@@ -578,6 +585,7 @@
     bool withDebugger = false,
     bool startPaused = false,
     bool pauseOnExceptions = false,
+    bool singleWidgetReloads = false,
     int? attachPort,
   }) async {
     assert(!startPaused || withDebugger);
@@ -585,6 +593,7 @@
       args,
       script: script,
       withDebugger: withDebugger,
+      singleWidgetReloads: singleWidgetReloads,
     );
 
     final Completer<void> prematureExitGuard = Completer<void>();
@@ -797,11 +806,13 @@
     bool withDebugger = false,
     bool pauseOnExceptions = false,
     Future<void> Function()? beforeStart,
+    bool singleWidgetReloads = false,
   }) async {
     await super._setupProcess(
       args,
       script: script,
       withDebugger: withDebugger,
+      singleWidgetReloads: singleWidgetReloads,
     );
 
     // Stash the PID so that we can terminate the VM more reliably than using
diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart
index d51ec23..65fb557 100644
--- a/packages/flutter_tools/test/src/fakes.dart
+++ b/packages/flutter_tools/test/src/fakes.dart
@@ -443,6 +443,7 @@
     this.isMacOSEnabled = false,
     this.isWebEnabled = false,
     this.isWindowsEnabled = false,
+    this.isSingleWidgetReloadEnabled = false,
     this.isAndroidEnabled = true,
     this.isIOSEnabled = true,
     this.isFuchsiaEnabled = false,
@@ -463,6 +464,9 @@
   final bool isWindowsEnabled;
 
   @override
+  final bool isSingleWidgetReloadEnabled;
+
+  @override
   final bool isAndroidEnabled;
 
   @override
@@ -488,6 +492,8 @@
         return isMacOSEnabled;
       case flutterWindowsDesktopFeature:
         return isWindowsEnabled;
+      case singleWidgetReload:
+        return isSingleWidgetReloadEnabled;
       case flutterAndroidFeature:
         return isAndroidEnabled;
       case flutterIOSFeature: