HTML / CSS

Optimizing Script Loading in Web Development: Strategies and Techniques

A single badly-placed <script> tag can block your entire page from rendering. I've seen landing pages that took 6 seconds to show content because a third-...

8 May 2024

Optimizing Script Loading in Web Development: Strategies and Techniques

A single badly-placed <script> tag can block your entire page from rendering. I've seen landing pages that took 6 seconds to show content because a third-party analytics script was loaded synchronously in the <head>. Moving it to defer cut the time to first paint in half.

How you load scripts determines when users see your page. Here are the options and when to use each.

1. Inline scripts

Scripts placed directly in the HTML block parsing while they execute. The browser stops rendering, runs the script, then continues.

Text
<script>
  console.log('I block rendering')
</script>

Use inline scripts only for tiny, critical initialization logic -- like setting a CSS class for dark mode before the page paints. For everything else, use external scripts.

2. External scripts with async

The async attribute tells the browser to download the script in parallel with HTML parsing. Once downloaded, it executes immediately -- which means it can still block rendering at that point.

Text
<script src="analytics.js" async></script>

Use async for scripts that are independent of the page content and don't depend on other scripts. Analytics, tracking pixels, and ad scripts are good candidates. The execution order is not guaranteed, so don't use async if scripts depend on each other.

3. External scripts with defer

defer also downloads in parallel, but delays execution until after the HTML is fully parsed. Scripts execute in the order they appear in the document.

Text
<script src="app.js" defer></script>

This is my default for application scripts. The user sees the page content while the script downloads. Execution happens in a predictable order after the DOM is ready.

4. Dynamic script loading

Sometimes you don't want a script to load at all until a user takes an action. Dynamic loading gives you full control.

Text
const loadScript = (src: string): Promise<void> => {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = src
    script.onload = () => resolve()
    script.onerror = () => reject(new Error(`Failed to load: ${src}`))
    document.head.appendChild(script)
  })
}

// Load a heavy chart library only when the user opens the dashboard
const handleOpenDashboard = async () => {
  await loadScript('/vendor/chart-library.js')
  renderChart()
}

This is the right approach for heavy third-party libraries that not every user needs.

The rule I follow

Put critical styles in the <head>. Put application scripts at the bottom of <body> with defer. Lazy-load everything else. If a script isn't needed for the initial page render, don't load it until it is.