Asynchronous CSS Loading in Sage

I am having the same problem, but I’m not specifying the CSS route. As far as I understand that’s not needed as the plugin will generate a CSS file from the HTML specified in ‘src’.

What is your config.devUrl value? It works for me with http://localhost:8000, but not if I use my dev or prod URLs.

Asked for help in this thread too Critical CSS plugin

Edit: Forgot to say that I’m running Sage in a docker container.

@Alfalfa, I would prefer not to have to specify the css file(s) and instead have it generate from the src html. That does work, but only when I disable the functionality that adds the media=“print” and onload=“‘all’” in the ‘style_loader_tag’ filter

My config.devUrl config value is my custom local development url, not http://localhost

Your issue could also be related to the media=“print” and onload=“‘all’” issue I am running into as well.

I am using sage in a lando/docker environment.

Hey @RyanBaron thanks for your answer. It’s not a problem with the async, as I have tried the critical CSS with and without it, and it is still failing with the following error:

95% emitting TypeError: Cannot read property 'content-type' of undefined
at temp (/home/jenkins/workspace/some_path/node_modules/critical/lib/file-helper.js:106:37)
at requestAsync.then.response (/home/jenkins/workspace/some_path/node_modules/critical/lib/file-helper.js:204:27)
at process._tickCallback (internal/process/next_tick.js:68:7) error Command failed with exit code 1.

I have been using localhost:8000 as a devUrl the whole time. I have local environment, dev, staging and prod. Everything works fine so far with this devUrl, should I change it to be my dev URL? I don’t understand how the ‘src’ works in here. I’ve tried dev and staging URLs anyway, and it’s still failing in the same point.
If I have a look to the line shown in the error, it’s the following:

    const contentType = resp.headers['content-type']; 

About the async error you have, I had the same problem and solved it adding an URL parameter as follows:

In your webpack.config.optimize.js change the ‘src’ to be src: config.devUrl + '?no-async=true',, and then in your filters:

if (is_admin() || $_GET['no-async']) {
    return $html;

This way it won’t load CSS asynchronously and the critical CSS will be generated without problems. This works for me in localhost as I said, can’t make it work in dev/staging, even removing the async.

Wouldn’t be the same http://localhost or ?

Any ideas?

I don’t fully understand your dev setup, but the can't read content type of undefined combined with the fact that the line throwing that error appears to be dealing with an HTTP response object is what I might expect to see if something was trying to get a CSS file while you were running yarn start. When using HMR Sage 9 doesn’t actually generate any CSS files: it inserts the CSS directly into the DOM so that the styles update without a page reload. Not sure if that’s your issue, though.

That’s true, but the problem appears when running yarn build:production which creates all the assets for production. Doing that locally it creates a ‘dist’ folder with scripts and styles, and I can see my critical.css file being generated. The error appears trying to do the same in my dev server, which is deployed by a jenkins job creating a docker container.

Is your entire project (web server, build process, etc) in a single container, or do you have a couple of networked containers handling different concerns? I fully admit I’m not super familiar with docker, but my understanding is that containers have an internal network they communicate with each other on. Is it possible that your build process is unable to access your web server because the external url you use to access the server isn’t available on the internal network?

I had thought on that but no, it’s in a single container and it should be able to access to any external URL as in the docker container it’s installing different external resources. Any external URL should be valid to get the critical CSS, right? At the moment the config.devUrl is pointing to my dev server, but I could point directly to production site if I wanted too, right?
Just want to make sure that I understand correctly how the devUrl is being used as a ‘src’.

Another concern I had is if the docker container couldn’t use headless Chrome, so I installed it too, but still no luck :frowning:

I get the same thing locally if I don’t use env variables and try to run both HtmlCriticalWebpackPlugin and setting media="print". So it depends on your deployment setup, but if you use trellis to run yarn build:production locally and use env variables to async CSS only in prod then that combo should work.

While playing around with this I also found a bug with getting a critical CSS path. Currently locate_asset() runs twice in critical CSS snippet and inside get_file_contents() so here is the update:

add_action('wp_head', function (): void {
    if ('development' === env('WP_ENV')) {

    if (is_front_page()) {
        // $critical_CSS = locate_asset('styles/critical-home.css');
        $critical_CSS = 'styles/critical-home.css';
    } elseif (is_singular()) {
        // $critical_CSS = locate_asset('styles/critical-singular.css');
        $critical_CSS = 'styles/critical-singular.css';
    } else {
        // $critical_CSS = locate_asset('styles/critical-landing.css');
        $critical_CSS = 'styles/critical-landing.css';

    // if (file_exists($critical_CSS)) {
    if (file_exists(locate_asset($critical_CSS))) {
        echo '<style id="critical-css">' . get_file_contents($critical_CSS) . '</style>';
}, 1);

@ben I’d apreciate if you could update the guide.

1 Like

I a looking to add rel=“preload” in stylesheet and i follow your instruction but not affecting anything.

can you please guide me where i can add

 * Async load CSS
add_filter('style_loader_tag', function (string $html, string $handle): string {
    if ('development' === env('WP_ENV') || is_admin()) {
        return $html;

    $dom = new \DOMDocument();
    $tag = $dom->getElementById($handle . '-css');
    $tag->setAttribute('media', 'print');
    $tag->setAttribute('onload', "'all'");
    $html = $dom->saveHTML($tag);

    return $html;
}, 999, 2);

Best to add it to filters.php.
Keep in mind that this is not going to run in development because we add:

    if ('development' === env('WP_ENV') || is_admin()) {
        return $html;

So comment out this part for testing in development.