2

I can't find a reliable way to find when the page has finished loading. My current code looks like this:

function pageLoaded(callback) {
    function completed() {
        document.removeEventListener('DOMContentLoaded', completed);
        callback();
    }

    if (document.readyState === 'complete') {
        callback();
    } else {
        document.addEventListener('DOMContentLoaded', completed);
    }
}

The issue is that DOMContentLoaded gets fired before document.readyState gets set to 'complete' which means that the callback never gets called.

When my function runs DOMContentLoaded has already been fired but document.readyState === 'interactive'. However I don't think I can run my callback when document.readyState === 'interactive' due to these possible issues.

Unfortunately I don't think I can make a demo due to the use of async. Also this doesn't happen 100% of the time but it seems to happen always when I do a hard reload (I assume something to do with caching).

Note that I'm loading in my script like this in my <head>:

<script src="script.js" async></script>
joshhunt
  • 4,685
  • 2
  • 31
  • 56
  • … don’t use `async`? – Ry- Jul 09 '15 at 04:24
  • 2
    A fully built function to do this is here: http://stackoverflow.com/questions/9899372/pure-javascript-equivalent-to-jquerys-ready-how-to-call-a-function-when-the/9899701#9899701 – jfriend00 Jul 09 '15 at 04:26
  • @minitech but then it blocks rendering? – joshhunt Jul 09 '15 at 04:28
  • It seems to me that the `javascript loader` solves the problem. LABjs? – stdob-- Jul 09 '15 at 04:33
  • @jfriend00 Thanks I'll take a look and make sure it works with async. I had a look at jQuery's current code but I don't really understand it, it looks like they are using promises? https://github.com/jquery/jquery/blob/842958e7aecd0d75a7ee9e2aaec83457701aa2f3/src/core/ready.js – joshhunt Jul 09 '15 at 04:33
  • @joshhunt: Put it at the bottom of the page. (Actually, at that point, you can keep the `async` if your script doesn’t modify the page’s layout.) – Ry- Jul 09 '15 at 07:45
  • @minitech If there is a delay between the browser being sent the footer after the header is sent, then the browser won't find it and will only be able to start downloading it once it has received the whole page. – joshhunt Jul 09 '15 at 20:20
  • @joshhunt: Yes. Have you found that to be an issue in practice? – Ry- Jul 09 '15 at 21:15
  • @minitech Me personally or in general? If you are talking about in general then yes there are practical examples. If you are talking about me personally then I don't see how this is relevant to the question. – joshhunt Jul 09 '15 at 21:28
  • @jfriend00 while it does work, it is essentially just falling back to `window.addEventListener('load')` which isn't ideal because it has to wait for images / scripts to load. It might be the best way to fix it but I'm hoping it isn't. – joshhunt Jul 09 '15 at 21:59
  • @joshhunt - I guess I really don't understand what problem you're trying to solve. See [this answer](http://stackoverflow.com/questions/13346746/document-readystate-on-domcontentloaded) for some relevant explanation. – jfriend00 Jul 10 '15 at 00:57
  • @joshhunt: I’m recommending the simple solution instead of adding a bunch of complexity to start downloading a script (which will be made larger by the complete fix) a negligible amount of time earlier (especially since you’ll be able to start running things *earlier* than `DOMContentLoaded`). – Ry- Jul 10 '15 at 01:02
  • @jfriend00 just posted an answer, does it make sense? Any feedback would be appreciated. I think the biggest issue is that IE9/10 don't handle `document.ready` properly. – joshhunt Jul 10 '15 at 01:03
  • @minitech While your suggestions are appreciated (genuinely) I feel like there are real scenarios where this approach is optimal. For example your server can send the start of a html document whilst it is waiting on a third party service to generate the content of the document. – joshhunt Jul 10 '15 at 01:11

2 Answers2

5

I couldn't find much useful information on the subject so I decided to do some tests. I set up a Node server that would:

  1. Send the start of an html document
  2. Wait 5 seconds
  3. Send the rest of the html which includes an image

I then recorded the status of document.ready and DOMContentLoaded at each stage. My code:

var http = require('http');

var server = http.createServer(function(req, res) {
    // Send the first part of the html
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.write(
        '<!doctype html>' +
            '<html lang="en">' +
                '<head>' +
                    '<meta charset="utf-8">' +
                    '<meta http-equiv="x-ua-compatible" content="ie=edge">' +
                    '<title>JS Ready Test</title>' +
                    '<meta name="description" content="">' +
                    '<meta name="viewport" content="width=device-width, initial-scale=1">' +

                    '<script>' +
                        'console.log(document.readyState);' +

                        'document.onreadystatechange = function () {' +
                            'console.log(document.readyState);' +
                        '};' +

                        'document.addEventListener("DOMContentLoaded", function() {' +
                            'console.log("DOMContentLoaded");' +
                        '});' +
                    '</script>' +
                '</head>' +
                '<body>');
    // Send a bunch of blank spaces so that the browser will load the buffer, if the buffer is too small the browser will wait for more data
    var str = 'Start';
    for (var i = 0; i < 2000; i++){
      str += ' ';
    }
    res.write(str);

    // Wait 5 seconds and send the rest of the data
    setTimeout(function () {
        res.write('Finish<img src="https://upload.wikimedia.org/wikipedia/commons/3/3d/LARGE_elevation.jpg"></body></html>');
        res.end();
    }, 5000);
});

// Listen on port 3000
server.listen(3000);

Results from my tests

First Buffer
Chrome (v43) / FF (v39) / IE11: document.ready === 'loading'
IE9 / IE10: document.ready === 'interactive'

Final buffer
Chrome / FF / IE11: document.ready === 'interactive', DOMContentLoaded called
IE9 / IE10: No change in document.ready, DOMContentLoaded called

Sub-resources finish loading (in this case the image)
Chrome / FF / IE11: document.ready === 'complete'
IE9 / IE10: document.ready === 'complete'

As you can see IE9 & IE10 set document.ready === 'interactive' too early.

Some possible solutions

1. Ignore IE9 / IE10

if (document.readyState === 'interactive' || document.readyState === 'complete') {
    callback();
} else {
    document.addEventListener('DOMContentLoaded', callback);
}

2. Add the DOMContentLoaded in the <head> of your document outside of your async script. This ensures that it will be attached before it is called.

// In <head>
<script>
    var pageLoaded = false;

    document.addEventListener('DOMContentLoaded', function() {
        pageLoaded = true;
    });
</script>

// In script.js
if (pageLoaded) {
    callback();
} else  {
    document.addEventListener('DOMContentLoaded', callback);
}

3. Fallback to the load event on `window.

if (document.readyState === 'complete') {
    callback();
} else {
    // You would need to add a safety so that your functions don't get called twice
    document.addEventListener('DOMContentLoaded', callback);
    window.addEventListener( "load", callback);
}
joshhunt
  • 4,685
  • 2
  • 31
  • 56
0
function pageLoad(callback) {
    if ("function" == typeof callback) {
        if (document.addEventListener) { // Event that fires when the initial HTML document has been completely loaded and parsed
            document.addEventListener("DOMContentLoaded", callback, false);
        } else if (window.attachEvent) { // For IE 8 and below
            window.attachEvent("onload", callback);
        } else if ("function" == typeof window.onload) { // Event that fires when the page has fully loaded including images / scripts etc
            var o = window.onload;
            window.onload = function() {
                o();
                callback();
            };
        } else {
            window.onload = callback;
        }
    }
}
joshhunt
  • 4,685
  • 2
  • 31
  • 56
Haniel Bitton
  • 84
  • 1
  • 7
  • Unfortunately I don't think this solves the issue as the "DOMContentLoaded" event gets fired before the event listener can be attached. Thanks for trying though. – joshhunt Jul 09 '15 at 21:33