/    Sign up×
Community /Pin to ProfileBookmark

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

Hi everybody!

After I have received lots of helpful information here, I´d 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[^>]*>([sS]*?)</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);
}
})();
[/CODE]

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”>
[B]domWrite( ‘myDivId’, ‘http://pagead2.googlesyndication.com/pagead/show_ads.js’ );[/B]
</script>
[/CODE]

( “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:
[URL=”http://blog.phpbuero.de/?p=26″]http://blog.phpbuero.de/?p=26[/URL]
The ads below the right sidebar are loaded using this technique.

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

I hope this helps …

Have fun,
Frankie

to post a comment
JavaScript

10 Comments(s)

Copy linkTweet thisAlerts:
@FrankTauthorNov 09.2008 — 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[^>]*>([sS]*?)</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);
}
})();
[/CODE]
Copy linkTweet thisAlerts:
@FrankTauthorNov 11.2008 — 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[^>]*>([sS]*?)</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();
}
}
})();[/CODE]

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>
[/CODE]


In the blog you can now see 3(!) simultaneous lazy-loaded adsense blocks: 2xright sidebar, 1xfooter ...

Have fun,

Frankie
Copy linkTweet thisAlerts:
@FrankTauthorNov 13.2008 — Still a work in progress ;-)

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

Frankie
Copy linkTweet thisAlerts:
@serverherderOct 13.2009 — 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.
[code=html]
document.write("<scr" + "ipt>var foo=1;</script>");
alert(foo);
document.write("<div id=foo></div>");
var div= document.getElementById("foo");
[/code]


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.
[code=html]
document.write("<img src="foo"/>");
var _i= document.images[document.images.length - 1]
[/code]
Copy linkTweet thisAlerts:
@rnd_meOct 13.2009 — 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...
Copy linkTweet thisAlerts:
@serverherderOct 13.2009 — 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....
[/QUOTE]

But where does the output go? How can it be moved into the "correct" position on the page?
Copy linkTweet thisAlerts:
@rnd_meOct 13.2009 — But where does the output go? How can it be moved into the "correct" position on the page?[/QUOTE]

it goes adjacent to the script tags, just like it would without the defer attrib...
Copy linkTweet thisAlerts:
@serverherderOct 13.2009 — it goes adjacent to the script tags, just like it would without the defer attrib...[/QUOTE]
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.

[code=html]
<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>

[/code]


In FireFox 3.5 I get this HTML:
[code=html]
<h3>Hello, world</h3>
<script ... > </script>
<h3> Hello again, world</h3>
<h2>Hello, script </h2> <!-- Wrong order -->
[/code]



IE simply outputs
[code=html]
<h2>Hello, script </h2>
[/code]



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("<!-- ")?
Copy linkTweet thisAlerts:
@rnd_meOct 14.2009 — 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.[/QUOTE]


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.
Copy linkTweet thisAlerts:
@rnd_meOct 15.2009 — 

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. [/QUOTE]


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){}; }
}());
[/CODE]


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){}; }[/CODE]
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.
×

Success!

Help @FrankT spread the word by sharing this article on Twitter...

Tweet This
Sign in
Forgot password?
Sign in with TwitchSign in with GithubCreate Account
about: ({
version: 0.1.9 BETA 5.23,
whats_new: community page,
up_next: more Davinci•003 tasks,
coming_soon: events calendar,
social: @webDeveloperHQ
});

legal: ({
terms: of use,
privacy: policy
});
changelog: (
version: 0.1.9,
notes: added community page

version: 0.1.8,
notes: added Davinci•003

version: 0.1.7,
notes: upvote answers to bounties

version: 0.1.6,
notes: article editor refresh
)...
recent_tips: (
tipper: @AriseFacilitySolutions09,
tipped: article
amount: 1000 SATS,

tipper: @Yussuf4331,
tipped: article
amount: 1000 SATS,

tipper: @darkwebsites540,
tipped: article
amount: 10 SATS,
)...