Async Everywhere Would Have Broken the Footer
A site-wide script tag audit across every page - and the one script that had to stay exactly where the rule said it shouldn't go.
"Use async for order-independent stuff, use
defer for anything that needs the DOM or a specific
order" is a correct rule almost everywhere. Applying it mechanically
to every <script> tag on a real site is a
different exercise, because a handful of scripts don't announce
which category they're in just from what they do.
This is that audit, across every real page on this site - what got reclassified, what stayed exactly as it was, and the one script where the obvious-looking answer was wrong.
Real content pages, each loading the same set of scripts:
header-min.js and favicon-min.js in the
head, then jquery.min.js, two jQuery plugins,
browser.min.js, breakpoints.min.js,
main.js, and footer-min.js before the
closing tag. The second group was already consistently marked
defer on every page. The first group wasn't -
header-min.js and favicon-min.js had no
loading attribute at all on most pages, and defer on a
handful of others, with no pattern explaining the difference.
favicon-min.js only appends
<link rel="icon"> tags to the head - zero
rendering impact, zero dependency on the DOM body, zero dependency
on any other script. A clean async
candidate.
header-min.js injects preconnect hints and the Font
Awesome / Google Fonts stylesheet links - not a tracker, but still
order-independent relative to every other script on the page, and
with no DOM-readiness requirement of its own.
jquery.min.js through footer-min.js form
a real dependency chain: the jQuery plugins need jQuery defined
first, main.js reads globals that
browser.js and breakpoints.js set up,
and footer-min.js registers the
<scott-footer> custom element that's already
sitting in every page's HTML by the time these scripts run.
src attribute at all, so async and
defer don't apply to it either way - it already
handles its own asynchronous loading internally by setting
.async = true on the gtag/js element
it creates at runtime.
This one wasn't close. Favicon links don't affect layout, paint,
or any other script's behavior. There's no scenario where this
script running slightly later or slightly out of order changes
anything a visitor sees.
async, across all pages.
This is the one that looked like it should be
defer at first glance - it touches the head, and
"touches the head" reads a lot like "needs to be careful about
timing." But defer specifically means "wait until the
entire document has finished parsing," and this script injects the
Font Awesome and Google Fonts stylesheet links. Pushing that
injection until after the whole page has parsed is the exact
regression documented in
a previous audit on this site
- it dropped the mobile PageSpeed score from 71 to 51 the last
time a font-related script got delayed like that.
async keeps the parser unblocked without pushing the
injection all the way to the end of the document.
These six scripts were already correctly marked
defer on every page, and that was worth confirming
rather than assuming. defer scripts execute in the
order they appear in the document, right before
DOMContentLoaded - which is precisely what this chain
needs, since each script after the first one depends on something
the previous one set up. Marking any of these
async would let the browser run them in whatever
order they finish downloading, which for a same-origin set of
small files could easily mean main.js running before
browser.js has set the values it reads.
The gtag.js bootstrap script has no src, and per the
HTML spec, async and defer have no
effect on a classic script without one - browsers ignore them on
inline scripts. Adding either would be a no-op at best, and
worth documenting as a no-op rather than leaving it to look like
an oversight.
| Script | Before | After |
|---|---|---|
header-min.js |
No attribute on most pages, defer on a few
|
async, consistent across all pages |
favicon-min.js |
No attribute on most pages, defer on a few
|
async, consistent across all pages |
jquery.min.js through
footer-min.js
|
defer, in document order |
Unchanged - already correct |
| Inline gtag.js bootstrap | No src, no attribute |
Unchanged - attributes don't apply |
A regex across all HTML files, two of which turned out to be
directory placeholders with no scripts to touch at all. The actual
work was smaller than the checking: confirming every
<script src> tag site-wide ended up with exactly
one loading attribute, that no tag picked up a duplicate, and that
every embedded JSON-LD block still parsed after the sweep. None of
that shows up in a diff - it's the difference between a change that
looks done and one that's actually verified. Auditing script-loading
behavior like this falls under
Web & Digital work, the same
practice applied here to this site's own script tags.
Because "sort it out" is exactly what async doesn't
guarantee - it explicitly gives up execution order between scripts.
For scripts with real dependencies, like a plugin that needs its
library loaded first, that's not a performance optimization, it's a
race condition.
Ask two questions: does it read or rely on something another script
sets up, and does it need the page's HTML to be fully parsed before
it runs. A yes to either points to
defer. A no to both, and no rendering impact either
way, points to async.
Not for execution order between them - that's determined by whichever one finishes downloading first, not by document position. Position still matters for readability and for where the browser discovers the tag during parsing, just not for the order the scripts actually run in.
A short conversation is usually enough to map out which scripts have real dependencies and which ones don't.