www.webdeveloper.com
Results 1 to 11 of 11

Thread: solution: lazy loading JS ad code containing document.write()

  1. #1
    Join Date
    Apr 2007
    Posts
    7

    solution: lazy loading JS ad code containing document.write()

    Hi everybody!

    After I have received lots of helpful information here, Id like to give back today...

    Some of you may have experienced problems using JS ad code (e.g. google adsense etc). This can include complete page freezing due to ad server lags... the solution would be to lazy load the code. Unfortunately ad code often includes document.write() statements, which makes it impossible to run it if the page has already rendered. Also most adserver policies forbid changes in the delivered code, so you have to stick with document.write() ...

    Ok whats the solution here: simply overload document.write for a short period of time, and replace it with a function that buffers the output, and later writes the buffer to the innerHTML of a div. Of course you have to restore the old document.write function afterwards...

    Here is the code:

    Code:
    var domWrite = (function(){               // by Frank Thuerigen
     // private 
     // (probably helper functions for code sanitizing etc.)
     
     return function( pDiv, pSrc ){           // public
     
      var e = ( typeof pDiv == 'string' ? 
                document.getElementById( pDiv ) :
                pDiv ),
          s = document.createElement('script'),
          dw = document.write,              // save document.write()
          buf = '',                         // output string buffer
          oldbuf = '',                      // output string buffer
          t = '',                           // timeout
          ms = 100;                         // milliseconds
          
      function evals( pString ){            // eval embedded script tags in HTML code
       var scripts = [],
           script,
           regexp = /<script[^>]*>([\s\S]*?)<\/script>/gi;
       while ((script = regexp.exec(pString))) scripts.push(script[1]);
       scripts = scripts.join('\n');
       if (scripts) eval(scripts);
       }
       
      function exec(){                      // output & restore document.write()
       if ( buf !== oldbuf ){
        oldbuf = buf;
        t=window.setTimeout( exec, ms );    // repeat test timeout
        }
       else {
        e.innerHTML = buf;                  // write output to element
        evals( buf );                       // tbd: correct eval sequence if multiple scripts
        document.write=dw;                  // restore document.write()
        }
       }
       
      document.write = function( pString ){ // overload document.write()
       window.clearTimeout(t);
       oldbuf = buf;
       buf += pString;                        // add string to buffer
       t=window.setTimeout( exec, ms );
       }
    
      s.setAttribute('language','javascript');
      s.setAttribute('type','text/javascript');
      s.setAttribute('src', pSrc);
      document.getElementsByTagName('head')[0].appendChild(s);
      }
     })();
    Here is a sample of how to use it with google adsense:
    Code:
    <script type="text/javascript"><!--
    google_ad_client = "pub-568508999XXXXX";
    /* header block */
    google_ad_slot = "05460XXXXX";
    google_ad_width = 468;
    google_ad_height = 15;
    //-->
    </script>
    <script type="text/javascript">
    domWrite( 'myDivId', 'http://pagead2.googlesyndication.com/pagead/show_ads.js' );
    </script>
    ( "myDivId" of course is the id of the DIV you want the ad to appear in )

    ...and finally here you can see it at work:
    http://blog.phpbuero.de/?p=26
    The ads below the right sidebar are loaded using this technique.

    CAVEAT: I dont know wether google approves this, but since it doesnt change their code or functionality I dont think it is a problem. Anyway, use it on your own risk!

    I hope this helps ...

    Have fun,
    Frankie

  2. #2
    Join Date
    Apr 2007
    Posts
    7
    This improved version can handle several simultaneous calls without interfering:

    Code:
    var domWrite = (function(){            // by Frank Thuerigen
     // private
     
     var c=0;                              // counter for running domWrite() calls
     
     function evals( pString ){            // eval embedded script tags in HTML code
      var scripts = [],
          script,
          regexp = /<script[^>]*>([\s\S]*?)<\/script>/gi;
      while ((script = regexp.exec(pString))) scripts.push(script[1]);
      scripts = scripts.join('\n');
      if (scripts) eval(scripts);
      }
     
     return function( pDiv, pSrc ){         // public
     
      var e = ( typeof pDiv == 'string' ? 
                document.getElementById( pDiv ) :
                pDiv ),                     // the div element
          s = document.createElement('script'),
          dw = document.write,              // save document.write()
          buf = '',                         // output string buffer
          oldbuf = '',                      // output string buffer
          t = '',                           // timeout
          ms = 100;                         // milliseconds
             
      function exec(){                      // output & restore document.write()
       if ( buf !== oldbuf ){
        oldbuf = buf;
        t=window.setTimeout( exec, ms );    // repeat test timeout
        }
       else {
        e.innerHTML = buf;                  // write output to element
        evals( buf );                       // tbd: correct eval sequence if multiple scripts
        if ( (c--) === 0 ) {                // no more domWrite()s running 
         document.write=dw;                 // restore document.write()    
         }
        }
       }
       
      document.write = function( pString ){ // overload document.write()
       window.clearTimeout(t);
       oldbuf = buf;
       buf += pString;                        // add string to buffer
       t=window.setTimeout( exec, ms );
       }
      c++;                                      // inc domWrite() counter
    
      s.setAttribute('language','javascript');
      s.setAttribute('type','text/javascript');
      s.setAttribute('src', pSrc);
      document.getElementsByTagName('head')[0].appendChild(s);
      }
     })();

  3. #3
    Join Date
    Apr 2007
    Posts
    7
    The next version ( and the last for now ). Improvement is that it now really works for simultaneous downloads... which the previous one did not, at least not crossbrowser.
    This Version has been tested on IE 5.5-6-7 FF 1.x-2.x-3.x SAFARI 2.x-3.x and several OPERA Versions.

    Code:
    var domWrite = (function(){            // by Frank Thuerigen
     // private 
    
     var dw = document.write,              // save document.write()
              myCalls = [],                // contains all outstanding Scripts
              t = '';                      // timeout
     
     function startnext(){                 // start next call in pipeline
      if ( myCalls.length > 0 ) {
       if ( Object.watch ) console.log( 'next is '+myCalls[0].f.toString() );
       myCalls[0].startCall();
       }
      }
    
     function evals( pCall ){            // eval embedded script tags in HTML code
      var scripts = [],
          script,
          regexp = /<script[^>]*>([\s\S]*?)<\/script>/gi;
      while ((script = regexp.exec(pCall.buf))) scripts.push(script[1]);
      scripts = scripts.join('\n');
      if (scripts) {
       eval(scripts);
       }
      }
    
     function finishCall( pCall ){
       pCall.e.innerHTML = pCall.buf;             // write output to element
       evals( pCall );
       document.write=dw;                        // restore document.write()
       myCalls.shift();
       window.setTimeout( startnext, 50 );
       }
    
     function testDone( pCall ){
       var myCall = pCall;
       return function(){
        if ( myCall.buf !== myCall.oldbuf ){
         myCall.oldbuf = myCall.buf;
         t=window.setTimeout( testDone( myCall ), myCall.ms );
         }
        else {
         finishCall( myCall );
         }
        }
       }  
       
     function MyCall( pDiv, pSrc, pFunc ){                    // Class
      this.e = ( typeof pDiv == 'string' ? 
                 document.getElementById( pDiv ) :
                 pDiv ),                     // the div element
      this.f = pFunc || function(){},
      this.stat = 0,                         // 0=idle, 1=waiting, 2=running, 3=finished
      this.src = pSrc,                       // script source address
      this.buf = '',                         // output string buffer
      this.oldbuf = '',                      // compare buffer
      this.ms = 100,                         // milliseconds
      this.scripttag;                        // the script tag 
      }
     
     MyCall.prototype={
      startCall: function(){
       this.f.apply( window );                 // execute settings function
       this.stat=1;
       var that = this;                            // status = waiting
       document.write = (function(){
        var o=that,
            cb=testDone( o ),
            t;
        return function( pString ){            // overload document.write()
         window.clearTimeout( t );
         o.stat=2;                             // status = running
         window.clearTimeout(t);
         o.oldbuf = o.buf;
         o.buf += pString;                     // add string to buffer
         t=window.setTimeout( cb, o.ms );
         };
        })();
       var s=document.createElement('script');
       s.setAttribute('language','javascript');
       s.setAttribute('type','text/javascript');
       s.setAttribute('src', this.src);
       document.getElementsByTagName('head')[0].appendChild(s);
       }
      }
      
     return function( pDiv, pSrc, pFunc ){  // public
      var c = new MyCall( pDiv, pSrc, pFunc );
      myCalls.push( c );
      if ( myCalls.length === 1 ){
       startnext();
       }
      }
     })();
    You can download the code directly as a file here.


    Implementation in HTML has changed a bit (but became easier I guess):

    Code:
        <div id="googlesidebar1"></div>
    
        <script type="text/javascript">
         domWrite(
          "googlesidebar1",
          "http://pagead2.googlesyndication.com/pagead/show_ads.js",
          function(){
           google_ad_client = "pub-YourOwnGoogleCode";
           google_ad_slot = "YourOwnGoogleCode";
           google_ad_width = 160;
           google_ad_height = 600;
           }
          );
        </script>
    In the blog you can now see 3(!) simultaneous lazy-loaded adsense blocks: 2xright sidebar, 1xfooter ...

    Have fun,
    Frankie
    Last edited by FrankT; 11-10-2008 at 06:07 PM.

  4. #4
    Join Date
    Apr 2007
    Posts
    7
    Still a work in progress ;-)

    Im testing with other ad code on my site, so if you have a problem with the downloaded JS file please give me some time Im problably working on it... just check back after some time or contact me...

    Frankie

  5. #5
    Join Date
    Oct 2009
    Posts
    3
    This will work for certain ads but a few improvements are needed. Believe it or not, the following is valid HTML. The alert dialog will show 1 and div will be a reference to the written out DIV element.
    HTML Code:
    document.write("<scr" + "ipt>var foo=1;<\/script>"); 
    alert(foo); 
    document.write("<div id=foo><\/div>"); 
    var div= document.getElementById("foo"); 
    Moreover, using innerHTML to set the buffer will cause Firefox to re-execute the JavaScript, resulting in un-handled document.write() calls. Also, if the HTML contains style tags they will need to be extracted from the HTML and added to the HEAD.

    A working example of pre-load emulation of document.write in a post-load context can be seen at http://www.app.com/

    I am still struggling with a couple of things: 1. The write()'ing script using various document properties to access written element and 2. Handling the execution of event handlers for the window onload event (or DOMContentReady) as this may have all ready fired.
    HTML Code:
    document.write("<img src=\"foo\"/>"); 
    var _i= document.images[document.images.length - 1]

  6. #6
    Join Date
    Jul 2008
    Location
    urbana, il
    Posts
    2,787

    there's a simple way

    just add the defer attrib, set to true to any script tag.

    with defer, the page can continue to load while the script downloads and writes, and everything magically falls into it's right place.

    let me be clear, a defer'ed tag can doc.write after the page loads, without wiping out the whole page....

    works in 99% of browsers.
    the folks with old 3rd-party browsers will just have to wait for the tag to load...

  7. #7
    Join Date
    Oct 2009
    Posts
    3
    Quote Originally Posted by rnd me View Post
    just add the defer attrib, set to true to any script tag.
    let me be clear, a defer'ed tag can doc.write after the page loads, without wiping out the whole page....
    But where does the output go? How can it be moved into the "correct" position on the page?

  8. #8
    Join Date
    Jul 2008
    Location
    urbana, il
    Posts
    2,787
    Quote Originally Posted by serverherder View Post
    But where does the output go? How can it be moved into the "correct" position on the page?
    it goes adjacent to the script tags, just like it would without the defer attrib...

  9. #9
    Join Date
    Oct 2009
    Posts
    3
    Quote Originally Posted by rnd me View Post
    it goes adjacent to the script tags, just like it would without the defer attrib...
    This is not the behavior I see.

    In Firefox, the output of document.write() for defer'd scripts is appended to the body.

    In IE the effect is the same as document.write()'ing to a closed document.

    HTML Code:
    <html> <head><title> Test</title> </head> 
    <body> 
    <h3> Hello, world</h3> 
    <script type="text/javascript" defer="true"> 
    document.write("<h2> Hello, script <\/h2> "); 
    </script> 
    <h3> Hello again, world</h3> 
    </body> 
    </html> 
    In FireFox 3.5 I get this HTML:
    HTML Code:
    <h3>Hello, world</h3> 
    <script ... > </script> 
    <h3> Hello again, world</h3> 
    <h2>Hello, script </h2> <!-- Wrong order --> 

    IE simply outputs
    HTML Code:
    <h2>Hello, script </h2> 

    IMHO, this is a shame and the functionality provided by write() serves a legitimate need: By allowing the script author to define her HTML structure without having to know the parent container (albeit using strings instead of DOM methods) and the page author to dictate where the output should be placed, it decouples the page and the script. Requiring the JS developer to either know ahead of time where to place any required HTML or the page author to "take action" after a script has run (to embed the content where it really needs to go) makes including "widgets" very difficult.

    For now, document.write() is the only way to achieve this decoupling. Using the defer attribute and placing the output adjacent to the script seems to make sense to me and should be supported, but the devil is in the details: What happens when you document.write("<!-- ")?

  10. #10
    Join Date
    Jul 2008
    Location
    urbana, il
    Posts
    2,787
    Quote Originally Posted by serverherder View Post
    This is not the behavior I see.

    IMHO, this is a shame and the functionality provided by write() serves a legitimate need: By allowing the script author to define her HTML structure without having to know the parent container (albeit using strings instead of DOM methods) and the page author to dictate where the output should be placed, it decouples the page and the script. Requiring the JS developer to either know ahead of time where to place any required HTML or the page author to "take action" after a script has run (to embed the content where it really needs to go) makes including "widgets" very difficult.

    For now, document.write() is the only way to achieve this decoupling.
    well that's what i get for reading specs, thanks for the tests.


    regarding dynamic content placement, you can use document.appendChild or innerHTML to insert new content anywhere in the document. If you are replacing a document.write type of functionality, i don't see the issue with appending just before the script tag. before, in, or after, as long as it touches the script tag, DOM-added content should appear the same as written content.

    I don't see how document.write provides anything that can't be done with DOM methods, other than possibly pausing execution by writing new script tags.
    If defer doesn't work, then i would highly recommend avoiding document.write, as the user must wait on the remote transfer. With dom methods, you can defer the script and add the content after page load, when the remote file connects and downloads.
    Last edited by rnd me; 10-13-2009 at 10:09 PM.

  11. #11
    Join Date
    Jul 2008
    Location
    urbana, il
    Posts
    2,787
    Quote Originally Posted by serverherder View Post

    IMHO, this is a shame and the functionality provided by write() serves a legitimate need: By allowing the script author to define her HTML structure without having to know the parent container (albeit using strings instead of DOM methods) and the page author to dictate where the output should be placed, it decouples the page and the script. Requiring the JS developer to either know ahead of time where to place any required HTML or the page author to "take action" after a script has run (to embed the content where it really needs to go) makes including "widgets" very difficult.
    forgot to mention this the other day;

    you can use dom methods to find script tags, thus giving you a DOM slot where the original tag was located, just like a document.write script would "know".

    this pattern works for inline, non-deferred scripts, but not post-onload, dynamically added ones.
    but hey; that's the same limit as document.write right?
    Magiacally, since scripts load one-at-a-time, top-to-bottom, the last script tag is always the "active" one.

    Code:
    (function(){
      var t= document.getElementsByTagName("script");
      t= t[ t.length -1];
      var tx =  t.text || t.textContent || t.innerText;
    	 if(tx && t.src){ try{ eval(tx) }catch(y){}; }
    }());
    what this code does is evaluate the text inside a remote script tag, which is normally ignored. You can just change this line:
    Code:
     if(tx && t.src){ try{ eval(tx) }catch(y){}; }
    to do what you need, t is the currently active script tag.

    If you want to do this kind of thing after the page loads, you will have to iterate all script tags, and match the filename to the external script. This mean that a script must know it's filename. If you are hosting widgets for anyone to include, hard-coding in a filename is not a problem.

    just a few lines at the end of your script, and you should be able to hit those script tags anytime, anywhere.

Thread Information

Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  
HTML5 Development Center



Recent Articles