Allow CSS to be added at end of page by rendering assets with placeholders

Created on 30 July 2018, over 6 years ago
Updated 19 March 2024, 8 months ago

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.

Feature request
Status

Active

Version

11.0 🔥

Component
Asset library 

Last updated about 5 hours ago

No maintainer
Created by

🇨🇦Canada gapple

Live updates comments and jobs are added and updated live.
  • Performance

    It affects performance. It is often combined with the Needs profiling tag.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇬🇧United Kingdom catch

    @gapple we'd need to add a new interface with a new method, update the existing implementations to support both versions as well as the new system asset controllers.

  • 🇬🇧United Kingdom catch

    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.

  • 🇬🇧United Kingdom catch

    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()

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    +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?

  • 🇬🇧United Kingdom catch

    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.

  • 🇬🇧United Kingdom catch

    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.

  • 🇬🇧United Kingdom catch

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

Production build 0.71.5 2024