skip to Main Content

I’m prerendering my HTML pages for the search engines bots via PhantomJS through Selenium, so that they can see the fully loaded content. Currently, after PhantomJS reached the page, I’m waiting 5 seconds so that I’m sure everything is loaded.

Instead of waiting those 5 seconds every time, one solution I contemplate is to wait until an attribute html-ready on the <body /> tag is set to true:

<html ng-app>
    <head>...</head>
    <body html-ready="{{htmlReady}}">
        ...
    </body>
</html>
.controller("AnyController", function($scope, $rootScope, AnyService) {
    $rootScope.htmlReady = false;
    AnyService.anyLongAction(function(anyData) {
        $scope.anyData = anyData;
        $rootScope.htmlReady = true;
    });
})

The question is: will the html-ready attribute always be set to true after any view update has been done (e.g. displaying the anyData)? In other words, is it possible that during a laps, the html-ready attribute is true while the page is not fully loaded yet? If yes, how can it be handled?

2

Answers


  1. Short answer

    No. It isn’t guaranteed that your markup will be completely rendered when html-ready is set.

    Long answer

    To the best of my knowledge it’s not possible to accurately determine when Angular has finished updating the DOM after the model changed. In general it happens very fast and it doesn’t take more than a few cycles to finish, but that’s not always the case.

    Correctly detecting when a page has finished loading/rendering is actually quite a challenge, and if you take a look at the source code of specialized tools, like prerender, you’ll see that they use several different checks in order to try to decide whether a page is ready or not. And even so it doesn’t work 100% of the time (Phantom may crash, a request may take longer than usual to complete, and so on).

    If you really want to come up with your own solution for this problem, I suggest that you take a look at prerender’s source code (or another similar project) to get some inspiration.

    Login or Signup to reply.
  2. It should be done after the digest, thus it has more chances to work as expected.

    AnyService.anyLongAction(function(anyData) {
        $scope.anyData = anyData;
        $timeout(function () {
            $rootScope.htmlReady = true;
        }, 0, false);
    });
    

    But it is useless in terms of the app. You have to watch for changes in every single place, Angular doesn’t offer anything to make the task easier.

    Fortunately, you are free to abstract from Angular and keep it simple.

    var ignoredElements = [];
    ignoredElements = ignoredElements.concat($('.continuously-updating-widget').toArray());
    
    var delay = 200; // add to taste
    var timeout;
    
    var ready = function () {
      $('body').off('DOMSubtreeModified');
      clearTimeout(timeoutLimit);
      alert('ready');
    };
    
    $('body').on('DOMSubtreeModified', function (e) {
      if (ignoredElements.indexOf(e.target) < 0) {
        clearTimeout(timeout);
        timeout = setTimeout(ready, delay);
      }
    });
    
    var timeoutLimit = setTimeout(ready, 5000);
    

    Feel free to angularify it if needed, though it isn’t the production code anyway.

    It is a good idea to put the handler into throttle wrapper function (the event will spam all the way). If you use remote requests on the page that can potentially exceed timeout delay, it may be better to combine this approach with several promises from async services and resolve them with $q.all. Still, much better than looking after every single directive and service.

    DOMSubtreeModified is considered to be obsolete (it never was really acknowledged, MutationObserver is recommended instead), but current versions of FF and Chrome support it, and it should be ok for Selenium.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search