diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php
index fdd9de9aff..efdd9b1b1a 100644
--- a/src/wp-includes/blocks.php
+++ b/src/wp-includes/blocks.php
@@ -1518,6 +1518,83 @@ function traverse_and_serialize_block( $block, $pre_callback = null, $post_callb
 	);
 }
 
+/**
+ * Replaces patterns in a block tree with their content.
+ *
+ * @since 6.6.0
+ *
+ * @param array $blocks An array blocks.
+ *
+ * @return array An array of blocks with patterns replaced by their content.
+ */
+function resolve_pattern_blocks( $blocks ) {
+	static $inner_content;
+	// Keep track of seen references to avoid infinite loops.
+	static $seen_refs = array();
+	$i                = 0;
+	while ( $i < count( $blocks ) ) {
+		if ( 'core/pattern' === $blocks[ $i ]['blockName'] ) {
+			$attrs = $blocks[ $i ]['attrs'];
+
+			if ( empty( $attrs['slug'] ) ) {
+				++$i;
+				continue;
+			}
+
+			$slug = $attrs['slug'];
+
+			if ( isset( $seen_refs[ $slug ] ) ) {
+				// Skip recursive patterns.
+				array_splice( $blocks, $i, 1 );
+				continue;
+			}
+
+			$registry = WP_Block_Patterns_Registry::get_instance();
+			$pattern  = $registry->get_registered( $slug );
+
+			// Skip unknown patterns.
+			if ( ! $pattern ) {
+				++$i;
+				continue;
+			}
+
+			$blocks_to_insert   = parse_blocks( $pattern['content'] );
+			$seen_refs[ $slug ] = true;
+			$prev_inner_content = $inner_content;
+			$inner_content      = null;
+			$blocks_to_insert   = resolve_pattern_blocks( $blocks_to_insert );
+			$inner_content      = $prev_inner_content;
+			unset( $seen_refs[ $slug ] );
+			array_splice( $blocks, $i, 1, $blocks_to_insert );
+
+			// If we have inner content, we need to insert nulls in the
+			// inner content array, otherwise serialize_blocks will skip
+			// blocks.
+			if ( $inner_content ) {
+				$null_indices  = array_keys( $inner_content, null, true );
+				$content_index = $null_indices[ $i ];
+				$nulls         = array_fill( 0, count( $blocks_to_insert ), null );
+				array_splice( $inner_content, $content_index, 1, $nulls );
+			}
+
+			// Skip inserted blocks.
+			$i += count( $blocks_to_insert );
+		} else {
+			if ( ! empty( $blocks[ $i ]['innerBlocks'] ) ) {
+				$prev_inner_content          = $inner_content;
+				$inner_content               = $blocks[ $i ]['innerContent'];
+				$blocks[ $i ]['innerBlocks'] = resolve_pattern_blocks(
+					$blocks[ $i ]['innerBlocks']
+				);
+				$blocks[ $i ]['innerContent'] = $inner_content;
+				$inner_content               = $prev_inner_content;
+			}
+			++$i;
+		}
+	}
+	return $blocks;
+}
+
 /**
  * Given an array of parsed block trees, applies callbacks before and after serializing them and
  * returns their concatenated output.
diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-block-patterns-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-block-patterns-controller.php
index d8f083924e..c98b2a7c57 100644
--- a/src/wp-includes/rest-api/endpoints/class-wp-rest-block-patterns-controller.php
+++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-block-patterns-controller.php
@@ -162,6 +162,12 @@ class WP_REST_Block_Patterns_Controller extends WP_REST_Controller {
 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
 	 */
 	public function prepare_item_for_response( $item, $request ) {
+		// Resolve pattern blocks so they don't need to be resolved client-side
+		// in the editor, improving performance.
+		$blocks        = parse_blocks( $item['content'] );
+		$blocks        = resolve_pattern_blocks( $blocks );
+		$item['content'] = serialize_blocks( $blocks );
+
 		$fields = $this->get_fields_for_response( $request );
 		$keys   = array(
 			'name'          => 'name',
diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php
index 0ac6c7de5a..cbf0ee040a 100644
--- a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php
+++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php
@@ -668,6 +668,12 @@ class WP_REST_Templates_Controller extends WP_REST_Controller {
 	 * @return WP_REST_Response Response object.
 	 */
 	public function prepare_item_for_response( $item, $request ) {
+		// Resolve pattern blocks so they don't need to be resolved client-side
+		// in the editor, improving performance.
+		$blocks        = parse_blocks( $item->content );
+		$blocks        = resolve_pattern_blocks( $blocks );
+		$item->content = serialize_blocks( $blocks );
+
 		// Restores the more descriptive, specific name for use within this method.
 		$template = $item;
 
diff --git a/tests/phpunit/tests/blocks/resolvePatternBlocks.php b/tests/phpunit/tests/blocks/resolvePatternBlocks.php
new file mode 100644
index 0000000000..b2e6fa6463
--- /dev/null
+++ b/tests/phpunit/tests/blocks/resolvePatternBlocks.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Tests for resolve_pattern_blocks.
+ *
+ * @package WordPress
+ * @subpackage Blocks
+ *
+ * @since 6.6.0
+ *
+ * @group blocks
+ * @covers resolve_pattern_blocks
+ */
+class Tests_Blocks_ResolvePatternBlocks extends WP_UnitTestCase {
+	public function set_up() {
+		parent::set_up();
+
+		register_block_pattern(
+			'core/test',
+			array(
+				'title'       => 'Test',
+				'content'     => '<!-- wp:paragraph -->Hello<!-- /wp:paragraph --><!-- wp:paragraph -->World<!-- /wp:paragraph -->',
+				'description' => 'Test pattern.',
+			)
+		);
+		register_block_pattern(
+			'core/recursive',
+			array(
+				'title'       => 'Recursive',
+				'content'     => '<!-- wp:paragraph -->Recursive<!-- /wp:paragraph --><!-- wp:pattern {"slug":"core/recursive"} /-->',
+				'description' => 'Recursive pattern.',
+			)
+		);
+	}
+
+	public function tear_down() {
+		unregister_block_pattern( 'core/test' );
+		unregister_block_pattern( 'core/recursive' );
+
+		parent::tear_down();
+	}
+
+	/**
+	 * @dataProvider data_should_resolve_pattern_blocks_as_expected
+	 *
+	 * @ticket 61228
+	 *
+	 * @param string $blocks   A string representing blocks that need resolving.
+	 * @param string $expected Expected result.
+	 */
+	public function test_should_resolve_pattern_blocks_as_expected( $blocks, $expected ) {
+		$actual = resolve_pattern_blocks( parse_blocks( $blocks ) );
+		$this->assertSame( $expected, serialize_blocks( $actual ) );
+	}
+
+	/**
+	 * Data provider.
+	 *
+	 * @return array
+	 */
+	public function data_should_resolve_pattern_blocks_as_expected() {
+		return array(
+			// Works without attributes, leaves the block as is.
+			'pattern with no slug attribute' => array( '<!-- wp:pattern /-->', '<!-- wp:pattern /-->' ),
+			// Resolves the pattern.
+			'test pattern'                   => array( '<!-- wp:pattern {"slug":"core/test"} /-->', '<!-- wp:paragraph -->Hello<!-- /wp:paragraph --><!-- wp:paragraph -->World<!-- /wp:paragraph -->' ),
+			// Skips recursive patterns.
+			'recursive pattern'              => array( '<!-- wp:pattern {"slug":"core/recursive"} /-->', '<!-- wp:paragraph -->Recursive<!-- /wp:paragraph -->' ),
+			// Resolves the pattern within a block.
+			'pattern within a block'         => array( '<!-- wp:group --><!-- wp:paragraph -->Before<!-- /wp:paragraph --><!-- wp:pattern {"slug":"core/test"} /--><!-- wp:paragraph -->After<!-- /wp:paragraph --><!-- /wp:group -->', '<!-- wp:group --><!-- wp:paragraph -->Before<!-- /wp:paragraph --><!-- wp:paragraph -->Hello<!-- /wp:paragraph --><!-- wp:paragraph -->World<!-- /wp:paragraph --><!-- wp:paragraph -->After<!-- /wp:paragraph --><!-- /wp:group -->' ),
+		);
+	}
+}