diff --git a/core/core.services.yml b/core/core.services.yml index 5ffa0d96f4d92ee014d4d8b6226a26caf3fc38d8..dedc278b68b0b12387e83dcf367265704ecd1dc6 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 162a7bccbe8c0659710ddaac2b975819abd1012f..4aae4316ccab2a24818eb4b22275f3269a976d14 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,95 @@ 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. Be sure to mark the files as + * preprocess: false too so they're not aggregated. + * + * @param array $assets + * 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/tests/Drupal/FunctionalTests/Theme/OliveroTest.php b/core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php index c3be4d60279e8b70c7b56f40864834b85698578a..d63440da66642e7d37b7601cd1e5c50ba4d443a3 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'); } diff --git a/core/themes/olivero/olivero.libraries.yml b/core/themes/olivero/olivero.libraries.yml index d759787b762a06086c749edab10fb77b4936ff27..0478b8efd268c66bc966a8b673638a271ce12d3c 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 }, 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: {} - css/layout/grid.css: {} + 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: {}