Speed Lab: blocking, deferred and async scripts

The same two scripts placed five ways: blocking in the head, defer, async, a module and the end of the body. First paint, blocking time and when the code runs.

IntermediateHow-to8 min readUpdated October 30, 2026

In this guide
  1. The question
  2. The page and the scripts
    1. The five variants
  3. Results
    1. When things happened
    2. Why Total Blocking Time misleads here
    3. The async variant
  4. The same test on a shaped network
  5. What this test does not show
  6. What to do
  7. Run it yourself
  8. Mistakes to avoid
  9. Check your own page
  10. Sources

The question

A script in the head of a page normally stops the browser from reading any further until the script has downloaded and run. The defer and async attributes, module scripts and the old habit of putting scripts at the end of the page all exist to avoid that. The guide to defer, async and modules explains what each does. This test puts a number on it: how much does the placement of a script change when the first content appears, and what happens to the work the script does?

The page and the scripts

The test page holds only text: a header, a heading, three paragraphs, a short list and a small external style sheet. With no images, the biggest thing on the screen is text, and it is painted together with everything else. Two scripts sit beside it, and every variant uses the same files:

const t = performance.now();
while (performance.now() - t < 120) {}
document.documentElement.setAttribute('data-ready', '1');

I ran it as described in how the Speed Lab tests are run: nine Lighthouse runs per variant, taken in turns, on a local server, with Lighthouse's mobile settings.

The five variants

<!-- a. blocking, in the head -->
<script src="lib.js"></script><script src="app.js"></script>

<!-- b. defer, in the head -->
<script defer src="lib.js"></script><script defer src="app.js"></script>

<!-- c. async, in the head -->
<script async src="lib.js"></script><script async src="app.js"></script>

<!-- d. blocking, at the end of the body -->
<script src="lib.js"></script><script src="app.js"></script>

<!-- e. module, in the head -->
<script type="module" src="lib.js"></script><script type="module" src="app.js"></script>

Results

The first and the largest paint were the same moment, so one column shows both. Total Blocking Time is Lighthouse's sum of the time over 50 ms in main-thread tasks after the first paint.

VariantFirst paint, median (lowest to highest)Total Blocking Time, median (lowest to highest)
a. Blocking, in the head1,706 ms (1,678 to 1,735)0 ms (0 to 0)
b. defer1,220 ms (1,211 to 1,236)77 ms (74 to 78)
c. async1,223 ms (1,211 to 1,249)70 ms (63 to 72)
d. Blocking, at the end of the body1,220 ms (1,206 to 1,240)73 ms (72 to 75)
e. Module1,218 ms (1,208 to 1,284)75 ms (73 to 79)

The page was 60.4 KiB in every variant, and Cumulative Layout Shift was 0.

Four things stand out.

  1. The blocking script in the head cost 486 ms, a 40% longer wait for the first content. Its range does not overlap with any other.
  2. The other four are the same. defer, async, the module and the end of the body painted within 5 ms of each other, which is noise.
  3. The code ran in every variant. Nothing was lost; the work only moved.
  4. The slowest page has the best blocking-time score. Variant a shows 0 ms of Total Blocking Time and the others 70 to 77 ms.

When things happened

The Lighthouse timeline and its list of long tasks explain it. These are medians, in milliseconds.

Variantlib.js finishedThe 120 ms task startedFirst paintPriority of the scripts
a. Blocking, in the head1,5121,5281,706High
b. defer1,5071,5271,220Low
c. async1,5091,2201,223Low
d. End of the body1,5011,5221,220High
e. Module1,5051,5231,218High

The style sheet finished at about 1,185 ms in every variant. That is when the page could paint.

In variant a, the parser stops at the first script. The page cannot paint until lib.js has arrived at 1,512 ms and app.js has run for 123 ms, which ends around 1,651 ms. The paint follows at 1,706 ms. Everything the visitor could have read at 1,220 ms waited about half a second for code that has nothing to do with reading it.

In the other variants the browser paints as soon as the style sheet is there. The scripts finish downloading at about 1,500 ms, and the 120 ms task runs after the paint, so the visitor already has the text.

Why Total Blocking Time misleads here

Lighthouse counts blocking time only between the first paint and the point where the page is quiet. In variant a the long task ran before the first paint, so it does not count. The metric reports a good number for the page that made visitors wait the longest. This is why a score should never be read alone. See Total Blocking Time and Speed Index.

The async variant

Variant c painted on time, but with a caution. An async script runs the moment it arrives. Here app.js arrived at 1,201 ms, 16 ms after the style sheet, and its task started at 1,220 ms, next to the paint at 1,223 ms. If it had arrived a little earlier, the 120 ms task could have come before the paint and delayed it. That is my reasoning from the timeline, and I did not test it. It also means async scripts run in whatever order they arrive: app.js ran before lib.js had finished here. That is fine for scripts that depend on nothing, and breaks code that expects its library to be loaded first.

The same test on a shaped network

Here are the five variants again on the shaped network of the test of many small files, five runs each, with the first paint of the usual throttling beside it.

VariantUsual throttlingShaped network
a. Blocking, in the head1,706 ms1,225 ms (1,205 to 1,279)
b. defer1,220 ms588 ms (559 to 611)
c. async1,223 ms570 ms (548 to 584)
d. Blocking, at the end of the body1,220 ms567 ms (560 to 609)
e. Module1,218 ms582 ms (548 to 591)

The picture is the same and the cost is larger: the blocking script in the head delayed the first paint by 637 ms (1,225 ms against 588 ms), where the usual throttling found 486 ms. The other four variants are equal here too. The reason is the size of the script. lib.js is 59 KB and was sent uncompressed. On the shaped network a new connection starts slowly, so the script arrived at about 1,024 ms, roughly 500 ms after the style sheet, against 327 ms after it with the usual throttling, which has no slow start. A smaller script, or a compressed one, would cost less.

What this test does not show

What to do

  1. Find the blocking scripts. The insight called "Render-blocking requests" in the audit lists them. In my runs it listed three requests for variant a (both scripts and the style sheet) and only the style sheet for defer. For the script at the end of the body it also named app.js, although the first paint did not wait for it, so treat the list as candidates and check the timeline.
  2. Use `defer` for scripts that need the page or each other. It keeps their order, and they run after the page is read.
  3. Use `async` for scripts that depend on nothing, such as a stand-alone tag. Remember that they run when they arrive.
  4. Treat a script at the end of the body as a second choice. It painted as well as defer here, but it blocks the parser at that point and runs in order only by position.
  5. Look at the work, not only the download. Variants b to e all spent 70 to 77 ms of blocking time. Cutting what a script does is the next step. See break up long tasks.
  6. Ask what each blocking tag is for. If it protects against a flash, test whether the flash matters more than half a second.

Run it yourself

Make five folders, one per variant, each with the same style.css, lib.js and app.js and an index.html that differs only in the script tags above. Use the script from the lazy-loading lab with one change: read first-contentful-paint instead of largest-contentful-paint. To see the long tasks, open a Lighthouse report's list of long tasks, or record the load in the Performance panel. See Chrome DevTools Performance panel.

Mistakes to avoid

Check your own page

Run the free audit and open the render-blocking insight. For each script it lists, try defer and run the audit again. Compare the First Contentful Paint and the Total Blocking Time together.

Sources

Start here

Free website audit

Live Google data for any public page: mobile and desktop.

Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it

Rather have this done for you? Speed optimization · Book a call

Related guides

All guides →