Commit a779b4c25b9 for woocommerce
commit a779b4c25b99b33221f69ac44cb8def315af6ca6
Author: Tony Arcangelini <33258733+arcangelini@users.noreply.github.com>
Date: Tue Mar 3 16:50:18 2026 +0100
Email Editor: fix redundant CSS inlining (#63454)
* Email Editor: fix redundant CSS inlining
* Add changefile(s) from automation for the following project(s): packages/php/email-editor
* Fix CSS style ordering so content styles take precedence
* Update changelog
* Update Content_Renderer docs for new return type
* Preserve render() BC, add render_with_styles(), fix PHPCS alignment
* Add changefile(s) from automation for the following project(s): packages/php/email-editor
* Preserve render() with full CSS inlining, add render_without_css_inline()
* Add @since annotation to render_without_css_inline()
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/packages/php/email-editor/changelog/63454-fix-email-editor-css-inlining-efficiency b/packages/php/email-editor/changelog/63454-fix-email-editor-css-inlining-efficiency
new file mode 100644
index 00000000000..1526f671925
--- /dev/null
+++ b/packages/php/email-editor/changelog/63454-fix-email-editor-css-inlining-efficiency
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Email Editor: consolidate redundant CSS inlining into a single pass for improved rendering performance.
\ No newline at end of file
diff --git a/packages/php/email-editor/docs/rendering.md b/packages/php/email-editor/docs/rendering.md
index 448cbf276a1..b1edd184bb3 100644
--- a/packages/php/email-editor/docs/rendering.md
+++ b/packages/php/email-editor/docs/rendering.md
@@ -121,15 +121,19 @@ $text_content = $rendered_email['text'];
The `Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Content_Renderer` class is responsible for rendering only the HTML of block template content and a post. The block template has to contain a `core/post-content` block.
-**Main Method:**
+**Methods:**
+
+#### `render()`
+
+Returns the rendered HTML content as a string with CSS styles inlined.
```php
/**
- * Render the content
+ * Render the content with inlined CSS styles.
*
* @param WP_Post $post Post object.
* @param WP_Block_Template $template Block template.
- * @return string
+ * @return string Rendered HTML content with inlined styles.
*/
public function render(
WP_Post $post,
@@ -137,8 +141,6 @@ public function render(
): string
```
-**Returns:** A string containing the rendered HTML content
-
**Example Usage:**
```php
@@ -148,6 +150,40 @@ $template = get_block_template( $template_id );
$content = $content_renderer->render( $post, $template );
```
+#### `render_without_css_inline()`
+
+Returns both the rendered HTML and collected CSS styles as an array, without inlining the CSS. This is used by the `Renderer` class, which combines these content styles with template styles into a single inlining pass.
+
+```php
+/**
+ * Render the content and collect CSS styles without inlining them.
+ *
+ * @param WP_Post $post Post object.
+ * @param WP_Block_Template $template Block template.
+ * @return array{html: string, styles: string} Rendered HTML and collected CSS.
+ */
+public function render_without_css_inline(
+ WP_Post $post,
+ WP_Block_Template $template
+): array
+```
+
+**Returns:** An array containing:
+
+- `html`: The rendered HTML content (without inlined styles)
+- `styles`: The collected CSS string (without `<style>` wrapper)
+
+**Example Usage:**
+
+```php
+$post = get_post( $post_id );
+$template_id = get_stylesheet() . '//' . $template_slug;
+$template = get_block_template( $template_id );
+$result = $content_renderer->render_without_css_inline( $post, $template );
+$html = $result['html'];
+$styles = $result['styles'];
+```
+
## Core Blocks Integration
The package provides specialized renderers for the most commonly used WordPress core blocks, with plans to eventually cover all core blocks. These individual block renderers are located in the [packages/php/email-editor/src/Integrations/Core/Renderer/Blocks](https://github.com/woocommerce/woocommerce/tree/trunk/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks) directory.
diff --git a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/class-content-renderer.php b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/class-content-renderer.php
index 3c0e3d77419..29dfcde64c1 100644
--- a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/class-content-renderer.php
+++ b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/class-content-renderer.php
@@ -44,13 +44,6 @@ class Content_Renderer {
*/
private WP_Block_Type_Registry $block_type_registry;
- /**
- * CSS inliner
- *
- * @var Css_Inliner
- */
- private Css_Inliner $css_inliner;
-
/**
* Property to store the backup of the current template content.
*
@@ -100,11 +93,18 @@ class Content_Renderer {
*/
private $backup_post_content_callback;
+ /**
+ * CSS inliner
+ *
+ * @var Css_Inliner
+ */
+ private Css_Inliner $css_inliner;
+
/**
* Content_Renderer constructor.
*
* @param Process_Manager $preprocess_manager Preprocess manager.
- * @param Css_Inliner $css_inliner Css inliner.
+ * @param Css_Inliner $css_inliner CSS inliner.
* @param Theme_Controller $theme_controller Theme controller.
* @param Email_Editor_Logger $logger Logger instance.
*/
@@ -115,8 +115,8 @@ class Content_Renderer {
Email_Editor_Logger $logger
) {
$this->process_manager = $preprocess_manager;
- $this->theme_controller = $theme_controller;
$this->css_inliner = $css_inliner;
+ $this->theme_controller = $theme_controller;
$this->logger = $logger;
$this->block_type_registry = WP_Block_Type_Registry::get_instance();
$this->fallback_renderer = new Fallback();
@@ -147,19 +147,39 @@ class Content_Renderer {
}
/**
- * Render the content
+ * Render the content with inlined CSS styles.
*
* @param WP_Post $post Post object.
* @param WP_Block_Template $template Block template.
- * @return string
+ * @return string Rendered HTML content with inlined styles.
*/
public function render( WP_Post $post, WP_Block_Template $template ): string {
+ $result = $this->render_without_css_inline( $post, $template );
+ $styles = '<style>' . $result['styles'] . '</style>';
+ $html = $this->css_inliner->from_html( $styles . $result['html'] )->inline_css()->render();
+
+ return $this->process_manager->postprocess( $html );
+ }
+
+ /**
+ * Render the content and collect CSS styles without inlining them.
+ *
+ * @since 10.7.0
+ *
+ * @param WP_Post $post Post object.
+ * @param WP_Block_Template $template Block template.
+ * @return array{html: string, styles: string} Rendered HTML and collected CSS.
+ */
+ public function render_without_css_inline( WP_Post $post, WP_Block_Template $template ): array {
$this->set_template_globals( $post, $template );
$this->initialize();
$rendered_html = get_the_block_template_html();
$this->reset();
- return $this->process_manager->postprocess( $this->inline_styles( $rendered_html, $post, $template ) );
+ return array(
+ 'html' => $rendered_html,
+ 'styles' => $this->collect_styles( $post, $template ),
+ );
}
/**
@@ -286,14 +306,13 @@ class Content_Renderer {
}
/**
- * Method to inline styles into the HTML
+ * Collects CSS for the rendered content without inlining it.
*
- * @param string $html HTML content.
* @param WP_Post $post Post object.
* @param WP_Block_Template|null $template Block template.
- * @return string
+ * @return string The collected CSS string (without <style> wrapper).
*/
- private function inline_styles( $html, WP_Post $post, $template = null ) {
+ private function collect_styles( WP_Post $post, $template = null ): string {
$styles = (string) file_get_contents( __DIR__ . '/' . self::CONTENT_STYLES_FILE );
$styles .= (string) file_get_contents( __DIR__ . '/../../content-shared.css' );
@@ -342,15 +361,6 @@ class Content_Renderer {
$styles .= $block_support_styles;
- /*
- * Debugging for content styles. Remember these get inlined.
- * echo '<pre>';
- * var_dump($styles);
- * echo '</pre>';
- */
-
- $styles = '<style>' . wp_strip_all_tags( (string) apply_filters( 'woocommerce_email_content_renderer_styles', $styles, $post ) ) . '</style>';
-
- return $this->css_inliner->from_html( $styles . $html )->inline_css()->render();
+ return wp_strip_all_tags( (string) apply_filters( 'woocommerce_email_content_renderer_styles', $styles, $post ) );
}
}
diff --git a/packages/php/email-editor/src/Engine/Renderer/class-renderer.php b/packages/php/email-editor/src/Engine/Renderer/class-renderer.php
index 3a335e35370..913653f98a6 100644
--- a/packages/php/email-editor/src/Engine/Renderer/class-renderer.php
+++ b/packages/php/email-editor/src/Engine/Renderer/class-renderer.php
@@ -9,6 +9,7 @@ declare(strict_types = 1);
namespace Automattic\WooCommerce\EmailEditor\Engine\Renderer;
use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Content_Renderer;
+use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Process_Manager;
use Automattic\WooCommerce\EmailEditor\Engine\Templates\Templates;
use Automattic\WooCommerce\EmailEditor\Engine\Theme_Controller;
use Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalization_Tags_Registry;
@@ -46,6 +47,13 @@ class Renderer {
*/
private Css_Inliner $css_inliner;
+ /**
+ * Process manager
+ *
+ * @var Process_Manager
+ */
+ private Process_Manager $process_manager;
+
/**
* Personalization tags registry
*
@@ -72,19 +80,22 @@ class Renderer {
* @param Css_Inliner $css_inliner CSS Inliner.
* @param Theme_Controller $theme_controller Theme controller.
* @param Personalization_Tags_Registry $personalization_tags_registry Personalization tags registry.
+ * @param Process_Manager $process_manager Process manager.
*/
public function __construct(
Content_Renderer $content_renderer,
Templates $templates,
Css_Inliner $css_inliner,
Theme_Controller $theme_controller,
- Personalization_Tags_Registry $personalization_tags_registry
+ Personalization_Tags_Registry $personalization_tags_registry,
+ Process_Manager $process_manager
) {
$this->content_renderer = $content_renderer;
$this->templates = $templates;
$this->theme_controller = $theme_controller;
$this->css_inliner = $css_inliner;
$this->personalization_tags_registry = $personalization_tags_registry;
+ $this->process_manager = $process_manager;
}
/**
@@ -105,15 +116,17 @@ class Renderer {
/** @var \WP_Block_Template $template */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan
$template = $this->templates->get_block_template( $template_slug );
- $email_styles = $this->theme_controller->get_styles();
- $template_html = $this->content_renderer->render( $post, $template );
- $layout = $this->theme_controller->get_layout_settings();
+ $email_styles = $this->theme_controller->get_styles();
+ $content_result = $this->content_renderer->render_without_css_inline( $post, $template );
+ $template_html = $content_result['html'];
+ $content_styles = $content_result['styles'];
+ $layout = $this->theme_controller->get_layout_settings();
ob_start();
include self::TEMPLATE_FILE;
$rendered_template = (string) ob_get_clean();
- $template_styles =
+ $template_styles =
WP_Style_Engine::compile_css(
array(
'background-color' => $email_styles['color']['background'] ?? 'inherit',
@@ -128,10 +141,16 @@ class Renderer {
),
'body, .email_layout_wrapper'
);
- $template_styles .= '.email_layout_wrapper { box-sizing: border-box;}';
- $template_styles .= file_get_contents( __DIR__ . '/' . self::TEMPLATE_STYLES_FILE );
- $template_styles = '<style>' . wp_strip_all_tags( (string) apply_filters( 'woocommerce_email_renderer_styles', $template_styles, $post ) ) . '</style>';
- $rendered_template = $this->inline_css_styles( $template_styles . $rendered_template );
+ $template_styles .= '.email_layout_wrapper { box-sizing: border-box;}';
+ $template_styles .= file_get_contents( __DIR__ . '/' . self::TEMPLATE_STYLES_FILE );
+ $template_styles = wp_strip_all_tags( (string) apply_filters( 'woocommerce_email_renderer_styles', $template_styles, $post ) );
+
+ // Single CSS inlining pass: combine content and template styles, then inline all at once.
+ $all_styles = '<style>' . $template_styles . $content_styles . '</style>';
+ $rendered_template = $this->inline_css_styles( $all_styles . $rendered_template );
+
+ // Postprocess after CSS inlining (border normalization, CSS variable replacement, etc.).
+ $rendered_template = $this->process_manager->postprocess( $rendered_template );
// This is a workaround to support link :hover in some clients. Ideally we would remove the ability to set :hover
// however this is not possible using the color panel from Gutenberg.
diff --git a/packages/php/email-editor/src/class-email-editor-container.php b/packages/php/email-editor/src/class-email-editor-container.php
index e64642d2b86..563f937dac2 100644
--- a/packages/php/email-editor/src/class-email-editor-container.php
+++ b/packages/php/email-editor/src/class-email-editor-container.php
@@ -217,6 +217,7 @@ class Email_Editor_Container {
new Email_Css_Inliner(),
$container->get( Theme_Controller::class ),
$container->get( Personalization_Tags_Registry::class ),
+ $container->get( Process_Manager::class ),
);
}
);
diff --git a/packages/php/email-editor/tests/integration/Email_Editor_Integration_Test_Case.php b/packages/php/email-editor/tests/integration/Email_Editor_Integration_Test_Case.php
index 6209c6fe7eb..e243c560998 100644
--- a/packages/php/email-editor/tests/integration/Email_Editor_Integration_Test_Case.php
+++ b/packages/php/email-editor/tests/integration/Email_Editor_Integration_Test_Case.php
@@ -258,6 +258,7 @@ abstract class Email_Editor_Integration_Test_Case extends \WP_UnitTestCase {
$container->get( Email_Css_Inliner::class ),
$container->get( Theme_Controller::class ),
$container->get( Personalization_Tags_Registry::class ),
+ $container->get( Process_Manager::class ),
);
}
);
diff --git a/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Content_Renderer_Test.php b/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Content_Renderer_Test.php
index 43c1702edf1..b58685c077b 100644
--- a/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Content_Renderer_Test.php
+++ b/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Content_Renderer_Test.php
@@ -49,7 +49,7 @@ class Content_Renderer_Test extends \Email_Editor_Integration_Test_Case {
}
/**
- * Test it renders the content.
+ * Test render() returns an HTML string with inlined styles.
*/
public function testItRendersContent(): void {
$template = new \WP_Block_Template();
@@ -59,13 +59,14 @@ class Content_Renderer_Test extends \Email_Editor_Integration_Test_Case {
$this->email_post,
$template
);
+ $this->assertIsString( $content );
$this->assertStringContainsString( 'Hello!', $content );
}
/**
- * Test it inlines content styles.
+ * Test render() inlines content styles into the HTML.
*/
- public function testItInlinesContentStyles(): void {
+ public function testRenderInlinesContentStyles(): void {
$template = new \WP_Block_Template();
$template->id = 'template-id';
$template->content = '<!-- wp:post-content /-->';
@@ -76,6 +77,44 @@ class Content_Renderer_Test extends \Email_Editor_Integration_Test_Case {
$this->assertStringContainsString( 'display: block', $paragraph_styles );
}
+ /**
+ * Test render_without_css_inline() returns HTML and collected CSS.
+ */
+ public function testRenderWithoutCssInlineReturnsArray(): void {
+ $template = new \WP_Block_Template();
+ $template->id = 'template-id';
+ $template->content = '<!-- wp:post-content /-->';
+ $result = $this->renderer->render_without_css_inline( $this->email_post, $template );
+ $this->assertArrayHasKey( 'html', $result );
+ $this->assertArrayHasKey( 'styles', $result );
+ $this->assertStringContainsString( 'Hello!', $result['html'] );
+ }
+
+ /**
+ * Test it collects content styles without inlining them.
+ */
+ public function testItCollectsContentStyles(): void {
+ $template = new \WP_Block_Template();
+ $template->id = 'template-id';
+ $template->content = '<!-- wp:post-content /-->';
+ $result = $this->renderer->render_without_css_inline( $this->email_post, $template );
+ $this->assertStringContainsString( 'margin: 0', $result['styles'] );
+ $this->assertStringContainsString( 'display: block', $result['styles'] );
+ }
+
+ /**
+ * Test render_without_css_inline() returns HTML without inlined styles.
+ */
+ public function testRenderWithoutCssInlineDoesNotInlineStyles(): void {
+ $template = new \WP_Block_Template();
+ $template->id = 'template-id';
+ $template->content = '<!-- wp:post-content /-->';
+ $result = $this->renderer->render_without_css_inline( $this->email_post, $template );
+ $paragraph_styles = $this->getStylesValueForTag( $result['html'], 'p' );
+ // Content_Renderer no longer inlines CSS; that happens in Renderer.
+ $this->assertNull( $paragraph_styles );
+ }
+
/**
* Test It Renders Block With Fallback Renderer
*/