Problem/Motivation

By default, all CSS is added to the page's head, and all JS is added before the closing body tag.
Optionally, by setting header: true in a library definition, that library's JS will be inserted into the page head instead of at the end of the page.

Page rendering is blocked while CSS is loaded and parsed, so non-essential CSS increases the time required for the browser to first render the page. Only adding CSS essential to the initial page render to the document head reduces the number of requests required and bytes transferred before the browser can perform the first paint of the page, improving perceived performance.

Some examples of items that are candidates for deferring CSS:

  • The portion of interactive menus only visible on user interaction
  • Interactive elements that are only available after initialization by JS
  • Modal frames and their content

Proposed resolution

Add an attribute to library definitions to allow inserting CSS files before the closing body tag.

Issue fork drupal-2989324

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

gapple created an issue. See original summary.

gapple’s picture

HtmlResponseAttachmentsProcessor::processAssetLibraries() is where the variables for placing assets on the page are created.
AssetResolver::getCssAssets() does the work of retrieving, ordering, and optimizing CSS files for all of the libraries included on the page being rendered.

AssetResolverInterface::getJsAssets() returns an array of two items, each item being an array of the JS files for the head and footer of the page.
AssetResolverInterface::getCssAssets() returns an array of all of the CSS assets for the page, so I'm not sure how to separate the CSS between header and footer without changing the API.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

gapple’s picture

Since HTML 5.2, and in the current HTML Living Standard, <link rel="stylesheet"> is allowed within the body of a page: https://html.spec.whatwg.org/multipage/links.html#body-ok

This is an article on how the change in Chrome's rendering behaviour with link stylesheets in body markup allows progressive style loading:
https://jakearchibald.com/2016/link-in-body/

gapple’s picture

https://www.drupal.org/project/attachinbody

This module adds a new Twig function as a replacement for core's attach_library, {{ attach_library_inbody('extension/library') }}, that outputs assets within the calling template, and removes them from the page header or footer. This is more flexible for component based styling, and also allows selectively adding CSS to the footer of the page by including it in the page template.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

catch’s picture

@gapple we'd need to add a new interface with a new method, update the existing implementations to support the new method, as well as the new system asset controllers to handle it. When only the old method/interface is implemented, would keep everything in the header and ignore the declaration.

Some naming issues to deal with and the logic is fairly complicated (although most could be copied from the js version), but it's doable.

catch’s picture

I was wondering what happens with this and big pipe.

The answer is that big pipe relies on the AJAX system, and the AJAX system does:

   add_css(ajax, response, status) {
      if (typeof response.data === 'string') {
        Drupal.deprecationError({
          message:
            'Passing a string to the Drupal.ajax.add_css() method is deprecated in 10.1.0 and is removed from drupal:11.0.0. See https://www. drupal.org/node/3154948.',
        });
        $('head').prepend(response.data);
        return;
      }

I wonder if we actually need to add the CSS to the HEAD of the document now that it can appear in the body? But also wonder what this means for FOUC, presumably we'd still want the new CSS file to be downloaded before the new HTML is rendered.

This makes me think about this solution in general. If we add js-style header and footer to library definitions, then this allows libraries to determine where they're loaded, but I don't think it's possible for the module to know what's going to be appropriate.

Instead maybe we want everything to work a bit more like BigPipe does:

For CSS that is specific to certain page elements, if it's always loaded via #attached or single directory components, then BigPipe kind of handles it because those CSS files will not be in the aggregates when the page is initially loaded, allowing the page to render faster not just because it's being streamed but because placeholdered content will load the CSS later too.

Then I looked at Renderer::renderPlaceholder(), and I think it might be possible to change that (or add a new placeholder strategy) that works similar to BigPipe - i.e. collect the assets from the placeholder, build any CSS aggregates right there and render them just before the markup, leave the JS handling as-is.

It would then be up to themes to rely on #attached to add CSS as much as possible to take advantage of this.

But this way:

- CSS for non-placeholdered elements and in the theme's .info.yml 'libraries' key goes in the head as now.
- CSS for placeholdered elements - rendered when the placeholder is.

catch’s picture

To make #15 work we would need to track during the request which libraries have had their CSS rendered already, and then remove them from the libraries to render at the end of the request only for the CSS renderer. This would be a bit like ajaxPageState but it would have to be tracked separately because the JS still wants to be all together at the end of the page. Would need to happen in HtmlResponseAttachmentsProcessor::processAssetLibraries()

wim leers’s picture

+1 for this.

#15 is already how BigPipe has always worked for no-JS BigPipe clients! 😄

This would be a bit like ajaxPageState but it would have to be tracked separately because the JS still wants to be all together at the end of the page.

Why would it be different for CSS than for JS?

catch’s picture

For js for anything that's not in the header, we already load it in the footer so that it's non-blocking. If we started progressively rendering (not sure what to call it, but using this instead of 'inline') JavaScript assets it would then block rendering again - so we should keep it in the footer as it currently is.

For CSS it is blocking and we currently render all of it in the header, so by progressively rendering some CSS as we go, we're making it less blocking.

My first idea here was to add the 'header/footer' concept to CSS, but I think adding the assets 'progressively' with the placeholders they're attached to is the way to avoid FOUC.

So for JavaScript header + footer is good, but for CSS header + progressive is good.

I haven't tried to make this work yet, but I think it might be able to look something like this:

All the fun is in HtmlResponseAttachmentsProcessor::processAttachments() and the rest of that class.

Before rendering placeholders, get the current set of libraries. This gets us all of the page-level CSS that needs to be in the header (even if it's also attached via a placeholder).

When we render placeholders, we get $assets from the rendered placeholder.

For CSS, we then prepend the CSS link tags to the markup, but we need to exclude the following:
1. ajax_page_state
2. Page-level libraries
3. Any other libraries already prepended to a different placeholder.

Then when we render the page level CSS (in the place we already do that), we can just use what we got in the first place. For JavaScript we'll need to maintain an extra assets array, and this one gets all the placeholder libraries added to it as it currently does.

catch’s picture

I've just realised you might have meant essentially the opposite with #17.

We could send non-footer JS that's attached to a placeholder in the same way as CSS - that would then get it out of the header too, that would give us header + progressive + bottom JavaScript then and should work fine.

catch’s picture

Title: Allow CSS to be added at end of page » Allow CSS to be added at end of page by rendering assets with placeholders
Issue tags: +front end performance, +Performance

Just took me 10+ minutes to find this issue again, trying to make it a bit more searchable.

I had another look at what would be necessary to do this, and the above is about right, but it would be a non-trivial change.

In wondering what would benefit from the change in core, I discovered that'd be less than you might expect, due to #3432183: Move system/base component CSS to respective libraries where they exist. So we might want to work on re-organising things a bit before spending too much effort here.

catch’s picture

When https://jakearchibald.com/2016/link-in-body/ was written, chrome would still block rendering on stylesheets in the body, but now it doesn't - just got this confirmed by someone from the google chrome team.

However, Safari apparently does block rendering on these stylesheets (although it wouldn't for ones rendered via big pipe that use loadjs because it can't know they're possibly going to be added). Don't have a reference for this yet.

Firefox more or less does what Chrome does.

Unresolved HTML spec issue: https://github.com/whatwg/html/issues/1349

Safari not optimizing for this wouldn't necessarily mean we shouldn't do it, if it's an improvement in chrome/firefox and neutral or neutral-ish for safari.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.