Skip to content

Fix/add critical css

Nick Meijer requested to merge fix/add-critical-css into master

Add hook_preprocess_html for critical CSS handling

Background

We added a critical CSS feature to improve pagespeed scores. The idea is simple: instead of loading the full stylesheet in a render-blocking way, we inject the most important above-the-fold CSS inline so the page can render immediately, and defer the full stylesheet to load afterwards.

What went wrong

Getting this to work cleanly in Drupal turned out to be harder than expected, mostly due to how Drupal's hooks work internally.

The first approach was to handle everything in hook_library_info_alter and hook_page_attachments. The problem with hook_library_info_alter is that Drupal caches its output. This means any logic that depends on the current page or node — like checking whether critical CSS exists for this content type — is completely unreliable. Depending on which page was loaded first after a cache rebuild, the CSS would either load twice or not at all.

hook_page_attachments has a different problem: it doesn't fire on error pages like 403 and 404. This is because the library is attached through the react_app twig template, which simply doesn't render on error pages. So whenever critical CSS preloading was enabled, error pages ended up with no stylesheet at all.

We also looked into removing the render-blocking stylesheet in hook_page_attachments_alter, but that doesn't work either — Drupal renders library CSS separately from html_head, so it's not accessible there.

How we fixed it

The solution was to introduce hook_preprocess_html, which runs on every page including error pages and has full access to the current route and node. This hook now owns all CSS loading when critical CSS preloading is enabled, and handles three situations:

  1. Node with critical CSS — injects the critical CSS inline, loads the full stylesheet deferred with an onload trick, and adds a <noscript> fallback for browsers without JavaScript.
  2. Node without critical CSS — loads the full stylesheet normally, no tricks needed.
  3. Error pages and other non-node pages — same as above, just a regular stylesheet.

hook_library_info_alter is now simplified to just skip CSS entirely when critical CSS preloading is enabled, since the new hook handles it. And hook_page_attachments now always attaches the library explicitly so JavaScript still loads correctly on error pages.

Critical css

For each content type in the website a form gets rendered on: admin/structure/uncinc_react_frontend_critical_css . The critical css is now manually extracted with the minimalcss package: https://github.com/peterbe/minimalcss?tab=readme-ov-file. When no critical css is added for a content type the preloading/deferring of the css is completely skipped. It's also skipped for things like error pages, since this was causing issues.

Generating critical css

Critical css is now extracted based on the production styling. the minimalcss package as stated above should be installed globally. In your terminal you should then be able to extract the minimalcss with the command as stated in the readme: https://github.com/peterbe/minimalcss?tab=readme-ov-file.

Updating critical css

Since we also need to update the critical css when something changes, I've added a param which can be pasted against any url: no_critical=1 . This will let you view the page without any critical css, and defaults to the way the page was originally rendering css. This is needed because to be able to extract the correct css we don't want any old critical css in our way.

New config

The changes rely on a new config called enable_critical_css_preloading when this is set to true the css links will be loaded in async. Note that this functionality is not relying on the enable_preload config anymore.

Edited by Nick Meijer

Merge request reports

Loading