From d69bfe080c6747567736745b94d7587a5703bbd2 Mon Sep 17 00:00:00 2001 From: Lee Rowlands <lee.rowlands@previousnext.com.au> Date: Fri, 17 Jan 2025 10:20:05 +1000 Subject: [PATCH 1/4] Rough WIP --- core/core.services.yml | 2 +- .../Core/Asset/CssCollectionRenderer.php | 98 ++++++++++++++++++- core/themes/olivero/olivero.libraries.yml | 10 +- 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index 5ffa0d96f4d9..dedc278b68b0 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1649,7 +1649,7 @@ services: Drupal\Core\Session\MetadataBag: '@session_manager.metadata_bag' asset.css.collection_renderer: class: Drupal\Core\Asset\CssCollectionRenderer - arguments: [ '@asset.query_string', '@file_url_generator' ] + arguments: [ '@asset.query_string', '@file_url_generator' , '@current_route_match', '@request_stack'] asset.css.collection_optimizer: class: Drupal\Core\Asset\CssCollectionOptimizerLazy arguments: [ '@asset.css.collection_grouper', '@asset.css.optimizer', '@theme.manager', '@library.dependency_resolver', '@request_stack', '@file_system', '@config.factory', '@file_url_generator', '@datetime.time', '@language_manager'] diff --git a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php index 162a7bccbe8c..362545162b85 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php @@ -3,6 +3,9 @@ namespace Drupal\Core\Asset; use Drupal\Core\File\FileUrlGeneratorInterface; +use Drupal\Core\Render\Markup; +use Drupal\Core\Routing\RouteMatchInterface; +use Symfony\Component\HttpFoundation\RequestStack; /** * Renders CSS assets. @@ -16,10 +19,16 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface { * The asset query string. * @param \Drupal\Core\File\FileUrlGeneratorInterface $fileUrlGenerator * The file URL generator. + * @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch + * Current route match. + * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack + * Request stack. */ public function __construct( protected AssetQueryStringInterface $assetQueryString, protected FileUrlGeneratorInterface $fileUrlGenerator, + protected RouteMatchInterface $routeMatch, + protected RequestStack $requestStack, ) { } @@ -76,7 +85,94 @@ public function render(array $css_assets) { $elements[] = $element; } - return $elements; + return $this->inlineCriticalCss($elements); + } + + /** + * Uses an inline style tag for any CSS files that are flagged as critical. + * + * To make use of this, add {attributes: {critical: true} to CSS files in + * your theme or module's libraries.yml file. + * + * @param array $elements + * Existing CSS elements. + * + * @return array + * CSS elements with any CSS inlined as required. + */ + protected function inlineCriticalCss(array $assets): array { + // Only inline CSS if we are using the frontend theme. + // Also skip anything other than HTML requests. + $request = $this->requestStack->getCurrentRequest(); + $wrapperFormat = $request->get('_wrapper_format', 'html'); + // Views AJAX is special and doesn't set wrapper_format properly, so check + // for that too. + $currentRoute = $this->routeMatch->getRouteName(); + if ( + // Not HTML. + $wrapperFormat !== 'html' + // OR views AJAX. + || $currentRoute === 'views.ajax' + // OR Big Pipe placeholders. + || $request->headers->get('accept') === 'application/vnd.drupal-ajax') { + return $assets; + } + + $elements = []; + foreach ($assets as $asset) { + $attributes = $asset['#attributes']; + // Skip files with print media. + if ($asset['#attributes']['media'] === 'print') { + $elements[] = $asset; + continue; + } + // Any CSS file with a critical flag should be inlined. + if (isset($attributes['critical'])) { + $inlineCriticalCSS = $this->inlineCssFile(\substr($attributes['href'], 1)); + if ($inlineCriticalCSS !== NULL) { + $elements[] = $inlineCriticalCSS; + continue; + } + } + // @todo Defer all non critical CSS - + // https://www.drupal.org/project/drupal/issues/2989324 + $elements[] = $asset; + } + + return \array_filter($elements); + } + + /** + * Turn a file into a renderable <style> tag. + * + * @param string $path + * File URL to turn into inline CSS. + * + * @return array|null + * Return NULL if the file does not exist. + */ + protected function inlineCssFile(string $path): ?array { + $path = \strtok($path, "?"); + if (!\is_string($path)) { + return NULL; + } + $file_name = \sprintf('%s/%s', DRUPAL_ROOT, $path); + if (\file_exists($file_name)) { + $contents = \file_get_contents($path); + if (!\is_string($contents)) { + return NULL; + } + return [ + '#type' => 'html_tag', + '#tag' => 'style', + '#value' => Markup::create($contents), + '#attributes' => [ + // Add a data-src attribute to aid in debugging/identification. + 'data-src' => \basename($path), + ], + ]; + } + return NULL; } } diff --git a/core/themes/olivero/olivero.libraries.yml b/core/themes/olivero/olivero.libraries.yml index d759787b762a..92b1ed552813 100644 --- a/core/themes/olivero/olivero.libraries.yml +++ b/core/themes/olivero/olivero.libraries.yml @@ -2,12 +2,12 @@ global-styling: version: VERSION css: base: - css/base/fonts.css: {} - css/base/variables.css: {} - css/base/base.css: {} + css/base/fonts.css: { attributes: { critical: true } } + css/base/variables.css: { attributes: { critical: true } } + css/base/base.css: { attributes: { critical: true } } layout: - css/layout/layout.css: {} - css/layout/grid.css: {} + css/layout/layout.css: { attributes: { critical: true } } + css/layout/grid.css: { attributes: { critical: true } } css/layout/layout-content-narrow.css: {} css/layout/layout-content-medium.css: {} css/layout/layout-footer.css: {} -- GitLab From 2bea4be2c4810796888bd23b23c12b3d2650e87d Mon Sep 17 00:00:00 2001 From: Lee Rowlands <lee.rowlands@previousnext.com.au> Date: Fri, 17 Jan 2025 10:25:32 +1000 Subject: [PATCH 2/4] Opt out of aggregation for criticals --- core/themes/olivero/olivero.libraries.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/themes/olivero/olivero.libraries.yml b/core/themes/olivero/olivero.libraries.yml index 92b1ed552813..0478b8efd268 100644 --- a/core/themes/olivero/olivero.libraries.yml +++ b/core/themes/olivero/olivero.libraries.yml @@ -2,12 +2,12 @@ global-styling: version: VERSION css: base: - css/base/fonts.css: { attributes: { critical: true } } - css/base/variables.css: { attributes: { critical: true } } - css/base/base.css: { attributes: { critical: true } } + css/base/fonts.css: { attributes: { critical: true }, preprocess: false } + css/base/variables.css: { attributes: { critical: true }, preprocess: false } + css/base/base.css: { attributes: { critical: true }, preprocess: false } layout: - css/layout/layout.css: { attributes: { critical: true } } - css/layout/grid.css: { attributes: { critical: true } } + css/layout/layout.css: { attributes: { critical: true }, preprocess: false } + css/layout/grid.css: { attributes: { critical: true }, preprocess: false } css/layout/layout-content-narrow.css: {} css/layout/layout-content-medium.css: {} css/layout/layout-footer.css: {} -- GitLab From 53986bab2825230cf6ddb8b899fc967bdae9b272 Mon Sep 17 00:00:00 2001 From: Lee Rowlands <lee.rowlands@previousnext.com.au> Date: Fri, 17 Jan 2025 10:33:37 +1000 Subject: [PATCH 3/4] Add test coverage --- core/lib/Drupal/Core/Asset/CssCollectionRenderer.php | 3 ++- core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php index 362545162b85..747873703277 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php @@ -92,7 +92,8 @@ public function render(array $css_assets) { * Uses an inline style tag for any CSS files that are flagged as critical. * * To make use of this, add {attributes: {critical: true} to CSS files in - * your theme or module's libraries.yml file. + * your theme or module's libraries.yml file. Be sure to mark the files as + * preprocess: false too so they're not aggregated. * * @param array $elements * Existing CSS elements. diff --git a/core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php b/core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php index c3be4d60279e..d63440da6664 100644 --- a/core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php +++ b/core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php @@ -41,7 +41,7 @@ class OliveroTest extends BrowserTestBase { public function testBaseLibraryAvailable(): void { $this->drupalGet(''); $this->assertSession()->statusCodeEquals(200); - $this->assertSession()->responseContains('olivero/css/base/base.css'); + $this->assertSession()->elementExists('css', 'style[data-src="base.css"]'); $this->assertSession()->responseContains('olivero/js/navigation-utils.js'); } -- GitLab From fbf7a78dd041d039e8ba1e50f92dc5a9948c606f Mon Sep 17 00:00:00 2001 From: Lee Rowlands <lee.rowlands@previousnext.com.au> Date: Fri, 17 Jan 2025 10:35:13 +1000 Subject: [PATCH 4/4] Lint --- core/lib/Drupal/Core/Asset/CssCollectionRenderer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php index 747873703277..4aae4316ccab 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php @@ -95,7 +95,7 @@ public function render(array $css_assets) { * your theme or module's libraries.yml file. Be sure to mark the files as * preprocess: false too so they're not aggregated. * - * @param array $elements + * @param array $assets * Existing CSS elements. * * @return array -- GitLab