Skip to content
Snippets Groups Projects

Resolve #3499829 "Support inlining critical"

5 unresolved threads
4 files
+ 105
8
Compare changes
  • Side-by-side
  • Inline
Files
4
@@ -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.
Please register or sign in to reply
*/
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);
Please register or sign in to reply
if (!\is_string($contents)) {
return NULL;
}
return [
'#type' => 'html_tag',
'#tag' => 'style',
'#value' => Markup::create($contents),
    • Not minifying seems like a missed opportunity, but I get the concern with performance.

      Or we need to check the status of "preprocess" for the file?

    • This has security implications.

      Things like

      .some-class {
      </style><script>alert('oh noes');</script>

      would have previously been harmless but would now result in script execution.

Please register or sign in to reply
'#attributes' => [
// Add a data-src attribute to aid in debugging/identification.
'data-src' => \basename($path),
],
];
}
return NULL;
}
}
Loading