- Issue created by @larowlan
- π¬π§United Kingdom catch
Adding API support is a good idea, we could try to implement it in Olivero too maybe.
β¨ Allow CSS to be added at end of page by rendering assets with placeholders Active is trying to defer CSS.
Are you adding this to individual files or the library definition? And what happens if a critical file has a dependency on a non-critical one?
- π¬π§United Kingdom catch
β¨ [PP-2] Support inline and async CSS Postponed: needs info is already open but there was no progress there and I marked it 'needs more info' some time ago, so possibly we should start fresh and mark that issue duplicate of this.
- π¦πΊAustralia larowlan π¦πΊπ.au GMT+10
Pushed up some work based on our client projects but it doesn't work with asset aggregation turned on - that's because the file doesn't exist the first time around.
It works on our client project because we have build tooling for CSS so don't use core's aggregation for the FE theme
Will have to have a think about how to do this in a way that is compatible with the lazy asset optimization/rendering.
- π¦πΊAustralia larowlan π¦πΊπ.au GMT+10
Added
preprocess: false
to the inline items for now, but ideally we could automate that. Perhaps we need to move critical up to the top level of the definition - π¦πΊAustralia rikki_iki Melbourne
Maybe something like this?
global: css: critical: css/critical-stuff.css: {} base: css/normal-stuff.css: {}
Reads a bit better, but probably changes the whole implementation :)
- πΊπΈUnited States luke.leber Pennsylvania
This feature would allow the Inline All CSS β module to continue to live in Drupal 11+.
- π¬π§United Kingdom catch
Some aspects of implementing this were bothering me but I think I might finally have thought through it properly.
Adding critical: true support for individual files seems like it could be prone to a lot of fragility - e.g. if library A includes a critical file, but also depends on library B that doesn't, then what should happen?
It's also not possible in some cases for a library definition to 'know' if it's going to be above the fold or not. Something like navigation or toolbar knows it's going to be above the fold, a few theme libraries know files are going to be used above the fold (Olivero menus) or below the fold (Olivero footer and powered by), but a lot of other things are in the middle and might differ even between different pages on the same site. Say libraries added by text formats or most SDCs.
Also it feels like it's only worth inlining CSS if we also defer all other CSS - otherwise there are still render blocking network requests and with aggregation/minification we may not even save any network round trips (and browser caching would be worse), so it could be an overall loss if there are un-inlined, non-deferred stylesheets in the middle.
So I think we would want to have it so that all CSS is either inline or deferred, if any CSS is inlined.
I've been working on π Refactor system/base library Needs work , where I've been able to reduce the unused (and by definition not-critical) CSS that core loads by default from about 7kb to about 1kb. Opened a related issue for Olivero that would remove a couple of libraries (and 1-3 kb) from most pages too π [meta] Olivero page weight improvements Active .
Once those issues are completed, then core will only be loading CSS that it is used by elements on the page actually being viewed, and there will be an absolute minimum of unused CSS out of the box., except for maybe 1-2kb that's a bit harder to factor out.
If there's very little unused CSS on most pages, which has been possible in core for a decade now but the work to actually realise it was never completed, then we don't need to worry about if CSS is going to be used but only where it applies above or below the fold. This is a much better state to start from, both compared to Drupal previously but also compared to other projects/sites with more monolithic CSS and no #attached.
If the only libraries we can be certain will never appear above the fold are theme libraries for footer elements, then maybe we only need to be able to mark those ones as deferred, and at the library level we don't need to mark anything 'critical'. In Olivero it would be the couple of footer stylesheets and the powered by block CSS to defer.
Instead of treating individual CSS files as critical, we could introduce a 'mode' where any non-deferred CSS is treated as critical and inlined. We might want to make that for anonymous users only, so that authenticated/admin users who we know will hit multiple pages and repeat visits just rely on browser caching of assets instead.
That would mean rather than the API to mark individual files as critical, we could add the inline CSS support feature directly to core and it becomes a site builder decision (so closer to how the contrib modules currently work - although I have to admit I haven't used them).
If a site really wants to fine tune things and take some CSS out of 'critical' and defer it instead, it can manually mark those files as deferred - either via libraries override or via hook_library_info_alter().
This would also indirectly fix issues like π Preload lora-v14-latin-regular Active because the font-face declarations move to inline, so we don't need to decide which fonts to preload or not - the fonts would just be loaded early if they're needed, and not if they're not.
So I think if we can agree that we don't need a third layer of neither critical nor deferred CSS, then that might simplify things here and also allow us to provide support directly in core.
- πΊπΈUnited States luke.leber Pennsylvania
Also it feels like it's only worth inlining CSS if we also defer all other CSS
100%
I am mildly concerned about how the Drupal off-canvas CSS inline style rewrites would work with this. They were somewhat quirky sometimes with inline_all_css π₯.
Overall, I think that some real world data should be collected with this before arriving on a consensus. While I have a very strong suspicion that it'd lower LCP for most sites/themes, that intuition should be both quantified and peer reviewed.
In the case of the Penn State World Campus theme, inlining all render blocking resources significantly lowered LCP in lab testing (by a complete http round trip π₯³), but we haven't went forward with this in production yet. Sometimes RUM doesn't necessarily agree with what's measured in the lab.
- π¬π§United Kingdom catch
@lleber I think we could add support for defer, and implement it for core CSS that can use it, independently of what we do here? Apart from finishing off the componentization of core's CSS that feels like the next step.
- πΊπΈUnited States luke.leber Pennsylvania
I'm on the fence of deferring CSS. It's often the delicate act of striking a balance between LCP and CLS. The list of things that you truly get for free when deferring CSS is pretty small. Of course, dynamic content added via direct user action (ajax stuff?) and non-visual content that can only be made visible by user interaction (the skip to content link visual styles -- not the css that hides it visually by default?) are the only ones that come to mind.
Users can even land on the page footer with the right #fragment in some cases. We provide dozens of jump links to help people link directly to the correct information, which in some cases means that the footer is effectively above the fold.
I tend to lean toward prioritizing CLS optimization where there's a choice (no one likes to see incomplete stuff or content that bounces around).
- π¬π§United Kingdom catch
So rather than footers we could maybe look at contextual.module.css and drupal.dialog.off_canvas and similar? But then that means always inlining footer CSS which seems on-optimal for regular requests.
Would be good to get thoughts on β¨ Allow CSS to be added at end of page by rendering assets with placeholders Active - this would load the CSS as blocking, but just above the element getting rendered.
- πΊπΈUnited States luke.leber Pennsylvania
I don't see much utility in adding render-blocking CSS at the end of
<body>
. The HTML spec isn't particularly helpful there and it's really a wild west in terms of how user agents should behave. I did some brief chrome testing and it doesn't look like it matters if render-blocking styles are in the head or in the body. First paint seems to be blocked in either case equivalently. Adding the feature to make certain styles non-blocking seems to have a lot stronger of a behavior guarantee than relying on progressive painting.Something about
<!-- ... --> <link href="style.css" rel="preload" as="style" onload="this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="style.css"></noscript> </body>
has an aura of elegance around it.
- π¬π§United Kingdom catch
<!-- ... --> <link href="style.css" rel="preload" as="style" onload="this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="style.css"></noscript> </body>
Wouldn't this violate some Content Security Policies?
We could formalize it by using loadJs (which is already in core and used by BigPipe to load both JavaScript and CSS), but that means an additional HTTP request to load the JavaScript in the first place, although that HTTP request wouldn't be render blocking, so.... maybe? loadjs is 1.8kb uncompressed.
- πΊπΈUnited States luke.leber Pennsylvania
loadCSS is more or less abandonware nowadays. Regarding CSP's there are always some complications there, but nothing insurmountable. CSP has a great API that can probably handle anything we throw at it.
https://git.drupalcode.org/project/inline_all_css/-/blob/1.0.x/src/Event... is how I added CSP support to inline_all_css. Granted core wouldn't implement a subscriber, obviously. I guess CSP would have to come up with some solution to deploy in tandem with this feature.
- π¦πΊAustralia mstrelan
CSP without unsafe-inline will not allow the onload attribute, but you can use an event listener in an inline script with a nonce or hash instead.
- π¦πΊAustralia larowlan π¦πΊπ.au GMT+10
FWIW we used #attached csp_hash support from the CSP module to allow-list the inline JS
There's a lot of great stuff in that module that probably belongs in core as an API