4

I want to implement Asynchronously loading CSS files for faster performance. However I want security too, so I want my site to have CSP.

<link rel="stylesheet" media="print" class="AOcssLoad" .... onload="this.onload=null;this.media='all';" />

Without going into details it wants me to avoid things like onload and many other JS that are part of elements.

I want it to look like this

<link rel="stylesheet" media="print" class="AOcssLoad" href="" />

Please suggest a way to achieve Asynchronous CSS files without inline JS as used above.

We can use inline <script> tags or seperate JS files.

I tried the below code as an inline JS.. Below is the HTML for the JS,

<script  nonce="" type="text/javascript" data-exclude="true">

var Script = document.getElementsByClassName("AOcssLoad");
for (var i = 0 ; i < Script.length; i++) {
    this.className += " Loading";
    Script[i].addEventListener("load", function({
        this.onload=null;this.media="all";
        this.className += " OnLoad";
    });
}
</script>

While it works, it's highly unreliable.

I cannot comprehend the problem, but I shall say it works only 50% of the times, sometimes just reloading the page can solve/break the problem, with no apparent change to css/html/cache as such.

Please help me improve on this, or build a better approach for it.

Edit:

As Suggested in Comments I tried different methods, including the links to other resources from GitHub.

Those methods are unreliable I would say they work less than 50% of times. However I tried to use jQuery(document).ready() and add media="all" to all the css files, but that increases TBT (Total Blocking Time) thus impacting my site performance

Edit 2:

As many of you are repeatedly pointing out in answers, using DOMcontentLoaded and many other ways can help in doing what I want to implemnt.

However these approaches all contribute to significant increase in TBT (Total Blocking Time).

An approach that doesn't harm the TBT would be appreciated.

Aditya Agarwal
  • 52
  • 1
  • 14
  • 2
    AFAIK `link` tag loads css files asynchronously. – Teemu Sep 14 '21 at 12:32
  • @Teemu nope. The css file for mentioned was from Plugin Autoptimize, and developer confirmed that the onload script that I inserted is compulsory for CSS files to load async as a general rule – Aditya Agarwal Sep 14 '21 at 12:33
  • This may help you: https://github.com/filamentgroup/loadCSS/issues/312 – Bettylex Sep 14 '21 at 13:05
  • @Bettylex Agreed, this was helpful in building the script I added in question... It was only working for 1 ID, and I had multiple css styles, sometimes even 10... will try to modify – Aditya Agarwal Sep 14 '21 at 13:20
  • @Bettylex look at the JS included in my question, it was inspired from ur link only, however I adapted to work with classes using for loop. But in it's core it's pretty much same. Yet I can confirm inconsistent working on my site. It works ~50% of times, and I cant diagnose problem, so resorted to finding a different method – Aditya Agarwal Sep 14 '21 at 13:35
  • In the same page you can find [an example](https://github.com/filamentgroup/loadCSS/issues/312#issuecomment-600022209) to apply it on several scripts at the same time. – Bettylex Sep 14 '21 at 13:53
  • Yeah... I meant that only tried both.. You can yourself check my homepage https://milyin.com 5 scripts none of them are converting to media="all" – Aditya Agarwal Sep 14 '21 at 13:57
  • @Bettylex with `jQuery(document).ready()` i did a temporary workaround that sets all files to media="all" when document is ready, but that unnecessarily delays css execution which is not good.. CSS files load far before the document is ready... – Aditya Agarwal Sep 14 '21 at 14:01
  • @Bettylex Hey, I think the issue is that onload doesn't trigger when the css file is loaded from Service Worker's cache... Any idea on how to trigger an event upon file being loaded from Cache/Service Worker – Aditya Agarwal Sep 14 '21 at 15:03
  • I do not understand the issue. Why don't you just include `link` tags with a `href` attribute pointing to the desired location? – Lajos Arpad Sep 19 '21 at 10:47
  • @LajosArpad this is possible and works functionally perfect, however it is nightmare for performance, even Google recommend to Async the CSS files for better site performance, and on the internet almost every method I found used Inline JS to implement this, which violates CSP. At the end it's very important to async CSS in `link` tags – Aditya Agarwal Sep 19 '21 at 10:49
  • So, if I understand the problem, then you have some very large CSS files to download and it's a nightmare from the user's perspective to wait for them all to be loaded. So, if I understand the issue correctly, you want to postpone the CSS load after the page has been successfully loaded and then iterate the CSS paths you want to include and load them all. I wonder whether loading them sequentially, one-by-one would solve the issue. If not, then please add more details of how this is not working. – Lajos Arpad Sep 19 '21 at 11:08
  • Yes, file is large, and it's a nightmare. You are some what correct. Async means: Downloading the file, and executing while simultaneously loading other files. Traditionally when a CSS file loads and executes, no other file is loading at that time, this costs a lot of time. By doing the inline JS mentioned in question, the CSS file executes while allowing other files to be simultaneously download, saving precious time. – Aditya Agarwal Sep 19 '21 at 11:16
  • Personally, I would abandon this and go for HTTP2 protocol, cause that's what you need indeed. – skobaljic Sep 21 '21 at 08:17
  • Even I use HTTP/2 protocol, but that doesn't mean every user's browser would support it. As a true web dev, it's important to even take care of the edge cases where their wont be support for HTTP/2 – Aditya Agarwal Sep 22 '21 at 09:43

5 Answers5

1

I would suggest using fetch().then() and injecting it as a style element:

var stylesheetURLS = ["style.css", "style2.css"]
stylesheetURLS.forEach(url => {
  fetch(url).then(response => response.text()).then(stylesheet => {
    var styleElement = document.createElement("style");
    styleElement.textContent = stylesheet;
    document.head.append(styleElement);
  });
});

I am not sure if fetch is slow, but I wouldn't be surprised if it is.


Alternative to fetch: XMLHttpRequest

var stylesheetURLS = ["style.css", "style2.css"];
stylesheetURLS.forEach(url => {
  var request = new XMLHttpRequest();
  request.open("GET", url);
  request.send();
  request.onload = function() {
    var styleElement = document.createElement("style");
    styleElement.textContent = request.responseText || request.response;
    document.head.append(styleElement);
  }
});

I'm again not sure if this is any faster than fetch

Rojo
  • 2,749
  • 1
  • 13
  • 34
  • This could be a good approach, however I am using WordPress, this means file names change so I cant specify the urls in list, secondly this would mean that stylesheet is added after the scripts are loaded, which will further delay execution. Haven't tested it yet, but still think it will be slow – Aditya Agarwal Sep 26 '21 at 05:40
  • @AdityaAgarwal I'm not entirely sure how WordPress works; Is WordPress automatically loading the stylesheets for you? And you are just trying to change how it loads? – Rojo Sep 26 '21 at 14:39
  • The purpose is to reduce the blocking time... Implementing CLS and at the same time Async JS wil help in performance, but all current implementations lead to increase in TBT... I am using caching, so cache files are generated very often, so filenames keep changing... Well, that can be still dealt with worrk around, yet I tried your code, and google lighthouse asked not to use things like fetch and append, at the same time it had no noticeable performance jump... I can still solve the problem of filename changing by using some php, but ultimately it didnt improve performance – Aditya Agarwal Sep 27 '21 at 04:40
  • @AdityaAgarwal You could try using XMLHttpRequest. I edited my post to include that alternative. – Rojo Sep 27 '21 at 12:44
1

I have tested downloading a css file that takes over 6 seconds to download and I can confirm that downloading does not contribute to the TBT. As stated by google TBT is the time that the browser is unavailable for user input for example when the user clicks a button or scrolls the browser will not react. A long TBT is often caused by the main thread being busy because it has too much work to do. I think processing the CSS (applying all the rules to the html) is what your TBT increases because downloading files is already done in the background (async) and won't be the cause of a long TBT time.

Below an example that the TBT isn't increased when downloading a large file:

TBT: enter image description here

As you can see the downloading takes more than 6 seconds but doesn't add up to the TBT: enter image description here

Stan
  • 629
  • 7
  • 18
0

You could use a vanilla JS function based on jQuery .ready():

document.addEventListener("DOMContentLoaded", () => {
  document.querySelectorAll(".AOcssLoad").forEach(el => {
    el.media = "all"
    console.log(`Loaded: ${el.href}`)
  })
});
<link rel="stylesheet" media="print" class="AOcssLoad" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" />
<link rel="stylesheet" media="print" class="AOcssLoad" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap-utilities.min.css" />
<link rel="stylesheet" media="print" class="AOcssLoad" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap-reboot.min.css" />
<link rel="stylesheet" media="print" class="AOcssLoad" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap-grid.min.css" />
<h1>Hello world!</h1>

However, no matter which solution you choose, be aware you will have to deal with Flash of Unstyled Content (FOUC). Consider other approachs for managing your stylesheet files.

Felipe Saldanha
  • 1,087
  • 8
  • 17
  • Hey Felipe, sorry for late reply, I am already loading Critical CSS above the fold, so majority of viewable content is rendered via inline style, thus the FOUC will not trouble me. I like your approach, but hear me out. I have been using jQuery(document).ready to change the media attr to all, it works fine, but I observed significant increase in my site's Total Blocking Time (TBT), and if I remove CSP and use the Inline JS in then my TBT is nice again. So with due respect I think your DOMContentLoaded will also increase TBT... – Aditya Agarwal Sep 22 '21 at 09:41
  • You are possibly right, but kindly give it a try. This method should have a better perfomance than jQuery. As an alternative, you could also try [`document.onreadystatechange`](https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState#readystatechange_as_an_alternative_to_domcontentloaded_event). – Felipe Saldanha Sep 22 '21 at 12:35
  • Agreed... Gave it a try, my TBT improved but it is still more than 300ms, will hope to find a better solution... And Yea, as jQuery executes after pure js, and I added your script as inlin JS, so it shaved off 20-30ms but still not enough IMO... Google wants it to be a lot less than that. Anyways thanks – Aditya Agarwal Sep 23 '21 at 03:11
0

why not inject it with

<script>
document.write("<link rel=\"stylesheet\" media=\"print\" class=\"AOcssLoad" href=\"\" />
");
</script>

and place the <script> tags right above the place where you want the <link> tag to be.

or just

<link rel="stylesheet" media="print" class="AOcssLoad" href="" />

nothing happens all except for the css will load asynchronously and you can have csp

user135142
  • 135
  • 9
  • Well, Google's LIghthouse Best Practices suggest to not use Lighthouse, the ultimate goal is to improve the site without harming anything else you see... I appreciate your approach and it is the easiest among all, but my file url changes constantly so would have to edit this script like every 2 hours to update the url and document.write isnt right imo – Aditya Agarwal Sep 22 '21 at 09:38
0

Your script is just wrong, and it will just not work, not even 50% of the time.

var Script = document.getElementsByClassName("AOcssLoad");
for (var i = 0 ; i < Script.length; i++) {
    this.className += " Loading"; // here `this` is `window`
    Script[i].addEventListener("load", function({ // <-- this is an Object
        this.onload=null;this.media="all"; // <-- this is a syntax error
        this.className += " OnLoad";
    });
}

Here is a rewrite of what you probably meant to write, which also includes a check to see if the link got loaded before your script ran, just in case (e.g cache).

const links = document.getElementsByClassName("AOcssLoad");
for (const link of links) {
  link.className += " Loading";
  if(link.sheet) { // "already loaded"
    oncssloaded.call(link);
  }
  else {
    link.addEventListener("load", oncssloaded, { once: true });
  }
}
function oncssloaded() {
  this.media = "all";
  this.className += " OnLoad";
}
<link rel="stylesheet" media="print" class="AOcssLoad" href="data:text/css,body{color:green}" />
Some green text
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • As I said, the problem is CSP not it's execution. For UX it is important for CSS to be loaded early, that is inside Head. Just because I can load it in Body, doesn't mean that I "should" load it in their. The Async ensures faster execution, thus loading css early as well quickly. However it troubles with CSP, so I need a better approach to Async the rather than delay its loading. – Aditya Agarwal Sep 23 '21 at 06:42
  • "The Async ensures faster execution" what? No, async doesn't ensure that at all, on the contrary. If you need the CSS to be loaded faster, then use a preload tag on a previous page. – Kaiido Sep 23 '21 at 07:43
  • My bad, I meant faster execution of entire page, not the file itself, by allowing other files to keep downloading while the CSS file executes, it cuts off a lot of time and saves overall "execution time"... – Aditya Agarwal Sep 23 '21 at 11:20