diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ArrayBuilderPool`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ArrayBuilderPool`1.cs
new file mode 100644
index 0000000000..a157b88a14
--- /dev/null
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ArrayBuilderPool`1.cs
@@ -0,0 +1,53 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Immutable;
+
+namespace Microsoft.AspNetCore.Razor.PooledObjects;
+
+///
+/// A pool of instances.
+///
+///
+///
+/// Instances originating from this pool are intended to be short-lived and are suitable
+/// for temporary work. Do not return them as the results of methods or store them in fields.
+///
+internal static class ArrayBuilderPool
+{
+ private const int Threshold = 512;
+
+ private static readonly Func.Builder>, ImmutableArray.Builder> s_allocate = AllocateAndClear;
+ private static readonly Action.Builder>, ImmutableArray.Builder> s_release = ClearAndFree;
+
+ public static ObjectPool.Builder> DefaultPool { get; } = ObjectPool.Default(ImmutableArray.CreateBuilder);
+
+ public static PooledObject.Builder> GetPooledObject()
+ => new(DefaultPool, s_allocate, s_release);
+
+ private static ImmutableArray.Builder AllocateAndClear(ObjectPool.Builder> pool)
+ {
+ var builder = pool.Allocate();
+ builder.Clear();
+
+ return builder;
+ }
+
+ private static void ClearAndFree(ObjectPool.Builder> pool, ImmutableArray.Builder builder)
+ {
+ if (builder is null)
+ {
+ return;
+ }
+
+ builder.Clear();
+
+ if (builder.Capacity > Threshold)
+ {
+ builder.Capacity = Threshold;
+ }
+
+ pool.Free(builder);
+ }
+}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/HashSetPool`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/HashSetPool`1.cs
new file mode 100644
index 0000000000..a98539ccca
--- /dev/null
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/HashSetPool`1.cs
@@ -0,0 +1,54 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.AspNetCore.Razor.PooledObjects;
+
+///
+/// A pool of instances that compares items using default equality.
+///
+///
+///
+/// Instances originating from this pool are intended to be short-lived and are suitable
+/// for temporary work. Do not return them as the results of methods or store them in fields.
+///
+internal static class HashSetPool
+{
+ private const int Threshold = 512;
+
+ private static readonly Func>, HashSet> s_allocate = AllocateAndClear;
+ private static readonly Action>, HashSet> s_release = ClearAndFree;
+
+ public static ObjectPool> DefaultPool { get; } = ObjectPool.Default>();
+
+ public static PooledObject> GetPooledObject()
+ => new(DefaultPool, s_allocate, s_release);
+
+ private static HashSet AllocateAndClear(ObjectPool> pool)
+ {
+ var set = pool.Allocate();
+ set.Clear();
+
+ return set;
+ }
+
+ private static void ClearAndFree(ObjectPool> pool, HashSet set)
+ {
+ if (set is null)
+ {
+ return;
+ }
+
+ var count = set.Count;
+ set.Clear();
+
+ if (count > Threshold)
+ {
+ set.TrimExcess();
+ }
+
+ pool.Free(set);
+ }
+}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ListPool`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ListPool`1.cs
new file mode 100644
index 0000000000..78c1837047
--- /dev/null
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ListPool`1.cs
@@ -0,0 +1,54 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.AspNetCore.Razor.PooledObjects;
+
+///
+/// A pool of instances.
+///
+///
+///
+/// Instances originating from this pool are intended to be short-lived and are suitable
+/// for temporary work. Do not return them as the results of methods or store them in fields.
+///
+internal static class ListPool
+{
+ private const int Threshold = 512;
+
+ private static readonly Func>, List> s_allocate = AllocateAndClear;
+ private static readonly Action>, List> s_release = ClearAndFree;
+
+ public static ObjectPool> DefaultPool { get; } = ObjectPool.Default>();
+
+ public static PooledObject> GetPooledObject()
+ => new(DefaultPool, s_allocate, s_release);
+
+ private static List AllocateAndClear(ObjectPool> pool)
+ {
+ var list = pool.Allocate();
+ list.Clear();
+
+ return list;
+ }
+
+ private static void ClearAndFree(ObjectPool> pool, List list)
+ {
+ if (list is null)
+ {
+ return;
+ }
+
+ var count = list.Count;
+ list.Clear();
+
+ if (count > Threshold)
+ {
+ list.TrimExcess();
+ }
+
+ pool.Free(list);
+ }
+}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ObjectPool.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ObjectPool.cs
new file mode 100644
index 0000000000..1515904654
--- /dev/null
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ObjectPool.cs
@@ -0,0 +1,25 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNetCore.Razor.PooledObjects;
+
+internal static class ObjectPool
+{
+ private const int DefaultSize = 20;
+
+ public static ObjectPool Default()
+ where T : class, new()
+ => DefaultPool.Instance;
+
+ public static ObjectPool Default(Func factory)
+ where T : class
+ => new(factory, DefaultSize);
+
+ private static class DefaultPool
+ where T : class, new()
+ {
+ public static readonly ObjectPool Instance = new(() => new T(), DefaultSize);
+ }
+}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ObjectPool`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ObjectPool`1.cs
new file mode 100644
index 0000000000..a0a28db977
--- /dev/null
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ObjectPool`1.cs
@@ -0,0 +1,142 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+// Copied from https://github/dotnet/roslyn
+
+using System;
+using System.Diagnostics;
+using System.Threading;
+
+namespace Microsoft.AspNetCore.Razor.PooledObjects;
+
+///
+/// Generic implementation of object pooling pattern with predefined pool size limit. The main
+/// purpose is that limited number of frequently used objects can be kept in the pool for
+/// further recycling.
+///
+///
+///
+/// Notes:
+///
+///
+/// -
+/// It is not the goal to keep all returned objects. The pool is not meant for storage.
+/// If there is no space in the pool, extra returned objects will be dropped.
+///
+///
+/// -
+/// It is implied that if object was obtained from this pool, the caller will return it back in
+/// a relatively short time. Keeping checked out objects for long durations is fine, but reduces
+/// the usefulness of pooling. Just new up your own object.
+///
+///
+///
+///
+/// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice.
+/// Rationale: If there is no intent for reusing the object, do not use pool - just use "new".
+///
+///
+internal class ObjectPool
+ where T : class
+{
+ [DebuggerDisplay("{Value,nq}")]
+ private struct Element
+ {
+ public T? _value;
+ }
+
+ // Storage for the pool objects. The first item is stored in a dedicated field because we
+ // expect to be able to satisfy most requests from it.
+ private T? _firstItem;
+ private readonly Element[] _items;
+
+ private readonly Func _factory;
+
+ public ObjectPool(Func factory)
+ : this(factory, Environment.ProcessorCount * 2)
+ {
+ }
+
+ public ObjectPool(Func factory, int size)
+ {
+ _factory = factory;
+ _items = new Element[size - 1];
+ }
+
+ public ObjectPool(Func, T> factory, int size)
+ {
+ _factory = () => factory(this);
+ _items = new Element[size - 1];
+ }
+
+ private T CreateInstance() => _factory();
+
+ public T Allocate()
+ {
+ // PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements.
+ // Note that the initial read is optimistically not synchronized. That is intentional.
+ // We will interlock only when we have a candidate. in a worst case we may miss some
+ // recently returned objects. Not a big deal.
+
+ var item = _firstItem;
+ if (item is null || item != Interlocked.CompareExchange(ref _firstItem, null, item))
+ {
+ item = AllocateSlow();
+ }
+
+ return item;
+ }
+
+ private T AllocateSlow()
+ {
+ var items = _items;
+
+ for (var i = 0; i < items.Length; i++)
+ {
+ // Note that the initial read is optimistically not synchronized. That is intentional.
+ // We will interlock only when we have a candidate. in a worst case we may miss some
+ // recently returned objects. Not a big deal.
+
+ var item = _items[i]._value;
+ if (item is not null &&
+ item == Interlocked.CompareExchange(ref items[i]._value, null, item))
+ {
+ return item;
+ }
+ }
+
+ return CreateInstance();
+ }
+
+ public void Free(T obj)
+ {
+ if (_firstItem is null)
+ {
+ // Intentionally not using interlocked here.
+ // In a worst case scenario two objects may be stored into same slot.
+ // It is very unlikely to happen and will only mean that one of the objects will get collected.
+ _firstItem = obj;
+ }
+ else
+ {
+ FreeSlow(obj);
+ }
+ }
+
+ private void FreeSlow(T obj)
+ {
+ var items = _items;
+
+ for (var i = 0; i < items.Length; i++)
+ {
+ if (items[i]._value is null)
+ {
+ // Intentionally not using interlocked here.
+ // In a worst case scenario two objects may be stored into same slot.
+ // It is very unlikely to happen and will only mean that one of the objects will get collected.
+ items[i]._value = obj;
+ break;
+ }
+ }
+ }
+}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/PooledObject`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/PooledObject`1.cs
new file mode 100644
index 0000000000..0c0c33072c
--- /dev/null
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/PooledObject`1.cs
@@ -0,0 +1,38 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+// Copied from https://github/dotnet/roslyn
+
+using System;
+
+namespace Microsoft.AspNetCore.Razor.PooledObjects;
+
+internal struct PooledObject : IDisposable
+ where T : class
+{
+ private readonly ObjectPool _pool;
+ private readonly Action, T> _releaser;
+ private T? _object;
+
+ // Because of how this API is intended to be used, we don't want the consumption code to have
+ // to deal with Object being a nullable reference type. Intead, the guarantee is that this is
+ // non-null until this is disposed.
+ public T Object => _object!;
+
+ public PooledObject(ObjectPool pool, Func, T> allocator, Action, T> releaser)
+ : this()
+ {
+ _pool = pool;
+ _object = allocator(pool);
+ _releaser = releaser;
+ }
+
+ public void Dispose()
+ {
+ if (_object is { } obj)
+ {
+ _releaser(_pool, obj);
+ _object = null;
+ }
+ }
+}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ReferenceEqualityHashSetPool`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ReferenceEqualityHashSetPool`1.cs
new file mode 100644
index 0000000000..b834dbd453
--- /dev/null
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/ReferenceEqualityHashSetPool`1.cs
@@ -0,0 +1,56 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Razor.Utilities;
+
+namespace Microsoft.AspNetCore.Razor.PooledObjects;
+
+///
+/// A pool of instances that compares items using reference equality.
+///
+///
+///
+/// Instances originating from this pool are intended to be short-lived and are suitable
+/// for temporary work. Do not return them as the results of methods or store them in fields.
+///
+internal static class ReferenceEqualityHashSetPool
+ where T : class
+{
+ private const int Threshold = 512;
+
+ private static readonly Func>, HashSet> s_allocate = AllocateAndClear;
+ private static readonly Action>, HashSet> s_release = ClearAndFree;
+
+ public static ObjectPool> DefaultPool { get; } = ObjectPool.Default(() => new HashSet(ReferenceEqualityComparer.Instance));
+
+ public static PooledObject> GetPooledObject()
+ => new(DefaultPool, s_allocate, s_release);
+
+ private static HashSet AllocateAndClear(ObjectPool> pool)
+ {
+ var set = pool.Allocate();
+ set.Clear();
+
+ return set;
+ }
+
+ private static void ClearAndFree(ObjectPool> pool, HashSet set)
+ {
+ if (set is null)
+ {
+ return;
+ }
+
+ var count = set.Count;
+ set.Clear();
+
+ if (count > Threshold)
+ {
+ set.TrimExcess();
+ }
+
+ pool.Free(set);
+ }
+}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/StackPool`1.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/StackPool`1.cs
new file mode 100644
index 0000000000..58b560c177
--- /dev/null
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/StackPool`1.cs
@@ -0,0 +1,54 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.AspNetCore.Razor.PooledObjects;
+
+///
+/// A pool of instances.
+///
+///
+///
+/// Instances originating from this pool are intended to be short-lived and are suitable
+/// for temporary work. Do not return them as the results of methods or store them in fields.
+///
+internal static class StackPool
+{
+ private const int Threshold = 512;
+
+ private static readonly Func>, Stack> s_allocate = AllocateAndClear;
+ private static readonly Action>, Stack> s_release = ClearAndFree;
+
+ public static ObjectPool> DefaultPool { get; } = ObjectPool.Default>();
+
+ public static PooledObject> GetPooledObject()
+ => new(DefaultPool, s_allocate, s_release);
+
+ private static Stack AllocateAndClear(ObjectPool> pool)
+ {
+ var stack = pool.Allocate();
+ stack.Clear();
+
+ return stack;
+ }
+
+ private static void ClearAndFree(ObjectPool> pool, Stack stack)
+ {
+ if (stack is null)
+ {
+ return;
+ }
+
+ var count = stack.Count;
+ stack.Clear();
+
+ if (count > Threshold)
+ {
+ stack.TrimExcess();
+ }
+
+ pool.Free(stack);
+ }
+}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/StringBuilderPool.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/StringBuilderPool.cs
new file mode 100644
index 0000000000..e566911322
--- /dev/null
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/PooledObjects/StringBuilderPool.cs
@@ -0,0 +1,53 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System;
+using System.Text;
+
+namespace Microsoft.AspNetCore.Razor.PooledObjects;
+
+///
+/// A pool of instances.
+///
+///
+///
+/// Instances originating from this pool are intended to be short-lived and are suitable
+/// for temporary work. Do not return them as the results of methods or store them in fields.
+///
+internal static class StringBuilderPool
+{
+ private const int Threshold = 512;
+
+ private static readonly Func, StringBuilder> s_allocate = AllocateAndClear;
+ private static readonly Action, StringBuilder> s_release = ClearAndFree;
+
+ public static ObjectPool DefaultPool { get; } = ObjectPool.Default();
+
+ public static PooledObject GetPooledObject()
+ => new(DefaultPool, s_allocate, s_release);
+
+ private static StringBuilder AllocateAndClear(ObjectPool pool)
+ {
+ var builder = pool.Allocate();
+ builder.Clear();
+
+ return builder;
+ }
+
+ private static void ClearAndFree(ObjectPool pool, StringBuilder builder)
+ {
+ if (builder is null)
+ {
+ return;
+ }
+
+ builder.Clear();
+
+ if (builder.Capacity > Threshold)
+ {
+ builder.Capacity = Threshold;
+ }
+
+ pool.Free(builder);
+ }
+}
diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Utilities/ReferenceEqualityComparer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Utilities/ReferenceEqualityComparer.cs
new file mode 100644
index 0000000000..0940f206d9
--- /dev/null
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Utilities/ReferenceEqualityComparer.cs
@@ -0,0 +1,23 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT license. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace Microsoft.AspNetCore.Razor.Utilities;
+
+internal class ReferenceEqualityComparer : IEqualityComparer
+ where T : class
+{
+ public static readonly ReferenceEqualityComparer Instance = new();
+
+ private ReferenceEqualityComparer()
+ {
+ }
+
+ bool IEqualityComparer.Equals(T? x, T? y)
+ => ReferenceEquals(x, y);
+
+ int IEqualityComparer.GetHashCode(T obj)
+ => RuntimeHelpers.GetHashCode(obj);
+}