I was asked today if there was a way to somehow proxy or defer document.write calls. Many advertising solutions still use this outdated way of writing banners into a website, and sometimes it would be handy just to load all the page first, and then all the advertising, to drastically improve the user experience.

I gave it some thought, and after a bit of experimenting, I actually found a solution for this problem many people tried to solve before. Let’s walk through!

1. Overwriting document.write

Yes – this sounds dirty, but you can in fact just overwrite document.write and it works across all popular browsers. Here’s my first round:

var _write = document.write;
	document.write = function(t) {
	return _write(t);
}

This simple proxy pattern, directly forwarding the call to the original method, only worked in Internet Explorer though. To whatever reason it returns the following exception in Firefox: “Illegal operation on WrappedNative prototype object”. After a bit of more experimenting, I found that document.write in FF has a ‘call’ method attached that I could use to run the native write in the document scope. So here’s the slightly modified version using the excellent IE check recently posted on Ajaxian (we can’t use the call method in IE, it doesn’t exist on document.write):

var _write = document.write;
	document.write = function(t) {
	return 'v'=='v' ? _write(t) : _write.call(document, t);
}

2. Deferring document.write, or how to keep the context

Nice, we now have a cross-browser proxy function that delegates to the original function. But that doesn’t help much – after all, we want to delay all the writes until my page is fully loaded.

This one was an expecially tricky problem to solve, because we couldn’t just save the content to an Array or something, then echo it to the page at a later point. The reason is that we couldn’t save the context. document.write is a unique function, because it executes in the actual context, meaning on the actual line the call happens. This information, unfortunately, cannot be retrieved in any way. The obvious answer was that we have to write it to the document immediately to not loose the context information. However, we still don’t have to show it!

The final solution I came up with therefore enclosures the original write into a HTML comment block and then writes it out immediately. Of course, when the time comes, we have to resolve that comment block again through a regular expression. Here’s how our modified function looks:

var _write = document.write;
document.write = function(t) {
	t = '<!--##DEFER'+t+'DEFER##-->';
	return 'v'=='v' ? _write(t) : _write.call(document, t);
}

3. The final implementation

(function() {
	var _write = document.write;
	document.write = function(t) {
		t = '<!--##DEFER'+t+'DEFER##-->';
		return 'v'=='v' ? _write(t) : _write.call(document, t);
	}
})();

function resolve() {
	document.body.innerHTML = document.body.innerHTML.replace(/<!--##DEFER(.+)DEFER##-->/g, '$1');
}

I’ve additionally added a closure around the document.write proxy to save a global variable (_write is private this way), and also attached a function (I know I know, it’s lazy) called ‘resolve’ (rename it to whatever you want), that at a later point grabs the innerHTML of the body and resolves all the created comment blocks into their original content, at the right line.

Update: The innerHTML way of simply replacing the body is really lazy and should only be used for testing purposes. In a realistic setup, you mostly know where the write’s happen, and it will be much better to loop through them (i.e. using childNodes) in the DOM and filtering out comment nodes this way.

Enjoy!