Giter Club home page Giter Club logo

loadcss's Introduction

โš ๏ธ This project is archived and the repository is no longer maintained.

loadCSS

A pattern for loading CSS asynchronously [c]2020 @scottjehl, @zachleat Filament Group, Inc. Licensed MIT

Why an ansychronous CSS loader?

Referencing CSS stylesheets with link[rel=stylesheet] or @import causes browsers to delay page rendering while a stylesheet loads. When loading stylesheets that are not critical to the initial rendering of a page, this blocking behavior is undesirable. The pattern below allows us to fetch and apply CSS asynchronously. If necessary, this repo also offers a separate (and optional) JavaScript function for loading stylesheets dynamically.

How to use

As a primary pattern, we recommend loading asynchronous CSS like this from HTML:

<link rel="stylesheet" href="/path/to/my.css" media="print" onload="this.media='all'; this.onload=null;">

This article explains why this approach is best: https://www.filamentgroup.com/lab/load-css-simpler/

That is probably all you need! But if you want to load a CSS file from a JavaScript function, read on...

Dynamic CSS loading with the loadCSS function

The loadCSS.js file exposes a global loadCSS function that you can call to load CSS files programmatically, if needed. This is handy for cases where you need to dynamically load CSS from script.

loadCSS( "path/to/mystylesheet.css" );

The code above will insert a new CSS stylesheet link after the last stylesheet or script that it finds in the page, and the function will return a reference to that link element, should you want to reference it later in your script. Multiple calls to loadCSS will reference CSS files in the order they are called, but keep in mind that they may finish loading in a different order than they were called.

Function API

The loadCSS function has 3 optional arguments.

  • before: By default, loadCSS attempts to inject the stylesheet link after all CSS and JS in the page. However, if you desire a more specific location in your document, such as before a particular stylesheet link, you can use the before argument to specify a particular element to use as an insertion point. Your stylesheet will be inserted before the element you specify. For example, here's how that can be done by simply applying an id attribute to your script.
<head>
...
<script id="loadcss">
  // load a CSS file just before the script element containing this code
  loadCSS( "path/to/mystylesheet.css", document.getElementById("loadcss") );
</script>
...
</head>
  • media: You can optionally pass a string to the media argument to set the media="" of the stylesheet - the default value is all.
  • attributes: You can also optionally pass an Object of attribute name/attribute value pairs to set on the stylesheet. This can be used to specify Subresource Integrity attributes:
loadCSS( 
  "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css",
  null,
  null,
  {
    "crossorigin": "anonymous",
    "integrity": "sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
  }
);

Using with onload

Onload event support for link elements is spotty in some browsers, so if you need to add an onload callback, include onloadCSS function on your page and use the onloadCSS function:

var stylesheet = loadCSS( "path/to/mystylesheet.css" );
onloadCSS( stylesheet, function() {
	console.log( "Stylesheet has loaded." );
});

Browser Support

The loadCSS patterns attempt to load a css file asynchronously in any JavaScript-capable browser. However, some older browsers such as Internet Explorer 8 and older will block rendering while the stylesheet is loading. This merely means that the stylesheet will load as if you referenced it with an ordinary link element.

Changes in version 3.0 (no more preload polyfill)

As of version 3.0, we no longer support or include a polyfill for a rel=preload markup pattern. This is because we have since determined that the markup pattern described at the top of this readme is simpler and better for performance, while the former preload pattern could sometimes conflict with resource priorities in ways that aren't helpful for loading CSS in a non-blocking way.

To update, you can change your preload markup to this HTML pattern and delete the JS from your build.

Since this change breaks the API from prior versions, we made it a major version bump. That way, if you are still needing to use the now-deprecated preload pattern, you can keep your code pointing at prior versions that are still on NPM, such as version 2.1.0 https://github.com/filamentgroup/loadCSS/releases/tag/v2.1.0

Contributions and bug fixes

Both are very much appreciated - especially bug fixes. As for contributions, the goals of this project are to keep things very simple and utilitarian, so if we don't accept a feature addition, it's not necessarily because it's a bad idea. It just may not meet the goals of the project. Thanks!

loadcss's People

Contributors

adammcarth avatar addyosmani avatar albinekcom avatar c-vetter avatar combizs avatar dlemstra avatar dmzza avatar dominikwilkowski avatar evanhahn avatar fatjester avatar gr8bit avatar greenkeeperio-bot avatar jefflembeck avatar johnbender avatar kittygiraudel avatar kkirsche avatar krinkle avatar marcobiedermann avatar mmcev106 avatar nhoizey avatar okj579 avatar philsawicki avatar scottjehl avatar teodragovic avatar thierno2018 avatar tophmey avatar zachleat avatar zalog avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

loadcss's Issues

Double callback with onload

Firefox triggers an onload callback twice when the media attribute is changed before load succeeds.

This does not happen when the ss.media = media || "all"; line is commented out. It does happen even if the media value doesnโ€™t change (for example changing the initial ss.media = "only x"; to ss.media = "all";)

This is probably something we should file upstream at Mozillaโ€™s tracker.

Update: also happens in newest Chrome, Safari 8.0.3 and Iโ€™m assuming others.

Test url: http://filamentgroup.github.io/loadCSS/test-callback.html

Pass in optional arguments as options object

How do you feel about passing in the optional arguments as an options object.

loadCSS('path/to/styles.css', {
  'before': document.getElementsByTagName('script')[0],
  'media': 'all',
  'callback': function() {
    // magic
  }
}

It's a little cleaner when, for example, you only want to use a callback and not toch the before and after arguments.

Success callback

Is it currently possible (or easy to implement a feature that does so) to listen for a success callback function when the stylesheet has loaded successfully? Eg...

loadCSS("stylesheet.css", null, null, function(status) {
    if (status === true) {
        console.log("Stylesheet injected.");
    } elseif (status === false) {
        console.log("Timeout or unsupported browser.");
    }
});

I feel like this could be really useful, what do you guys think? Can you already do this?

Issue with relative path including parent folder

If the CSS href begins with ../, the indexOf test never returns true, the media attribute keeps the only x value, and the setTimeout goes indefinitely.

This is because the browser translates the relative path to an absolute one, that doesn't contain the ../ part anymore.

Bower support

I like the loadCSS approach, currently I've included loadCSS manually . Is there any change you can add a bower.json to the repository so I can use Bower to install it?

Examples

Hey,

Great job with this script. This is not an issue with the code. It's more of a tweak. To make your great idea more useful to more people.

Can you provide some more examples for the non so technical people out there? For example how to setup loadCSS for 5 CSS?

I can help with writing the wiki page also.

[Question] LoadCSS vs. Google Optimized CSS Delivery

I'm curious how loadCSS stacks up against Google's documented CSS delivery mechanism.

The code in question below:

<script>
      var cb = function() {
        var l = document.createElement('link'); l.rel = 'stylesheet';
        l.href = 'small.css';
        var h = document.getElementsByTagName('head')[0]; h.parentNode.insertBefore(l, h);
      };
      var raf = requestAnimationFrame || mozRequestAnimationFrame ||
          webkitRequestAnimationFrame || msRequestAnimationFrame;
      if (raf) raf(cb);
      else window.addEventListener('load', cb);
</script>

Callback too early

The old onload callback worked fine (it's called when the styles are loaded and applied). However the new onloadCSS() callback fires too early.

E.g. loading a stylesheet with #foo { display: none; } and asserting getComputedStyle(document.getElementById('foo')).display === 'none'; fails with the onloadCSS() callback, but passes with the onload callback.

After some trial and error, I determined that this is caused by the ss.media = "only x"; trick. While loadCSS's internal callback that changes the media value does run first, the second instance of the poller from onloadCSS() invokes the callback at a time that the styles are not yet live.

Removing the ss.media hack and its first callback counterpart fixes the issue.

do more

It's really quite simple, I think you could do more if you wish, like:
solve ie imposes a maximum limit of 32 individual styleSheet.

Performance of invalidating all stylesheets

Background

Browsers are built to optimize appending new styles to the end. That's why when you apply an inline style, browsers don't have to recompute the styles of the world in order to apply it.
The model is built to cheaply apply additive style rules; the browser takes the current CSSOM (basically) and applies the new css on top of it. It's cool.

Because CSS order matters, if new CSS is added in the middle (let's say halfway down), then the browser invalidates the style computation of 50% of the styles and has to recompute due to the potential cascade changes.

Current state

loadCSS injects the script like so:

var ref = before || window.document.getElementsByTagName( "script" )[ 0 ];
ref.parentNode.insertBefore( ss, ref );

This injection is better than adding to the very top of the head, but there's a good chance it is before other styles.

In discussions with Flipkart, the performance disadvantage they encountered with this was significant. I would expect similar results amongst publishers with plenty of DOM, the folks that typically would be eager to use loadCSS.

Proposal

I'd recommend placing the injected sheet at the end of all recognized styles. (Bonus: subsequent loadCSS calls will be generally placed after eachother, which is probably what folks would expect)

So I'd go for something like this:

var elems = document.querySelectorAll('style,link[rel=stylesheet]');
var ref = elems.item(elems.length - 1);
ref.parentNode.insertBefore(ss, ref.nextSibling);

It finds all external stylesheets and inline styles, gets the last one (in DOM order), then adds our ss right after it.


hows that sound?

clarification on CSS advantages over placing css at bottom

Can the docs be updated to specify what's the advantage of this are over say...

<!DOCTYPE html>
<meta charset="utf-8"/>
<title></title>

<style type="text/css">
/* inlined critical css styles: 
     - window dressing
     - main components
   etc */
</style>

<body>

<!-- html content from server for initial render -->

<style type="text/css">
    @import '/the/rest/of/the.css';
</style>

<script async 
    src="your/javascript.js"
></script>

Relative paths do not work

loadCSS("../dist/css/style.css");

If I'm calling loadCSS this way, it does not work. The problem is IMHO on line 29.

loadCSS does not found relatively called stylesheet and media attribute is remaining on only x value.

On my project I can fix it easily, but maybe someone will experience the same problem.

[question] - setting cookie for cached css to block rendering

So I have a test branch with async-loaded css using loadCSS, and everything seems to be working as expected (i'm even loading fonts as @scottjehl suggests in this article). On first visit, only 'above the fold' css is rendered and the rest is loaded in async. Great. However, even when the css file is cached, there's a small blip of time from when the DOM is first rendered to when the cached css file is parsed and selectors are matched. So if, for example, I scroll to the bottom of the page and the refresh, the browser will keep me at the bottom of the page, and I'll see a blip of unstyled content.

Does that make sense? What do you do to combat this? One thing that comes to mind would be setting a cookie as Scott did in that article for the fonts and writing (blocking with .writeIn or something) a stylesheet element for those who presumably have the file cached. Another option would be to include the entire page's css inline (this sounds like an awful idea, but I'm actually only loading css async for the homepage, and assuming all other pages will have the css file cached after that, so it wouldn't be the worst thing).

Here's a screenshot of the network timeline, where the first entry is the document and the second is the cached css file, with a gap not only between when the first one ends and the second begins, but we obviously won't actually see the styled DOM until the second one ends.

screen shot 2015-05-20 at 7 16 14 pm

Thank you so much for any insight!

Any perf. savings by putting CSS link(s) at the bottom?

Depending on the size of one's CSS, this could exacerbate the FOUT, but I'd love to see some metrics on non-JS solutions, or at least solutions not initially requiring JS.

For example, how much perf cost could one save by:

  • inlining critical CSS at the top of the page
  • putting the CSS file link at the bottom of the page to avoid blocking
  • progressively enhancing to be async

@scottjehl's comments so far:

You could! JS can make the async request sooner tho. We also set a cookie once we request it (so we can assume it's in cache)

we typically have a link to the full css in a noscript, but the JS probably could reuse that reference, I guess

we tend to define our urls in meta tags now, but there's an overlap there in the fallback css link, if that's what you mean

loadCSS and PageSpeed Insights "blocking CSS"

Thanks for all the great tools, including this one. I know user experience is more important than PageSpeed Insights score, and this is not a PageSpeed troubleshooting forum, however I'd like to check that I'm not doing something wrong with loadCSS before blaming Pagespeed.

Here's the entirety of my code, which is located in the <head>:

<script>
  function loadCSS(a,b,c){
    "use strict";function g(){for(var b,e=0;e<f.length;e++)f[e].href&&f[e].href.indexOf(a)>-1&&(b=!0);b?d.media=c||"all":setTimeout(g)}var d=window.document.createElement("link"),e=b||window.document.getElementsByTagName("script")[0],f=window.document.styleSheets;return d.rel="stylesheet",d.href=a,d.media="only x",e.parentNode.insertBefore(d,e),g(),d
  }
  loadCSS("/public/fontface-1f24f01.css");
</script>
<script>
  loadCSS("/public/bootstrap-ebf66f6.css");
  loadCSS("/public/global-e4c2321.css");
  loadCSS("/public/condo-01c4f25.css");
</script>

I then also have critical CSS inlined in the <head>, and the user experience is great (i.e. works as expected and promised by @scottjehl!). However, PageSpeed Insights reports:

Your page has 4 blocking CSS resources. This causes a delay in rendering your page.

And then lists those four .css files from above.

I'm satisfied that user experience is vastly improved but disappointed if this is a "false positive" from PageSpeed. Any suggestions?

bower does not have the minified version (say loadcss.min.js )

Thanks for providing this excellent lib to load css async.

I pulled the latest version ( 0.1.7 , as of writing this ) from bower.

It contains the 2 .js files ( the unminified ones ).

I did this locally: ( using https://github.com/mishoo/UglifyJS2 )

uglifyjs --compress -- bower_components/loadcss/onloadCSS.js bower_components/loadcss/loadCSS.js > loadCSS.min.js

to start using the same.

It would be nice if this is present in bower repo as well, dist as well. Thanks, again.

make `before` argument optional by making default insertion point less dependent on queryselector

If we can append after the last stylesheet, even if it's far after it, that's probably a good fallback, and potentially a fine default for all browsers if we want to just do that. For example:

var scope = document.body || document.getElementsByTagName( "head" )[ 0 ];
var lastElement = scope.childNodes[ scope.childNodes.length - 1 ]; 

...gives us an insertion point that's as far down in the DOM as can be, and certain to be after the last script/style/link in the indexed dom at least.

Callback firing multiple times, specifically Firefox

Is this the intended behavior? I noticed that in chrome you get multiple network activity for the same stylesheet where only the last retrieves the stylesheet. I am guessing that this is intended and the "multiple tries" are how the asynchronicity is achieved.

I believe for this very same reason, firefox is calling the callback function multiple times.

Any insight?

Experiment with how to use loadCSS to polyfill link[rel=preload]

In the following demo page I've referenced a slow-loading CSS file with link[rel=preload] and polyfilled its request using loadCSS if a feature test for preload fails. _(Note: the slow-responding CSS file is not critical to this test, but it's useful for visually observing whether or not the CSS request blocks rendering across our test pages).

It works, but I'm unsure whether the syntax and logic lines up with how this feature is intended to work.

http://filamentgroup.github.io/loadCSS/test/preload.html

Inserted before the first script tag or after the last stylesheet or script in the DOM?

Your README.md https://github.com/filamentgroup/loadCSS/blob/master/README.md#optional-arguments says:

"By default, your stylesheet will be inserted before the first script tag in the DOM (which may be the one shown above)."

The file loadCSS.js https://github.com/filamentgroup/loadCSS/blob/master/loadCSS.js#L13 says:

"By default, loadCSS attempts to inject the link after the last stylesheet or script in the DOM."

So which one is it?

Network latency can cause css to block render in some cases.

I noticed this when testing over 3g. Sometimes, our timeout is fast enough to toggle the link's media attribute to all before its stylesheet request goes out, and it ends up blocking render. Not good.

One workaround I've come up with so far involves polling the document.styleSheets array until it increments, which I've found is a safe time to toggle the media attribute on any connection speed, as the request appears to be in flight at the point it joins that array (yet it's still long before onload, which is good for minimizing the times this logic would have to run). That change looks something like this:

/*!
loadCSS: load a CSS file asynchronously.
[c]2014 @scottjehl, Filament Group, Inc.
Licensed MIT
*/
function loadCSS( href, before, media ){
    "use strict";
    // Arguments explained:
    // `href` is the URL for your CSS file.
    // `before` optionally defines the element we'll use as a reference for injecting our <link>
    // By default, `before` uses the first <script> element in the page.
    // However, since the order in which stylesheets are referenced matters, you might need a more specific location in your document.
    // If so, pass a different reference element to the `before` argument and it'll insert before that instead
    // note: `insertBefore` is used instead of `appendChild`, for safety re: http://www.paulirish.com/2011/surefire-dom-element-insertion/
    var ss = window.document.createElement( "link" );
    var ref = before || window.document.getElementsByTagName( "script" )[ 0 ];
    var ssLength = window.document.styleSheets.length;
    ss.rel = "stylesheet";
    ss.href = href;
    // temporarily, set media to something non-matching to ensure it'll fetch without blocking render
    ss.media = "only x";
    // inject link
    ref.parentNode.insertBefore( ss, ref );
    // This function sets the link's media back to `all` so that the stylesheet applies once it loads
    // It is designed to loop until document.styleSheets includes
    function toggleMedia(){
        if( window.document.styleSheets.length > ssLength ){
            ss.media = media || "all";
        }
        else {
            setTimeout( toggleMedia );
        }
    }

    toggleMedia();
    return ss;
 }

This works generally, but a downside to it is it may not work well with multiple loadCSS calls, since they'll all toggle their media as soon as the first call goes out. I think we might be able to work around this by queueing the media toggles in the order they're called. Maybe a loadCSS.locked property would give us that sort of mechanism. The queue wouldn't incur much delay, since it's not about waiting for requests to return so much as ensuring the stylesheets have joined the CSSOM.

Since it's high-priority, we'll try and close this one out in the morning.

Doesn't work in IE9

I'm currently using this on a project, and i've noticed that on IE9 the site just freezes, it doesn't load the css. I've added ss.type = "text/css"; figured that could be the solution but it didn't work.

Question: noscript tag

Hey

Should there be a <noscript> tag included in the example for when the browser throws an error or js is simply disabled?

Just checkin'...

[question] Browser-support

Hey Scott,

Can you tell me what the current browser-support of this library is? I couldn't find any documentation on that.

Either way, some pretty good stuff in here!

thanks - Ruben

Load multiple CSS Files

Hey, fantastic piece of script here! Just used it and it worked well (even tested it in IE11). I am just curious on how I would go about loading multiple CSS files using this piece of script. Is there any documentation available to support this?

Double request in Chrome

Reproduced with 250KB locally hosted stylesheet, network connection throttled in Chrome to EDGE.

image

Some discussion for this took place in #41, but we this needed to be its own ticket.

Consider removing querySelector condition and use the fallback insertion point in all browsers

This would mainly mean that the default injection location would be after the last childnode element in either the body, or the head if the body isn't yet defined. We currently inject after the last style/script/link in qsa browsers, but this seems like it may be an unnecessary refinement compared to the fallback.

Lines in question: https://github.com/filamentgroup/loadCSS/blob/master/loadCSS.js#L23-L28

Google PageSpeed

Hello and thank you for your script! I implemented it because Google PageSpeed recommended me to remove/optimize blocking CSS files. But it seems that Google PageSpeed does not care about the asyncronous load. With your script implemented, PageSpeed still recognizes the CSS file as blocking.
Any idea how this can be solved?

loadCSS sending double calls for the CSS file

First calls goes normally on insertBefore. But when the function sets media attribute to all, it triggers another call for the CSS file.

Why could that be happening?

Browser: Chrome 44
OS: Max OSX 10.9.2

Delay link insertion (and thus, the request) until either <head> or entire DOM is ready

I think we should consider delaying the link append until a time when we can be sure that we're seeing all the scripts and styles in the DOM.

Currently, loadCSS runs before the DOM is ready, and depending on its location in the DOM, it'll often only see nodes that come before its parent script tag. That's often not a problem, particularly if you place the script tag that contains loadCSS after your other stylesheets. But if you don't, it'll often mean that the new link will be inserted before later stylesheets, possibly messing with cascade.

waiting for domcontentloaded or RAF is probably best. If not that, maybe waiting for document.body would at least be sufficient to get all the links in head

Question: LoadCSS with LocalStorage

Hello @scottjehl

Is there a good solution to go with loadCSS in combination with the power that comes with localStorage.

The way I want to go down is like this:

  • loadCSS will load a stylesheet asynchronously
  • then the script will check if localStorage is there
    • if it is there we put the stylesheet there and prevent the script from being loaded on revisit
      • stylesheet will be loaded from localStorage from now on
      • the script will check the localStorage for the CSS file
    • it is not there, nothing happens and the world keeps on revolving
      • back to step one on revisit/next page load

That would be much better than any cookie-based solution anyways but the main advantage I see is, that apart from loading CSS asynchronously, we could prevent the script from firing everytime a user is requesting a page in a website.

My questions:
What do you think?
How could one build that (I'm too bad at writing JS, otherwise I would have pasted a possible solution with my question)?
May be that has been thought of already and I just couldn't see it?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.