Do not eagerly allocate inherited widget caches when initializing element tree (#95596)

diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index 189f3e2..a058a2d 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -395,6 +395,53 @@
   }
 }
 
+/// A cache of inherited elements with an optional parent cache.
+///
+/// When looking up an inherited element, this will look through parent
+/// caches until the element is found or the end is reached. When an element
+/// is found, it is cached at the first element where the search began.
+///
+/// The intention of this cache is to speed up the initial build of widget
+/// trees that contain a significant number of inherited widgets by deferring
+/// expensive map allocations until they are needed, and by only allocating
+/// in the "closest" hash map.
+///
+/// This will not cache `null` results if an inherited element is not found.
+@visibleForTesting
+class InheritedTreeCache {
+  /// Create a new [InheritedTreeCache] with an optional parent.
+  InheritedTreeCache([this._parent]);
+
+  final InheritedTreeCache? _parent;
+  final HashMap<Type, InheritedElement> _current = HashMap<Type, InheritedElement>();
+
+  /// Place the [element] in the cache under [type].
+  void operator []=(Type type, InheritedElement element) {
+    _current[type] = element;
+  }
+
+  /// Find the nearest [InheritedElement] of type [type], or `null` if it
+  /// cannot be found.
+  ///
+  /// This operation will also cache the inherited element to improve the
+  /// speed of future lookups.
+  InheritedElement? operator[](Type type) {
+    InheritedElement? potential = _current[type];
+    if (potential != null) {
+      return potential;
+    }
+    potential =  _parent?._lookupWithoutCaching(type);
+    if (potential != null) {
+      _current[type] = potential;
+    }
+    return potential;
+  }
+
+  InheritedElement? _lookupWithoutCaching(Type type) {
+    return _current[type] ?? _parent?._lookupWithoutCaching(type);
+  }
+}
+
 /// A widget that does not require mutable state.
 ///
 /// A stateless widget is a widget that describes part of the user interface by
@@ -3937,7 +3984,7 @@
       // implementation to decide whether to rebuild based on whether we had
       // dependencies here.
     }
-    _inheritedWidgets = null;
+    _inheritedLookup = null;
     _lifecycleState = _ElementLifecycle.inactive;
   }
 
@@ -4129,7 +4176,8 @@
     return null;
   }
 
-  Map<Type, InheritedElement>? _inheritedWidgets;
+  InheritedTreeCache? _inheritedLookup;
+
   Set<InheritedElement>? _dependencies;
   bool _hadUnsatisfiedDependencies = false;
 
@@ -4166,7 +4214,7 @@
   @override
   T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
     assert(_debugCheckStateIsActiveForAncestorLookup());
-    final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
+    final InheritedElement? ancestor = _inheritedLookup?[T];
     if (ancestor != null) {
       return dependOnInheritedElement(ancestor, aspect: aspect) as T;
     }
@@ -4177,13 +4225,13 @@
   @override
   InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
     assert(_debugCheckStateIsActiveForAncestorLookup());
-    final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
+    final InheritedElement? ancestor = _inheritedLookup?[T];
     return ancestor;
   }
 
   void _updateInheritance() {
     assert(_lifecycleState == _ElementLifecycle.active);
-    _inheritedWidgets = _parent?._inheritedWidgets;
+    _inheritedLookup = _parent?._inheritedLookup;
   }
 
   @override
@@ -5191,12 +5239,8 @@
   @override
   void _updateInheritance() {
     assert(_lifecycleState == _ElementLifecycle.active);
-    final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
-    if (incomingWidgets != null)
-      _inheritedWidgets = HashMap<Type, InheritedElement>.of(incomingWidgets);
-    else
-      _inheritedWidgets = HashMap<Type, InheritedElement>();
-    _inheritedWidgets![widget.runtimeType] = this;
+    _inheritedLookup = InheritedTreeCache(_parent?._inheritedLookup)
+      ..[widget.runtimeType] = this;
   }
 
   @override
diff --git a/packages/flutter/test/widgets/inherited_tree_cache_test.dart b/packages/flutter/test/widgets/inherited_tree_cache_test.dart
new file mode 100644
index 0000000..693d847
--- /dev/null
+++ b/packages/flutter/test/widgets/inherited_tree_cache_test.dart
@@ -0,0 +1,79 @@
+// 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/material.dart';
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  testWidgets('InheritedTreeCache returns null if element is not found', (WidgetTester tester) async {
+    final InheritedTreeCache parent = InheritedTreeCache();
+
+    expect(parent[InheritedElementA], isNull);
+  });
+
+  testWidgets('InheritedTreeCache can look up element from parent', (WidgetTester tester) async {
+    final InheritedTreeCache parent = InheritedTreeCache();
+    final InheritedTreeCache child = InheritedTreeCache(parent);
+    final InheritedElementA elementA = InheritedElementA(const InheritedWidgetA());
+
+    parent[InheritedElementA] = elementA;
+
+    expect(child[InheritedElementA], elementA);
+  });
+
+  testWidgets('InheritedTreeCache can look up multiple elements from parent', (WidgetTester tester) async {
+    final InheritedTreeCache parent = InheritedTreeCache();
+    final InheritedTreeCache child = InheritedTreeCache(parent);
+    final InheritedElementA elementA = InheritedElementA(const InheritedWidgetA());
+    final InheritedElementA elementB = InheritedElementA(const InheritedWidgetB());
+
+    parent[InheritedElementA] = elementA;
+    parent[InheritedElementB] = elementB;
+
+    expect(child[InheritedElementA], elementA);
+    expect(child[InheritedElementB], elementB);
+  });
+
+  testWidgets('InheritedTreeCache does not cache nulls', (WidgetTester tester) async {
+    final InheritedTreeCache parent = InheritedTreeCache();
+    final InheritedTreeCache child = InheritedTreeCache(parent);
+    final InheritedElementA elementA = InheritedElementA(const InheritedWidgetA());
+
+    // First look up element that has not been cached.
+    expect(child[InheritedElementA], null);
+
+    // Then manually add element to parent.
+    parent[InheritedElementA] = elementA;
+
+    // Then the child should be able to find it.
+    expect(child[InheritedElementA], elementA);
+  });
+}
+
+class InheritedElementA extends InheritedElement {
+  InheritedElementA(InheritedWidget widget) : super(widget);
+}
+
+class InheritedWidgetA extends InheritedWidget {
+  const InheritedWidgetA({ Key? key }) : super(child: const SizedBox(), key: key);
+
+  @override
+  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
+    return true;
+  }
+}
+
+class InheritedElementB extends InheritedElement {
+  InheritedElementB(InheritedWidget widget) : super(widget);
+}
+
+class InheritedWidgetB extends InheritedWidget {
+  const InheritedWidgetB({ Key? key }) : super(child: const SizedBox(), key: key);
+
+  @override
+  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
+    return true;
+  }
+}