Script | Async | Defer – Why You Should Care?
A few months ago in my current company, we launched a new marketing page for our React web app. It was beautifully designed, loaded with animations, third-party scripts, and a lot of excitement from the team. But shortly after launch, the excitement turned into confusion.
Why?
Because our First Contentful Paint (FCP) was terrible.
😫 The Problem: A Blank Screen and a Frustrated PM
"It takes almost 3 seconds before anything even shows up."
– Our PM, after checking the Lighthouse report
Our app had everything:
A React hydration setup for the hero section.
A third-party analytics library. - Google Analytics
A customer support chat widget. - Intercom
A font loader script. - Google Font loader script
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>And, a fancy scroll animation library.
So, what did we do? - Removed all these scripts. 😝 - Nope, Just Kidding
Digging into root cause of the problem -
All of those were being loaded like this in our index.html:
<script src="https://cdn.analytics.com/track.js"></script>
<script src="/scroll-animations.js"></script>
<script src="https://chat.support.io/widget.js"></script>
No async. No defer. Just plain ol’ blocking <script> tags.
The browser was blocking rendering until all scripts were downloaded, parsed, and executed. That meant our LCP image, headings, and layout paint were delayed.
Digging Deeper
We ran a Lighthouse audit. Here's what stood out:
First Contentful Paint: ~2.8s
Main Thread Blocking Time: High
Scripts: Loaded sequentially before any paint
You can read more about the important web-vitals here
The waterfall chart made it obvious:
The browser was stuck downloading and executing JS while the user stared at a white screen.
We realized something had to change. We needed to load those scripts in a non-blocking way. That’s when we dove into:
Understanding script, async, and defer
Before we picked the right attributes, we had to actually understand what each of them meant — beyond just StackOverflow one-liners.
Here’s how we broke it down for ourselves over coffee:
“Okay team, imagine the browser is reading your HTML like a book. Now someone yells ‘Hey, run this JavaScript!’—what happens next?”
🔎 What we found:
<script>
This is the default — the bossy one.
It stops everything mid-sentence, makes the browser download and execute it, and only then continues reading.“Nope, not going anywhere until I finish this!”
<script async>
This one multitasks.
The browser keeps reading the HTML while the script downloads in the background.
But as soon as it's ready, it interrupts everything to execute it.“I don’t care what you’re doing — I’m ready, let’s go!”
<script defer>
The polite one.
It also downloads while the HTML is being read, but waits until the whole DOM is parsed before it runs.
Plus, it respects order — if you have three defer scripts, they’ll run one after the other.“I’ll wait my turn. You finish your book first.”
Choosing the Right Tool: Real Scripts, Real Choices
Once we understood what each script loading strategy actually did, we huddled around our performance dashboard and started profiling our script usage like detectives.
Every script was costing us time. But not all scripts were equally guilty.
So, we broke them down—one by one—and matched them with the loading strategy that made the most sense. Here's what that looked like:
1. Third-Party Tracking (analytics.js)
This was our visitor analytics script. It helped us track page views, clicks, and user journeys. Important for marketing—but completely irrelevant to rendering the UI.
Yet... we were loading it using a plain <script>, blocking everything. That made zero sense.
🧠 Our Thought Process:
❌ Doesn’t affect the page visually.
❌ Doesn’t touch the DOM.
❌ Doesn’t need to run before the first paint.
✅ Just needs to send data eventually.
So, we switched to:
<script defer src="https://cdn.analytics.com/track.js"></script>
👉 defer made sure it loaded in the background and executed only after the browser had painted the first content. Non-blocking. Lightweight. Perfect.
2. Scroll Animation Library (scroll-animations.js)
This one was a little trickier. It handled fade-ins and parallax effects when users scrolled through sections. It absolutely needed the DOM to be fully parsed — otherwise it would error out trying to attach to elements that didn’t yet exist.
Originally, it too was blocking the render with a regular <script>, and it was a big file. Result: users were staring at a blank screen waiting for a scroll effect script to load. Not cool.
🧠 Our Thought Process:
✅ Needed DOM to exist
❌ Didn’t need to run immediately
❌ Shouldn’t block painting
We chose:
<script defer src="/scroll-animations.js"></script>
👉 defer ensured it only executed once the DOM was fully available. It waited politely in line — just how we wanted it.
3. Support Chat Widget (chat.support.io)
This one sat in the bottom-right corner and allowed users to chat with support. It injected a floating widget and opened a live session on click. Cool? Yes. Essential to initial load? Not at all.
In fact, most users didn’t use it unless they were stuck or had a question. So why were we forcing every user to wait for it?
🧠 Our Thought Process:
❌ Didn’t touch critical UI
❌ Didn’t depend on DOM content
✅ Could be loaded whenever ready
We flipped the switch:
<script async src="https://chat.support.io/widget.js"></script>
👉 async allowed it to download in parallel, and execute as soon as it finished. It didn’t block HTML parsing. It didn’t delay rendering. And it still showed up right on time when needed.
TL;DR: Treat Your Scripts Like Team Members
Each script plays a role—but not every script deserves a front-row seat.
Critical to DOM? Use
deferCan run whenever? Use
asyncNeed to block rendering? Ask yourself why, and change it if possible
This conscious shift in script loading gave us measurable wins — and more importantly, a smoother experience for our users.
What We Learned About Script Tags (The Hard Way)
When you're building a modern React app or any frontend application, it's easy to forget that how you load your scripts matters. Especially when third-party tools sneak into your page. Every script can cost you precious milliseconds – and your user’s patience.
defer is usually the safest and best choice for scripts that depend on the DOM but don’t need to block rendering.async is ideal for scripts that can run whenever they finish loading – especially those that don’t interact with the DOM.
So the next time you’re adding a <script> tag, ask yourself:
Does this really need to block my page?
Choose wisely. Your FCP (and your users) will thank you.
Keep an eye on how much time it is taking for your page to parse the HTML Doc
During our deep dive into what was slowing down our React app’s First Contentful Paint, we stumbled across something we hadn’t really paid much attention to before:
The time it takes just to parse the HTML document.
We’d always focused on JavaScript bundle size, lazy loading components, and image optimization — all important.
But this? This was subtle. Yet critical.

🛠 How You Can Spot It
Want to catch this in your own app?
Here’s how to do it:
Open Chrome DevTools → Performance tab
Hit the "⏺ Record" button
Reload your page
Look for the “Parse HTML” activity in the flame chart
Observe:
How often it's interrupted
Whether any scripts are pausing it
If rendering is delayed because of it
If your HTML parsing is constantly getting paused by script execution, that’s a sign your <script> tags are in the way — and it’s time to rethink how you're loading them.
Read more about it here