Written by Harry Roberts on CSS Wizardry.
Desk of Contents
Actually, I began writing this text for no actual purpose, and considerably
with out context, in December 2022—over half a 12 months in the past! However, I left it in
_drafts/
till in the present day, when a genuinely compelling situation got here up that provides
actual alternative for rationalization. It now not seems like
trivia-for-the-sake-of-it due to a latest consumer mission.
I by no means thought I’d write an article in defence of DOMContentLoaded
, however right here
it’s…
For a lot of, a few years now, efficiency engineers have been making a concerted
effort to maneuver away from technical metrics akin to Load
, and towards extra
user-facing, UX metrics akin to Pace
Index
or Largest Contentful
Paint.
Nevertheless, as an inner benchmark, there are compelling explanation why a few of you
may very well need to hold monitoring these ‘outdated’ metrics…
Measure the Person Expertise
The issue with utilizing diagnostic metrics like Load
or DOMContentLoaded
to
measure site-speed is that it has no bearing on how a consumer may really
expertise your web site. Certain, when you have Load
instances of 18 seconds, your web site
in all probability isn’t very quick, however a superb Load
time doesn’t imply your web site is
essentially very quick, both.

Within the comparability above, which do you suppose supplies the higher consumer expertise?
I’m keen to guess you’d all say B, proper? However, primarily based on DOMContentLoaded
,
A is definitely over 11s quicker!
Load
and DOMContentLoaded
are inner browser occasions—your customers haven’t any
thought what a Load
time even is. I guess half of your colleagues don’t both. As
metrics themselves, they’ve little to no reflection on the actual consumer
expertise, which is precisely why we’ve moved away from them within the first
place—they’re a poor proxy for UX as they’re not emitted when something helpful to
the consumer occurs.
Or are they…?
Technically Significant
Not all metrics want to be user-centric. I’m keen to guess you continue to monitor
TTFB, though
you realize your prospects could have no idea of a primary byte in anyway. That is
as a result of some metrics are nonetheless helpful to builders. TTFB is an effective measure of
your server response instances and normal back-end well being, and points right here could have
knock-on results later down the road (particularly with Largest Contentful Paint).
Equally, each DOMContentLoaded
and Load
aren’t simply meaningless browser
occasions, and when you perceive what they really signify, you may get some
actual insights as to your web site’s runtime behaviour from every of them. Diagnostic
metrics akin to these can spotlight bottlenecks, and the way they could finally
influence the consumer expertise in different methods, even when in a roundabout way.
That is notably true within the case of DOMContentLoaded
.
What Does It Truly Imply?
The DOMContentLoaded
occasion
fires as soon as all your defer
purple JavaScript has completed operating.
Subsequently, anybody leaning closely on defer
—or frameworks that utilise
it—ought to instantly see the importance of this metric.
If you happen to aren’t (capable of) monitoring customized metrics round your utility’s
interactivity, hydration state, and many others., then DOMContentLoaded
instantly
turns into a really helpful proxy. Understanding when your principal bundles have run is nice
perception in lieu of extra forensic runtime information, and it’s one thing I take a look at with
any consumer that leans closely on (frameworks that lean closely on) defer
or
kind=module
.
Extra precisely, DOMContentLoaded
signifies that all blocking and
defer
and kind=module
code has completed operating. We don’t have any
visibility on whether or not it ran efficiently but it surely has a minimum of completed.
Placing It to Use
I’m working with a consumer in the intervening time who’s utilizing Nuxt
and has their client-side JavaScript break up into an eyewatering 121 defer
purple
information:

Above, the vertical pink line at 12.201s signifies the DOMContentLoaded
occasion.
That’s late! This consumer doesn’t have any RUM or customized monitoring in place (but), so, aside from Core Internet Vitals, we don’t have a lot
visibility on how the location performs within the wild. Primarily based on a 12s
DOMContentLoaded
occasion, I can’t think about it’s doing so properly.
The issue with Core Internet Vitals, although, is that its solely actual JavaScripty
metric, First Enter Delay, solely offers with consumer
interplay: what I wish to know is with 121
Primarily based on the lab-based 12sdefer
purple information, when is
there one thing to truly work together with?!
above, I’d like to know what’s taking place for actual customers. And by chance, whereas
DOMContentLoaded
is now thought-about a legacy metric, we are able to nonetheless get discipline
information for it from two fairly respectable sources…
Chrome Person Expertise Report (CrUX)

CrUX Dashboard is one among
only a few CrUX assets that surfaces
the DOMContentLoaded
occasion to us. Above, we are able to see that, presently, solely 11%
of Chrome guests expertise a Good DOMContentLoaded
—nearly 90% of individuals
are ready over 1.5s earlier than the app’s key performance is out there, with
nearly half ready over 3.5s!

DOMContentLoaded
was 4.7s for 75% of Chrome guestsin Might 2023.
It might additionally appear that Treo (which is a very wonderful software)
surfaces DOMContentLoaded
information for a given
origin.
Google Analytics
Till, properly,
in the present day, Google
Analytics additionally surfaced DOMContentLoaded
info. Solely this time, we
aren’t restricted to only Chrome visits! That stated, we aren’t offered with
notably granular information, both:

After a little bit of including up (2.15 + 10.26 + 45.28 + 25.68 + 13.07
= 96.44), we see that the ninety fifth percentile of DOMContentLoaded
occasions for a similar time interval (Might 2023) is someplace between 5 and 10
seconds. Not massively useful, however an perception nonetheless, and a minimum of reveals
us that the lab-based 12s is unlikely to be felt by anybody aside from excessive
outliers within the discipline.
Takeaways listed here are:
- Solely about 10% of Chrome guests have what Google deem to be a Good
DOMContentLoaded
. Alldefer
purple JavaScript has run inside 1.5s for less than
the huge minority of tourists. - 3.56% of all customers waited over 10s for
DOMContentLoaded
. It is a 10
second look ahead to keydefer
purple JavaScript to run.
Provided that the DOMContentLoaded
occasion fires after the final of our
defer
purple information has run, there’s each chance that key performance from
any previous information has already turn into accessible, however that’s not one thing we
have any visibility over with out trying into customized monitoring, which is
precisely the state of affairs we’re in right here. Keep in mind, that is nonetheless a proxy metric—simply
a way more helpful one than you might have realised.
Digging Deeper: The Navigation Timing API
If we need to seize this information extra intentionally ourselves, we have to lean on
the Navigation Timing
API,
which supplies us entry to a set of milestone timings, lots of which you will
have heard of earlier than.
The DOMContentLoaded
as measured and emitted by the Navigation Timing API is
really known as domContentLoadedEventStart
—there is no such thing as a naked
domContentLoadedEvent
in that spec. As a substitute, we have now:
domContentLoadedEventStart
: That is the one we’re excited about, and is
equal to the idea we’ve been discussing on this article up to now. To
get the metric we’ve been referring to asDOMContentLoaded
, you want
window.efficiency.timing.domContentLoadedEventStart
.- As a result of
defer
purple JS is assured to run after synchronous JS, this occasion
additionally marks the purpose that each one synchronous work is full.
- As a result of
domContentLoadedEventEnd
: The top occasion captures the time at which all
JS wrapped in aDOMContentLoaded
occasion listener has completed operating:window.addEventListener('DOMContentLoaded', (occasion) => { // Do one thing });
- That is separate to
defer
purple JavaScript and runs after our
DOMContentLoaded
occasion—if we’re operating a nontrivial quantity of code at
DOMContentLoaded
, we’re additionally on this milestone. That’s not in
the scope of this text, although, so we in all probability gained’t come again to that
once more.
- That is separate to
Very, very crudely, with no syntactic sugar in anyway, you may get the web page’s
DOMContentLoaded
occasion in milliseconds with the next:
console.log(window.efficiency.timing.domContentLoadedEventStart - window.efficiency.timing.navigationStart);
…and the length (if any) of the DOMContentLoaded
occasion with:
window.efficiency.timing.domContentLoadedEventEnd - window.efficiency.timing.domContentLoadedEventStart
And naturally, we needs to be very used to seeing DOMContentLoaded
on the backside
of DevTools’ Community panel:

Even Extra Insights
Whereas DOMContentLoaded
tells us when our defer
purple code completed
operating—which is nice!—it doesn’t inform us how lengthy it took to run. We would
have a DOMContentLoaded
at 5s, however did the code begin operating at 4.8s? 2s? Who
is aware of?!
We do.

Within the above waterfall, which is similar one from earlier, solely even shorter,
we nonetheless have the vertical pink line round 12s, which is DOMContentLoaded
,
however we even have a vertical sort-of yellow line round 3.5s (really, it’s at
3.52s precisely). That is domInteractive
. domInteractive
is the occasion
instantly earlier than domContentLoadedEventStart
. That is the second the browser
has completed parsing all synchronous DOM work: your HTML and all blocking
scripts it encountered on the best way. Principally, the browser is now on the
</html>
tag. The browser is able to run your defer
purple JavaScript.
One crucial factor to notice is that the domInteractive
occasion fired lengthy,
lengthy earlier than the request for file 133 was even dispatched. Instantly this tells
us that the delta between domInteractive
and DOMContentLoaded
contains code
execution and any remaining fetch.
Fortunately, the browser wasn’t simply idling on this time. As a result of defer
purple code
runs in sequence, the browser sensibly fetches the information so as and
instantly executes them once they arrive. This stage of orchestration may be very
elegant and helps to utilise and preserve assets in essentially the most useful approach. Not
flooding the community with responses that may’t but be used, and in addition ensuring
that the principle thread is saved busy.
That is the JavaScript we have to measure how lengthy our defer
purple exercise took:
console.log(window.efficiency.timing.domContentLoadedEventStart - window.efficiency.timing.domInteractive);
Now, utilizing the Navigation Timing API, we have now visibility on when our defer
purple
completed operating, and the way lengthy it took!
This demo under accommodates:
- A slow-to-load, fast-to-run
defer
purple JavaScript file. - A quick-to-load, slow-to-run inline script set to run at
DOMContentLoaded
. - Logging that out to the console on the
Load
occasion.
<!-- [1] -->
<script src=https://slowfil.es/file?kind=js&delay=2000 defer></script>
<!-- [2] -->
<script>
window.addEventListener('DOMContentLoaded', (occasion) => {
// Cling the browser for 1s on the `DOMContentLoaded` occasion.
operate wait(ms) {
var begin = Date.now(),
now = begin;
whereas (now - begin < ms) {
now = Date.now();
}
}
wait(1000);
});
</script>
<!-- [3] -->
<script>
window.addEventListener('load', (occasion) => {
const timings = window.efficiency.timing;
const begin = timings.navigationStart;
console.log('Prepared to begin operating `defer`ed code: ' + (timings.domInteractive - begin + 'ms'));
console.log('`defer`ed code completed: ' + (timings.domContentLoadedEventEnd - begin + 'ms'));
console.log('`defer`ed code length: ' + (timings.domContentLoadedEventStart - timings.domInteractive + 'ms'));
console.log('`DOMContentLoaded`-wrapped code length: ' + (timings.domContentLoadedEventEnd - timings.domContentLoadedEventStart + 'ms'));
});
</script>
</physique>
</html>

`defer`ed code completed: 3129ms
traces upwith DevTools’ personal reported 3.13s
DOMContentLoaded
.Or check out the dwell demo on Glitch.
A Higher Method?
That is all genuinely thrilling and fascinating to me, however we’re operating into
points already:
DOMContentLoaded
is a proxy for when all of yourdefer
purple JavaScript has run,
but it surely doesn’t notify you if issues ran efficiently, or spotlight any key
milestones as performance is consistently turning into accessible for the length.DOMContentLoaded
tells us how lengthy every little thing took, however that might embrace
fetch, and there’s no approach of isolating the fetch from pure runtime.- If you happen to’re capturing these technical timings, you may as properly use the Person
Timing API.
I need to broaden on the final level.
If we’re going to go to the trouble of measuring Navigation Timing occasions, we
may as properly use the rather more helpful Person Timing
API.
With this, we are able to emit high-resolution timestamps at arbitrary factors in our
utility’s lifecycle, so as an alternative of proxying availability through a Navigation
Timing, we are able to drop, for instance, efficiency.mark('app booted')
in our code.
In actual fact, that is what Subsequent.js
does
to let you realize when the app has hydrated, and the way lengthy it took. These Person
Timings robotically seem within the Efficiency panel:

I take advantage of efficiency.mark()
and efficiency.measure()
in a number of locations on this
web site,
mainly to watch how lengthy it takes to parse the <head>
and its CSS.
The Person Timing API is way extra suited to this sort of monitoring than one thing
like DOMContentLoaded
—I’d solely take a look at DOMContentLoaded
if we don’t but
have applicable metrics in place.
Nonetheless, the important thing and most fascinating takeaway for me is that if all we have now
entry to is DOMContentLoaded
(or we aren’t already utilizing one thing extra
appropriate), then we do even have some visibility on app state and
availability. If you’re utilizing defer
or kind=module
, then DOMContentLoaded
is likely to be extra helpful to you than you realise.
Again to Work
I discussed beforehand that the DOMContentLoaded
occasion fires as soon as all
defer
purple JavaScript has run, which implies that we may probably be
trickling performance all through the whole time between domInteractive
and
DOMContentLoaded
.
In my consumer’s case, nonetheless, the location is totally nonfunctional till the
final file (response 133 within the waterfall) has efficiently executed. In
truth, blocking the request for file 133 has the very same impact as disabling
JavaScript completely. This implies the DOMContentLoaded
occasion for them is an
nearly actual measure of when the app is out there. Because of this monitoring
and enhancing DOMContentLoaded
could have a direct correlation to an improved
buyer expertise.
Bettering DOMContentLoaded
Provided that DOMContentLoaded
marks the purpose at which all synchronous HTML and
JavaScript has been handled, and all defer
purple JavaScript has been fetched
and run, this leaves us many various alternatives to enhance the metric: we
may scale back the scale of our HTML, we may take away or scale back costly
synchronous JavaScript, we may inline small scripts to take away any community
price, and we are able to scale back the quantity of defer
purple JavaScript.
Additional, as DOMContentLoaded
is a milestone timing, any time we are able to shave from
previous timings needs to be realised in a while. For instance, all issues being
equal, a 500ms enchancment in TTFB will yield a 500ms enchancment in subsequent
milestones, akin to First Contentful Paint or, in our case, DOMContentLoaded
.
Nevertheless, in our case, the delta between domInteractive
and
DOMContentLoaded
was 8.681s, or about 70%. And whereas their TTFB definitely does
want enchancment, I don’t suppose it might be the best place to spend
time whereas tackling this explicit drawback.
Nearly all of that 8.7s was misplaced to queuing and fetching that sheer variety of
bundles. Not essentially the scale of the bundles—simply the sheer amount of
information that want scheduling, and which every carry their very own latency price.
Whereas we haven’t labored out the candy spot for this mission, as a rule,
a smaller variety of bigger bundles would normally obtain a lot quicker than many
tiny ones:
As a rule, RTT (α) stays fixed whereas obtain time (𝑥) is proportional to filesize. Subsequently, splitting one giant bundle into 16 smaller ones goes from 1α + 𝑥 to 16α + 16(0.0625𝑥). Anticipate issues to in all probability get just a little slower. pic.twitter.com/c0hEsIAwKq
— Harry Roberts (@csswizardry) 21 January, 2021
My recommendation on this case is to tweak their construct to output possibly 8–10 bundles and
re-test from there. It’s essential to stability bundle dimension, variety of bundles,
and caching methods, but it surely’s clear to me that the difficulty right here is overzealous
code-splitting.
With that carried out, we must always have the ability to enhance DOMContentLoaded
, thus having
a noticeable influence on performance and subsequently buyer expertise.
DOMContentLoaded
has proved to be a really, very helpful metric for us.