2

I want to lazy load a css file, and then have javascript modify a page after said css loaded. To that end, I'm using this approach, as illustrated here:

lazyLoadCSS = function (pathToStyleSheet, options, callback) {
    var stylesheet = document.createElement('link');
    stylesheet.href = pathToStyleSheet;
    stylesheet.rel = 'stylesheet';
    stylesheet.type = 'text/css';
    // temporarily set media to something inapplicable to ensure 
    // it'll fetch without blocking render
    stylesheet.media = 'only x';
    // set the media back when the stylesheet loads
    stylesheet.onload = function() {
        stylesheet.media = 'all';

        // here be callbacks...
        callback();
    };
    document.getElementsByTagName('head')[0].appendChild(stylesheet);
};

So, the idea is, javascript fetches the CSS, but smuggles it in as 'only x' so the browser doesn't think it needs to wait for the file before rendering the rest of the page, which is the whole point. This approach seems to be pretty standard and the above code does work...mostly.

The only problem is the callback. It happens after the CSS is loaded, but before the styles have been applied to the document.

If I put a timer on the callback, like so:

window.setTimeout(function(){
    callback();
}, 2000);

then everything works exactly the way it should (only slow).

Question is, how can I wait not only for the CSS to load, but also to be applied before I run the callback()?

crowhill
  • 2,398
  • 3
  • 26
  • 55
  • A common way to run code after render is to call `setTimeout` without a delay value: `setTimeout(() => callback())`. Try it. If it works for your scenario, it should be sufficient. – ideaboxer Sep 29 '17 at 21:49
  • I was leaning towards something like this, but it seemed kinda...simpleminded? Too obvious? Good to have someone agree that it would work. – crowhill Sep 29 '17 at 21:52
  • You can think of it as `onIdle`. Even AngularJS adopted it (they call it `$timeout`) and it is used to simulate an idle callback. For instance here: https://stackoverflow.com/questions/20610450/angularjs-timeout-without-delay-parameters-reason – ideaboxer Sep 29 '17 at 21:59
  • You can distinguish 2 situations: If you can call `setTimout` without a delay to solve your problem, you are most likely doing the right thing. If you need to call `setTimeout(..., 1000)` because you need to counteract a race condition by waiting at least, say, half a second, then your code smells. Because you can never be sure a second is long enough, if `setTimeout(...)` (without delay) is not. – ideaboxer Sep 29 '17 at 22:13
  • Posted a possible answer. What do you think? Timeout delay is actually 0, which is...uhh...is that even a timer if the delay is 0? – crowhill Sep 29 '17 at 22:27

1 Answers1

0

As per some comments above, this is what I've come up with and it work great (though it's not thoroughly tested):

function waitForStylesToApply(cycleNumber, canary, callback){
    if(cycleNumber < 100){
        window.setTimeout(function(){
            var computedValue = window
                .getComputedStyle(window.document.body, null)
                .getPropertyValue('max-width');
            if(computedValue !== canary){
                if(typeof callback === 'function'){
                    console.log('done in ' + cycleNumber + ' cycles');
                    callback();                                            
                }
            }
            else {
                waitForStylesToApply(cycleNumber++, canary, callback);                        
            }
        }, 10);    
    }
}

Checks on the computed style for <body> over and over until it changes. Then it does the callback. So far, the code has never had to cycle through more than once, so the wait is infinitesimal.

Even a delay of 10 is too much. Code will work with a delay of 0 and still only cycle once.

EDIT: use this function in place of simply using callback() in the example above.

crowhill
  • 2,398
  • 3
  • 26
  • 55