2015.10.07 t
On the importance of simulated latency testing, and bulletproofing your page from the third-party JS load failures
- use
<script src=".." async defer>
for all your non-critical script, like analytics, ad networks etc. - listen to
DOMContentLoaded
event instead ofload
- sometimes however you may prefer to wait for
load
event; then consider loading non-critical external scripts programatically, in aload
event listener itself, instead of putting them in HTML, even when usingasync
(hence makingload
fire faster) - use guards in your JS whenever using a global variable from a non-critical script, or provide a stub implementation, to be overridden when the real third-party code gets loaded
Last week we've learnt the hard way (in production) about critical rendering path and its friends in modern web browsers.
An external host providing analytics script was very slow to respond. Unfortunately, the third-party script was blocking the load of all the further resources of the page. Hence it took around 70 seconds to load the login page although our own servers were working fine!
It is a common knowledge that you should use <script src=".." async defer>
(or set script.async = true
before assigning src
,
when you do it from JS) and/or put your scripts
at the very bottom of the page, so that as much as possible of the page gets loaded and rendered to the user, as fast as possible --
no one likes staring at a blank white page; users are impatient and will leave quickly.
If you fail to provide the async
flag, the script in synchronous; the browser can't proceed with rendering and any
JavaScript execution until the sync script is loaded executed
(since perhaps the script may want to use document.write
and append to the DOM on-the-fly).
If your page is just an HTML page enhanced with some JavaScript, then you're good with just <script async>
.
Our analytics script was loaded this way. We would display a splash screen without waiting for the script to load, but the actual app load took much more time. This is because...
If you're a JavaScript-heavy single-page application, the next important thing is the event you use to boot your app.
DOMContentLoaded
is an event fired when the HTML is parsed and rendered and DOM is constructed. It is usually fired pretty fast in the lifetime of the app.
On the other hand, load
is only fired when all the resources (images, stylesheets etc.) are retrieved from the network and available to the browser.
This means a single slow image or script can slow down the load event
by an order of magnitude.
(DOMContentLoaded
is not raised in IE8-, but you can work around it via a bit more complex readystatechange
observation).
What might be surprising is that <script async defer>
also blocks the raising of load
event!
In a web dev slang, you may hear a script with async
flag to be called non-blocking, but they're not blocking only
the construction of the DOM tree
(i.e. DOMContentLoaded
event). But they're still blocking the load
event.
Unfortunately we were doing just that: using window.onload = ...
instead of listening to DOMContentLoaded
to boot our app.
In normal circumstances, the difference was negligible, but today it his us hard.
Actually, in our case, we were loading an external script via JS, using script.async = true
, and that external script was in turn loading several others, also setting the async
flag to each of them. However, no amount of async
can save you if you're waiting for load
event!
If you're on Windows, you should already know Fiddler. If not, you're missing out. Go and install it now. I'll wait.
Fiddler registers itself as a proxy and allows you to sniff on a HTTP(S) traffic, modify it, simulate latencies etc. It's much better than any browser's extension.
The very cool feature is adding artificial latencies to particular HTTP requests. That way you can test what happens when an external resource is slow to load, just as in the described case. See below a screenshot which shows how to set up an autoresponder with a latency (drag'n'drop the request from the requests list to create an autoresponder pane and then assign it a latency).
I've assigned 9999 ms latency to test.js
. This makes the load
event fire after 10.07 seconds in my case.
Did I already tell you that waiting for load
event might be suboptimal?
You might like or not the adblockers, but your page should work fine regardless if the user is blocking or not the ads and trackers (for your personal page you may not care, but I bet the company you work for would like the page to be usable).
It happened to me once on a major airline's website that I could not proceed with the booking because I was blocking Google Analytics. The page loaded fine without GA, but then after I clicked some button, the JS code wanted to track that event, but since that required using a global JavaScript variabled injected by GA, it threw an exception and the JavaScript code could not complete.
Now, I am a web dev, so I figured this quickly. Your grandma to whom you've installed Adblock will not.
Try it now:
- Install Adblock Plus on Firefox.
- Open your website
- Click Open blockable items (
CTRL
-SHIFT
-V
) - Add all analytics scripts, ads providers etc. to the blacklist.
- Refresh the page
Your page should load normally, and should be working normally even without non-critical third-party JS. Otherwise you're doing it wrong, just as we were.
Let's say you load http://example.org/analyticsTracker.js
which exposes global trackEvent
JS function.
Then in the code you have calls like trackEvent("userClickedSomething")
.
If analyticsTracker.js
is blocked via an adblocker rule, this JS call will obviously fail, and since it is invoked in a click handler,
the method will throw an exception and fail to complete, perhaps missing to execute some application-critical code (like submitting
a request to the server).
You can just guard all the code with if (trackEvent) trackEvent(...)
but it's likely you'll forget a guard here and there and your app will still break.
A more bulletproof solution for protecting yourself is to just define window.trackEvent = function(){}
temporarily and wait for it to be overwritten once analyticsTracker.js
is loaded.
In case that analyticsTracker.js
somehow never gets loaded, or takes long time to do so, you lose some analytics, but at least the users can use the page normally.
Once I wrote all of this down, I found a similar article from 2011 which comes to many similar conclusions:
Quite useful. Thanks.