If you are loading boomerang.js separately from your main application bundle, i.e. from a CDN or a third-party service (such as mPulse), we recommend loading boomerang.js using the CSP-compliant non-blocking script loader pattern.
The methodology, developed by Philip Tellis
and others, and further improved in 2018,
ensures Boomerang (or any third-party JavaScript) loads asynchronously
and non-blocking. This means that the browser will not pause while the JavaScript
is loading, nor will it block the onload
event.
The Boomerang Loader Snippet is currently around 210 lines of code (with comments), and minifies to around 2500 bytes.
The snippet does the following:
- It checks whether or not the snippet has already been run or Boomerang has already been loaded. If so, it exits.
- It adds a
window
load
event handler, to ensure that Boomerang can measure the Page Load time in non-NavigationTiming browsers, even if boomerang.js loads after theload
event. - For browsers that support Preload
<link rel="preload" as="script">
, Boomerang will add a<link>
node to tell the browser to fetch Boomerang.js.- Once the Preload has finished, Boomerang adds a regular
<script>
node to the page with the same Boomerang URL, which tells the browser to execute Boomerang.
- Once the Preload has finished, Boomerang adds a regular
- For browsers that do not support Preload, or if Preload fails or doesn't trigger within the defined timeframe (default 3 seconds),
the non-blocking IFRAME loader method is used.
- A hidden
<iframe>
is injected into the page. - The snippet attempts to read the IFRAME's
contentWindow.document
. If it can't, it updates the IFRAME'ssrc
to add JavaScript that sets the IFRAME'sdocument.domain
to the current page'sdocument.domain
. This ensures the anonymous IFRAME can communicate with the host page. - It writes a function
_l()
to the IFRAME'sdocument
which will add a<script>
tag that loads boomerang.js. - It sets the IFRAME's
<body onload="document._l()">
to run the function above, so the<script>
tag is loaded after the IFRAME'sonload
event has fired.
- A hidden
- For IE 6 and IE 7, which don't support the non-blocking IFRAME loader method (due to problems they have with
about:blank
URLs in secure contexts), a dynamic<script>
node is added to the page.- Note this means that in IE 6 and 7, Boomerang could be a SPOF (Single Point of Failure) if the script is delayed, potentially delaying the Page Load.
Note: We split the <body
tag insertion into <bo
and dy
to avoid server-side output filters that may replace <body
tags with their own code.
For proof that the non-blocking script loader pattern does not affect page load, you can look at this test case that delays JavaScript from loading by 5 seconds or these WebPagetest results.
The Snippet
Here's the snippet:
<script>
(function() {
// Boomerang Loader Snippet version 14
if (window.BOOMR && (window.BOOMR.version || window.BOOMR.snippetExecuted)) {
return;
}
window.BOOMR = window.BOOMR || {};
window.BOOMR.snippetStart = new Date().getTime();
window.BOOMR.snippetExecuted = true;
window.BOOMR.snippetVersion = 14;
// NOTE: Set Boomerang URL here
window.BOOMR.url = "";
var // document.currentScript is supported in all browsers other than IE
where = document.currentScript || document.getElementsByTagName("script")[0],
// Parent element of the script we inject
parentNode = where.parentNode,
// Whether or not Preload method has worked
promoted = false,
// How long to wait for Preload to work before falling back to iframe method
LOADER_TIMEOUT = 3000;
// Tells the browser to execute the Preloaded script by adding it to the DOM
function promote() {
if (promoted) {
return;
}
var script = document.createElement("script");
script.id = "boomr-scr-as";
script.src = window.BOOMR.url;
// Not really needed since dynamic scripts are async by default and the script is already in cache at this point,
// but some naive parsers will see a missing async attribute and think we're not async
script.async = true;
parentNode.appendChild(script);
promoted = true;
}
// Non-blocking iframe loader (fallback for non-Preload scenarios) for all recent browsers.
// For IE 6/7, falls back to dynamic script node.
function iframeLoader(wasFallback) {
promoted = true;
var dom, doc = document, bootstrap, iframe, iframeStyle, win = window;
window.BOOMR.snippetMethod = wasFallback ? "if" : "i";
// Adds Boomerang within the iframe
bootstrap = function(parent, scriptId) {
var script = doc.createElement("script");
script.id = scriptId || "boomr-if-as";
script.src = window.BOOMR.url;
BOOMR_lstart = new Date().getTime();
parent = parent || doc.body;
parent.appendChild(script);
};
// For IE 6/7, we'll just load the script in the current frame, as those browsers don't support 'about:blank'
// for an iframe src (it triggers warnings on secure sites). This means loading on IE 6/7 may cause SPoF.
if (!window.addEventListener && window.attachEvent && navigator.userAgent.match(/MSIE [67]\./)) {
window.BOOMR.snippetMethod = "s";
bootstrap(parentNode, "boomr-async");
return;
}
// The rest of this function is IE8+ and other browsers that don't support Preload hints but will work with CSP & iframes
iframe = document.createElement("IFRAME");
// An empty frame
iframe.src = "about:blank";
// We set title and role appropriately to play nicely with screen readers and other assistive technologies
iframe.title = "";
iframe.role = "presentation";
// Ensure we're not loaded lazily
iframe.loading = "eager";
// Hide the iframe
iframeStyle = (iframe.frameElement || iframe).style;
iframeStyle.width = 0;
iframeStyle.height = 0;
iframeStyle.border = 0;
iframeStyle.display = "none";
// Append to the end of the current block
parentNode.appendChild(iframe);
// Try to get the iframe's document object
try {
win = iframe.contentWindow;
doc = win.document.open();
}
catch (e) {
// document.domain has been changed and we're on an old version of IE, so we got an access denied.
// Note: the only browsers that have this problem also do not have CSP support.
// Get document.domain of the parent window
dom = document.domain;
// Set the src of the iframe to a JavaScript URL that will immediately set its document.domain to match the parent.
// This lets us access the iframe document long enough to inject our script.
// Our script may need to do more domain massaging later.
iframe.src = "javascript:var d=document.open();d.domain='" + dom + "';void 0;";
win = iframe.contentWindow;
doc = win.document.open();
}
if (dom) {
// Unsafe version for IE8 compatibility. If document.domain has changed, we can't use win, but we can use doc.
doc._boomrl = function() {
this.domain = dom;
bootstrap();
};
// Run our function at load.
// Split the string so HTML code injectors don't get confused and add code here.
doc.write("<bo" + "dy onload='document._boomrl();'>");
}
else {
// document.domain hasn't changed, regular method should be OK
win._boomrl = function() {
bootstrap();
};
if (win.addEventListener) {
win.addEventListener("load", win._boomrl, false);
}
else if (win.attachEvent) {
win.attachEvent("onload", win._boomrl);
}
}
// Finish the document
doc.close();
}
// See if Preload is supported or not
var link = document.createElement("link");
if (link.relList &&
typeof link.relList.supports === "function" &&
link.relList.supports("preload") &&
("as" in link)) {
window.BOOMR.snippetMethod = "p";
// Set attributes to trigger a Preload
link.href = window.BOOMR.url;
link.rel = "preload";
link.as = "script";
// Add our script tag if successful, fallback to iframe if not
link.addEventListener("load", promote);
link.addEventListener("error", function() {
iframeLoader(true);
});
// Have a fallback in case Preload does nothing or is slow
setTimeout(function() {
if (!promoted) {
iframeLoader(true);
}
}, LOADER_TIMEOUT);
// Note the timestamp we started trying to Preload
BOOMR_lstart = new Date().getTime();
// Append our link tag
parentNode.appendChild(link);
}
else {
// No Preload support, use iframe loader
iframeLoader(false);
}
// Save when the onload event happened, in case this is a non-NavigationTiming browser
function boomerangSaveLoadTime(e) {
window.BOOMR_onload = (e && e.timeStamp) || new Date().getTime();
}
if (window.addEventListener) {
window.addEventListener("load", boomerangSaveLoadTime, false);
}
else if (window.attachEvent) {
window.attachEvent("onload", boomerangSaveLoadTime);
}
})();
</script>
Minified:
<script>(function(){if(window.BOOMR&&(window.BOOMR.version||window.BOOMR.snippetExecuted)){return}window.BOOMR=window.BOOMR||{};window.BOOMR.snippetStart=(new Date).getTime();window.BOOMR.snippetExecuted=true;window.BOOMR.snippetVersion=14;window.BOOMR.url="";var e=document.currentScript||document.getElementsByTagName("script")[0],a=e.parentNode,s=false,t=3e3;function n(){if(s){return}var e=document.createElement("script");e.id="boomr-scr-as";e.src=window.BOOMR.url;e.async=true;a.appendChild(e);s=true}function o(e){s=true;var t,o=document,n,i,d,r=window;window.BOOMR.snippetMethod=e?"if":"i";n=function(e,t){var n=o.createElement("script");n.id=t||"boomr-if-as";n.src=window.BOOMR.url;BOOMR_lstart=(new Date).getTime();e=e||o.body;e.appendChild(n)};if(!window.addEventListener&&window.attachEvent&&navigator.userAgent.match(/MSIE [67]\./)){window.BOOMR.snippetMethod="s";n(a,"boomr-async");return}i=document.createElement("IFRAME");i.src="about:blank";i.title="";i.role="presentation";i.loading="eager";d=(i.frameElement||i).style;d.width=0;d.height=0;d.border=0;d.display="none";a.appendChild(i);try{r=i.contentWindow;o=r.document.open()}catch(e){t=document.domain;i.src="javascript:var d=document.open();d.domain='"+t+"';void 0;";r=i.contentWindow;o=r.document.open()}if(t){o._boomrl=function(){this.domain=t;n()};o.write("<bo"+"dy onload='document._boomrl();'>")}else{r._boomrl=function(){n()};if(r.addEventListener){r.addEventListener("load",r._boomrl,false)}else if(r.attachEvent){r.attachEvent("onload",r._boomrl)}}o.close()}var i=document.createElement("link");if(i.relList&&typeof i.relList.supports==="function"&&i.relList.supports("preload")&&"as"in i){window.BOOMR.snippetMethod="p";i.href=window.BOOMR.url;i.rel="preload";i.as="script";i.addEventListener("load",n);i.addEventListener("error",function(){o(true)});setTimeout(function(){if(!s){o(true)}},t);BOOMR_lstart=(new Date).getTime();a.appendChild(i)}else{o(false)}function d(e){window.BOOMR_onload=e&&e.timeStamp||(new Date).getTime()}if(window.addEventListener){window.addEventListener("load",d,false)}else if(window.attachEvent){window.attachEvent("onload",d)}})();</script>
Delaying the Snippet
You may want to delay loading Boomerang until after the onload
event. This would
ensure that no Boomerang code is executed in the critical-path of the page load.
The main downside to doing this is that you are more likely to lose beacons from some users. The longer it takes Boomerang to load on the page, the higher chance that the user will have navigated away, or closed the browser, before boomerang.js is loaded.
Here is a modification of the Boomerang Loader Snippet to delay until after
onload
:
<script>
(function() {
// Boomerang Loader Snippet version 14
if (window.BOOMR && (window.BOOMR.version || window.BOOMR.snippetExecuted)) {
return;
}
window.BOOMR = window.BOOMR || {};
window.BOOMR.snippetStart = new Date().getTime();
window.BOOMR.snippetExecuted = true;
window.BOOMR.snippetVersion = 14;
// NOTE: Set Boomerang URL here
window.BOOMR.url = "";
var // document.currentScript is supported in all browsers other than IE
where = document.currentScript || document.getElementsByTagName("script")[0],
// Parent element of the script we inject
parentNode = where.parentNode,
// Whether or not Preload method has worked
promoted = false,
// How long to wait for Preload to work before falling back to iframe method
LOADER_TIMEOUT = 3000;
// Tells the browser to execute the Preloaded script by adding it to the DOM
function promote() {
if (promoted) {
return;
}
var script = document.createElement("script");
script.id = "boomr-scr-as";
script.src = window.BOOMR.url;
// Not really needed since dynamic scripts are async by default and the script is already in cache at this point,
// but some naive parsers will see a missing async attribute and think we're not async
script.async = true;
parentNode.appendChild(script);
promoted = true;
}
// Non-blocking iframe loader (fallback for non-Preload scenarios) for all recent browsers.
// For IE 6/7, falls back to dynamic script node.
function iframeLoader(wasFallback) {
promoted = true;
var dom, doc = document, bootstrap, iframe, iframeStyle, win = window;
window.BOOMR.snippetMethod = wasFallback ? "if" : "i";
// Adds Boomerang within the iframe
bootstrap = function(parent, scriptId) {
var script = doc.createElement("script");
script.id = scriptId || "boomr-if-as";
script.src = window.BOOMR.url;
BOOMR_lstart = new Date().getTime();
parent = parent || doc.body;
parent.appendChild(script);
};
// For IE 6/7, we'll just load the script in the current frame, as those browsers don't support 'about:blank'
// for an iframe src (it triggers warnings on secure sites). This means loading on IE 6/7 may cause SPoF.
if (!window.addEventListener && window.attachEvent && navigator.userAgent.match(/MSIE [67]\./)) {
window.BOOMR.snippetMethod = "s";
bootstrap(parentNode, "boomr-async");
return;
}
// The rest of this function is IE8+ and other browsers that don't support Preload hints but will work with CSP & iframes
iframe = document.createElement("IFRAME");
// An empty frame
iframe.src = "about:blank";
// We set title and role appropriately to play nicely with screen readers and other assistive technologies
iframe.title = "";
iframe.role = "presentation";
// Ensure we're not loaded lazily
iframe.loading = "eager";
// Hide the iframe
iframeStyle = (iframe.frameElement || iframe).style;
iframeStyle.width = 0;
iframeStyle.height = 0;
iframeStyle.border = 0;
iframeStyle.display = "none";
// Append to the end of the current block
parentNode.appendChild(iframe);
// Try to get the iframe's document object
try {
win = iframe.contentWindow;
doc = win.document.open();
}
catch (e) {
// document.domain has been changed and we're on an old version of IE, so we got an access denied.
// Note: the only browsers that have this problem also do not have CSP support.
// Get document.domain of the parent window
dom = document.domain;
// Set the src of the iframe to a JavaScript URL that will immediately set its document.domain to match the parent.
// This lets us access the iframe document long enough to inject our script.
// Our script may need to do more domain massaging later.
iframe.src = "javascript:var d=document.open();d.domain='" + dom + "';void 0;";
win = iframe.contentWindow;
doc = win.document.open();
}
if (dom) {
// Unsafe version for IE8 compatibility. If document.domain has changed, we can't use win, but we can use doc.
doc._boomrl = function() {
this.domain = dom;
bootstrap();
};
// Run our function at load.
// Split the string so HTML code injectors don't get confused and add code here.
doc.write("<bo" + "dy onload='document._boomrl();'>");
}
else {
// document.domain hasn't changed, regular method should be OK
win._boomrl = function() {
bootstrap();
};
if (win.addEventListener) {
win.addEventListener("load", win._boomrl, false);
}
else if (win.attachEvent) {
win.attachEvent("onload", win._boomrl);
}
}
// Finish the document
doc.close();
}
function boomerangLoad() {
// See if Preload is supported or not
var link = document.createElement("link");
if (link.relList &&
typeof link.relList.supports === "function" &&
link.relList.supports("preload") &&
("as" in link)) {
window.BOOMR.snippetMethod = "p";
// Set attributes to trigger a Preload
link.href = window.BOOMR.url;
link.rel = "preload";
link.as = "script";
// Add our script tag if successful, fallback to iframe if not
link.addEventListener("load", promote);
link.addEventListener("error", function() {
iframeLoader(true);
});
// Have a fallback in case Preload does nothing or is slow
setTimeout(function() {
if (!promoted) {
iframeLoader(true);
}
}, LOADER_TIMEOUT);
// Note the timestamp we started trying to Preload
BOOMR_lstart = new Date().getTime();
// Append our link tag
parentNode.appendChild(link);
}
else {
// No Preload support, use iframe loader
iframeLoader(false);
}
}
// Save when the onload event happened, in case this is a non-NavigationTiming browser
function boomerangSaveLoadTime(e) {
window.BOOMR_onload = (e && e.timeStamp) || new Date().getTime();
}
if (window.addEventListener) {
window.addEventListener("load", boomerangSaveLoadTime, false);
}
else if (window.attachEvent) {
window.attachEvent("onload", boomerangSaveLoadTime);
}
// Run at onload
function windowOnLoad(e) {
boomerangSaveLoadTime(e);
setTimeout(boomerangLoad, 0);
}
// If we think the load event has already fired, load Boomerang now
if (("performance" in win && win.performance && win.performance.timing && win.performance.timing.loadEventStart) ||
(document.readyState === "complete")) {
boomerangLoad();
}
else {
// Wait until onload
if (win.addEventListener) {
win.addEventListener("load", windowOnLoad, false);
}
else if (win.attachEvent) {
win.attachEvent("onload", windowOnLoad);
}
}
})();
</script>
Minified:
<script>(function(){if(window.BOOMR&&(window.BOOMR.version||window.BOOMR.snippetExecuted)){return}window.BOOMR=window.BOOMR||{};window.BOOMR.snippetStart=(new Date).getTime();window.BOOMR.snippetExecuted=true;window.BOOMR.snippetVersion=14;window.BOOMR.url="";var e=document.currentScript||document.getElementsByTagName("script")[0],r=e.parentNode,s=false,n=3e3;function t(){if(s){return}var e=document.createElement("script");e.id="boomr-scr-as";e.src=window.BOOMR.url;e.async=true;r.appendChild(e);s=true}function i(e){s=true;var n,i=document,t,o,d,a=window;window.BOOMR.snippetMethod=e?"if":"i";t=function(e,n){var t=i.createElement("script");t.id=n||"boomr-if-as";t.src=window.BOOMR.url;BOOMR_lstart=(new Date).getTime();e=e||i.body;e.appendChild(t)};if(!window.addEventListener&&window.attachEvent&&navigator.userAgent.match(/MSIE [67]\./)){window.BOOMR.snippetMethod="s";t(r,"boomr-async");return}o=document.createElement("IFRAME");o.src="about:blank";o.title="";o.role="presentation";o.loading="eager";d=(o.frameElement||o).style;d.width=0;d.height=0;d.border=0;d.display="none";r.appendChild(o);try{a=o.contentWindow;i=a.document.open()}catch(e){n=document.domain;o.src="javascript:var d=document.open();d.domain='"+n+"';void 0;";a=o.contentWindow;i=a.document.open()}if(n){i._boomrl=function(){this.domain=n;t()};i.write("<bo"+"dy onload='document._boomrl();'>")}else{a._boomrl=function(){t()};if(a.addEventListener){a.addEventListener("load",a._boomrl,false)}else if(a.attachEvent){a.attachEvent("onload",a._boomrl)}}i.close()}function o(){var e=document.createElement("link");if(e.relList&&typeof e.relList.supports==="function"&&e.relList.supports("preload")&&"as"in e){window.BOOMR.snippetMethod="p";e.href=window.BOOMR.url;e.rel="preload";e.as="script";e.addEventListener("load",t);e.addEventListener("error",function(){i(true)});setTimeout(function(){if(!s){i(true)}},n);BOOMR_lstart=(new Date).getTime();r.appendChild(e)}else{i(false)}}function d(e){window.BOOMR_onload=e&&e.timeStamp||(new Date).getTime()}if(window.addEventListener){window.addEventListener("load",d,false)}else if(window.attachEvent){window.attachEvent("onload",d)}function a(e){d(e);setTimeout(o,0)}if("performance"in win&&win.performance&&win.performance.timing&&win.performance.timing.loadEventStart||document.readyState==="complete"){o()}else{if(win.addEventListener){win.addEventListener("load",a,false)}else if(win.attachEvent){win.attachEvent("onload",a)}}})();</script>
Known Issues
- Websites using Google Tag Manager (GTM) to inject the Loader Snippet may not see beacons from Firefox <= 74
- These versions of Firefox do not support Preload, so fallback to using the IFRAME loader
- boomerang.js is not fetched due to a Firefox bug with setting the
iframe.src = "about:blank"
, which is done for Content Security Policies (CSP) compatibility - Websites that are not using Content Security Policies can change:
to// An empty frame iframe.src = "about:blank";
// An empty frame iframe.src = "javascript:void(0)";
- Websites that are using Content Security Policies should use a
<script async>
tag to load boomerang.js instead of the Loader Snippet