Skip to lesson

How a page loadsLesson

A page load is a chain of waits.

Between pressing Enter and seeing the first words, the browser finds the server, may be sent somewhere else, opens a connection, asks for the page and reads it, finding more files as it goes. Most of that time is spent waiting, and much of the waiting is yours to remove.

Submodule 1 of 14About 19 min readFree interactive preview

Before you begin

You know what HTML, CSS and JavaScript files are, and that a browser asks a server for them. You do not need any networking knowledge.

You type an address, press Enter, and for a moment nothing happens. That blank moment is not one slow thing. It is a chain of small waits, each one starting only when the one before it ends. Once you can name the links in the chain, you can see which ones your own HTML controls.

The journey from URL to pixels

A first visit to a page moves through five stages, in this order. The first three are waiting for messages to travel across the network. The last two are work the browser does with what arrived.

From pressing Enter to the first pixelsFive stages in order. Stages 1 to 3 wait on the network: 1, find the server by turning its name into an address; 2, open a connection and secure it; 3, ask for the page and wait for the first bytes. Stages 4 and 5 happen in the browser: 4, read the HTML and build the page's structure; 5, draw the first pixels.Waiting on the networkWork in the browser1Find the servername → address2Open a connectionagree, then secure3Ask and waitrequest → first bytes4Read the HTMLbuild the structure5Drawfirst pixels
From pressing Enter to the first pixelsFive stages in order. Stages 1 to 3 wait on the network: 1, find the server by turning its name into an address; 2, open a connection and secure it; 3, ask for the page and wait for the first bytes. Stages 4 and 5 happen in the browser: 4, read the HTML and build the page's structure; 5, draw the first pixels.Waiting on the networkWork in the browser1Find the servername → address2Open a connectionagree, then secure3Ask and waitrequest → first bytes4Read the HTMLbuild the structure5Drawfirst pixels
A first visit, in five stages: three network waits, then two stages of browser work.
Read this figure as text

Five stages in order. Stages 1 to 3 wait on the network: 1, find the server by turning its name into an address; 2, open a connection and secure it; 3, ask for the page and wait for the first bytes. Stages 4 and 5 happen in the browser: 4, read the HTML and build the page's structure; 5, draw the first pixels.

Two things can make the chain longer. The server can answer "the page is somewhere else", which sends the browser back to the start for a new address. And the page can ask for files that are found only after other files arrive, so their waits line up one behind the other. Later visits can also make it shorter, because answers and files are reused. This lesson walks through the stages one at a time, then looks at each of those cases.

Find the server

The address you typed contains a domain name: the readable name of a site, such as thinkfe.dev. People remember names, but computers find each other by number. Every device on the internet has an IP address (Internet Protocol address), a number such as 203.0.113.10 that tells the network where to deliver data.

So before anything else, the browser must turn the name into a number. It asks the DNS (Domain Name System), the internet's lookup system for names.

The answer is kept in a cache: a stored copy the browser can reuse instead of asking again. That is why a second visit usually skips this stage. A page that loads fonts, images or scripts from other domain names needs one lookup for each different name.

How DNS finds an answer

The browser does not search the internet for the name itself. First it checks its own cache. If the answer is not there, it usually asks the computer's operating system, which keeps a cache of its own. Only if neither knows does the question leave your computer.

It goes to a DNS resolver: a server, usually run by your internet provider or by a public service, that looks names up on your behalf and remembers the answers. When many people use the same resolver, a popular name is usually already in its cache, and you get the answer after a single question and reply.

A resolver with nothing useful in its cache works its way down from the top of the DNS. It asks the servers at the very top which servers know about names that end in .dev. It asks those which servers know about thinkfe.dev. Then it asks one of them. That last server is an authoritative name server: a server that holds a domain's own records, the answers its owner has set.

How a DNS lookup finds an addressA lookup for thinkfe.dev that no cache can answer. 1, the browser checks its own cache, and 2, the operating system's cache: no answer. 3, it asks a DNS resolver, whose cache has no answer either. 4, the resolver asks the servers at the top of the DNS, which reply: try the .dev servers. 5, it asks the .dev servers, which reply: try thinkfe.dev's own servers. 6, it asks thinkfe.dev's authoritative name servers, which reply 203.0.113.10 with a TTL of 300 seconds. The resolver keeps the answer and passes it to the browser. Each step happens only when every cache before it had no answer.BrowserDNS resolverTop of DNS.dev serversthinkfe.devown servers1 own cache: no2 system cache: nothinkfe.dev?3 cache: nothinkfe.dev?try .dev serversthinkfe.dev?try its own serversthinkfe.dev?203.0.113.10 · TTL 300203.0.113.10456
How a DNS lookup finds an addressA lookup for thinkfe.dev that no cache can answer. 1, the browser checks its own cache, and 2, the operating system's cache: no answer. 3, it asks a DNS resolver, whose cache has no answer either. 4, the resolver asks the servers at the top of the DNS, which reply: try the .dev servers. 5, it asks the .dev servers, which reply: try thinkfe.dev's own servers. 6, it asks thinkfe.dev's authoritative name servers, which reply 203.0.113.10 with a TTL of 300 seconds. The resolver keeps the answer and passes it to the browser. Each step happens only when every cache before it had no answer.1Browser's own cacheno answer2Operating system's cacheno answer3DNS resolver's cacheno answer, so it asks:4Top of DNS“try the .dev servers”5.dev servers“try thinkfe.dev's own servers”6thinkfe.dev's own servers203.0.113.10 · TTL 300The resolver keeps the answerand passes it to the browser.
Only when no cache has the answer does the resolver ask three servers in turn. Most lookups stop at a cache.
Read this figure as text

A lookup for thinkfe.dev that no cache can answer. 1, the browser checks its own cache, and 2, the operating system's cache: no answer. 3, it asks a DNS resolver, whose cache has no answer either. 4, the resolver asks the servers at the top of the DNS, which reply: try the .dev servers. 5, it asks the .dev servers, which reply: try thinkfe.dev's own servers. 6, it asks thinkfe.dev's authoritative name servers, which reply 203.0.113.10 with a TTL of 300 seconds. The resolver keeps the answer and passes it to the browser. Each step happens only when every cache before it had no answer.

Every answer carries a TTL (time to live): the number of seconds the answer may be kept before someone has to ask again. If thinkfe.dev's record has a TTL of 300, the resolver may reuse the address for five minutes. Every cache along the way may keep the answer for at most that long; some keep it for less.

Open a connection

Knowing the address is not enough. The browser and the server first agree to talk, and every agreement costs a round trip: one message travelling across the network and the reply coming back. How long a round trip takes depends mostly on distance and on the network, not on how fast the server is.

The browser sets up a connection, an open channel that both sides agree to use. Setting one up takes a handshake, a short exchange of messages before any page data moves.

There are two handshakes. TCP (Transmission Control Protocol) is the set of rules that delivers data reliably and in order; its handshake is three messages and costs about one round trip. The second handshake is for TLS (Transport Layer Security), the layer that encrypts the connection and proves the server is genuine. With TLS 1.3, the current version, that adds one more round trip; older versions needed more.

Almost every site needs that second handshake because it uses HTTPS (HyperText Transfer Protocol Secure): HTTP (HyperText Transfer Protocol), the web's rules for requests and responses, sent over a connection encrypted with TLS. Every address that starts with https:// uses it.

The figure below uses the messages' real names. SYN, SYN-ACK and ACK are TCP's three messages; ClientHello, ServerHello and Finished open the TLS handshake. You do not need to remember the names. Count the round trips.

Four round trips before the first byte of HTMLA first visit over HTTPS, modeled. Round trip 1: the browser asks the DNS resolver where thinkfe.dev is and gets back an IP address. Round trip 2: the TCP handshake, SYN to the server and SYN-ACK back. Round trip 3: the TLS 1.3 handshake, the browser's ACK and ClientHello, then the server's ServerHello through Finished. Round trip 4: the browser's Finished message with GET /, then the first bytes of the HTML. Only then can parsing begin.DNS resolverBrowserServerWhere is thinkfe.dev?203.0.113.10Round trip 1DNSSYNSYN-ACKRound trip 2TCPACK, ClientHelloServerHello … FinishedRound trip 3TLS 1.3Finished, GET /First bytes of the HTMLRound trip 4HTTP
Four round trips before the first byte of HTMLA first visit over HTTPS, modeled. Round trip 1: the browser asks the DNS resolver where thinkfe.dev is and gets back an IP address. Round trip 2: the TCP handshake, SYN to the server and SYN-ACK back. Round trip 3: the TLS 1.3 handshake, the browser's ACK and ClientHello, then the server's ServerHello through Finished. Round trip 4: the browser's Finished message with GET /, then the first bytes of the HTML. Only then can parsing begin.1Round trip 1 · DNS→ Where is thinkfe.dev?← 203.0.113.102Round trip 2 · TCP→ SYN← SYN-ACK3Round trip 3 · TLS 1.3→ ACK, ClientHello← ServerHello … Finished4Round trip 4 · HTTP→ Finished, GET /← First bytes of the HTML4 round trips before parsing can start
On a first visit, four round trips pass before the first byte of HTML can arrive.
Read this figure as text

A first visit over HTTPS, modeled. Round trip 1: the browser asks the DNS resolver where thinkfe.dev is and gets back an IP address. Round trip 2: the TCP handshake, SYN to the server and SYN-ACK back. Round trip 3: the TLS 1.3 handshake, the browser's ACK and ClientHello, then the server's ServerHello through Finished. Round trip 4: the browser's Finished message with GET /, then the first bytes of the HTML. Only then can parsing begin.

Ask and wait

With an encrypted connection open, the browser finally sends its request for the page and waits. The server prepares the HTML and sends the first bytes back. The time from the start of the navigation, when you pressed Enter, to that first byte has a name: TTFB (Time to First Byte). It includes finding the server and opening the connection, not just the server's work.

You can watch these waits on any site. Every browser ships DevTools, developer tools for inspecting pages and their requests. Its network waterfall draws each request as a bar on a shared timeline, so you can see what waited for what. The lab walks you through it.

Explore

Step through a first visit

A first visit to https://thinkfe.dev

Model: first visit, nothing cached, TLS 1.3 full handshake over TCP. Round trips are counted, not timed. Newer setups can merge some steps; lesson 1.5 covers that.

Step 1 of 10Network round trips so far: 0 (modeled, not measured)

A first visit to https://thinkfe.devA sequence diagram with 10 numbered steps. The same steps are listed as text below the controls.DNS resolverBrowserServerWhere is thinkfe.dev?203.0.113.10SYNSYN-ACKACK, ClientHelloServerHello … FinishedFinished, GET /Prepare the HTMLFirst bytes of the HTMLStart parsing

RequestBrowser → DNS resolver

Where is thinkfe.dev?

The browser knows the domain name but not the IP address, so it asks a DNS resolver.

This is a simulation for learning. It sends no network requests, and real timings vary by network and device.

When the answer is "look elsewhere"

Every request names a method: the kind of request it is. GET asks for something, such as a page, and POST sends something, such as a filled-in form. Every response starts with a status code: a three-digit number that says how the request went. 200 means "here it is" and 404 means "not found".

After the status code come the headers: named lines of extra information about the response, such as content-type: text/html, which says that the content that follows is a web page.

A status code from 300 to 399 usually means a redirect: an answer that carries no page to show, only a location header with a different address to ask instead. The browser follows it on its own, so you rarely notice, but it starts a new request. (304 is the exception in that range: it means "the copy you already have is still good", and lesson 1.6 covers it.)

A redirect to the www address
HTTP/1.1 301 Moved Permanently
location: https://www.thinkfe.dev/

The parts that matter: a status code and the new address. Any short note sent with it is not shown.

301 and 308 say the move is permanent, so browsers may remember it. 302 and 307 say it is temporary. 307 and 308 also guarantee that the browser repeats the request with the same method, so a form that was being sent is sent again to the new address instead of turning into a plain GET.

A redirect costs at least one round trip, because the browser has to ask again. When the new address is on another domain name, it usually costs much more: a DNS lookup for the new name and a new connection with both handshakes. Sending thinkfe.dev to www.thinkfe.dev (opens in a new tab), or the other way round, is the most common case.

Explore

Step through a redirect to another name

A link to thinkfe.dev that redirects to www.thinkfe.dev

Model: nothing cached. Server A holds thinkfe.dev and server B holds www.thinkfe.dev. TLS 1.3 over TCP. If both names were on one server whose certificate covers both, HTTP/2 or HTTP/3 might reuse the first connection (lesson 1.5). Round trips are counted, not timed.

Step 1 of 12Network round trips so far: 0 (modeled, not measured)

A link to thinkfe.dev that redirects to www.thinkfe.devA sequence diagram with 12 numbered steps. The same steps are listed as text below the controls.ResolverBrowserServer AServer BWhere is thinkfe.dev?203.0.113.10TCP and TLS handshakesConnection readyGET /301, location: https://www.thinkfe.dev/Where is www.thinkfe.dev?203.0.113.20TCP and TLS handshakesConnection readyGET /First bytes of the page

RequestBrowser → Resolver

Where is thinkfe.dev?

The link points at thinkfe.dev, so the browser asks the DNS resolver for that name first.

This is a simulation for learning. It sends no network requests, and real timings vary by network and device.

Redirects also stack. In our test in Chrome 156, https://www.mozilla.org (opens in a new tab) answered with a 302 to a page in the visitor's language: one more request before the first byte of the real page. Link to the final address wherever you can.

What redirects add before the first byteRound trips before the first byte of the page, modeled with nothing cached and the two names on different servers. Linked to https://www.thinkfe.dev: 4 round trips (DNS; TCP; TLS; GET → page). Linked to thinkfe.dev, redirected to www: 8 round trips (DNS, spent on a redirect; TCP, spent on a redirect; TLS, spent on a redirect; GET → 301, spent on a redirect; DNS; TCP; TLS; GET → page). Typed http://, no automatic upgrade (older browsers): 6 round trips (DNS; TCP, http, spent on a redirect; GET → 301, spent on a redirect; TCP; TLS; GET → page). Typed http://, Chrome upgrades it first: 4 round trips (DNS; TCP; TLS; GET → page).Linked to https://www.thinkfe.dev4 round tripsDNSTCPTLSpageLinked to thinkfe.dev, redirected to www8 round tripsDNSTCPTLS301DNSTCPTLSpageTyped http://, no automatic upgrade (older browsers)6 round tripsDNShttp301TCPTLSpageTyped http://, Chrome upgrades it first4 round tripsDNSTCPTLSpageNeeded for the pageSpent on a redirect
What redirects add before the first byteRound trips before the first byte of the page, modeled with nothing cached and the two names on different servers. Linked to https://www.thinkfe.dev: 4 round trips (DNS; TCP; TLS; GET → page). Linked to thinkfe.dev, redirected to www: 8 round trips (DNS, spent on a redirect; TCP, spent on a redirect; TLS, spent on a redirect; GET → 301, spent on a redirect; DNS; TCP; TLS; GET → page). Typed http://, no automatic upgrade (older browsers): 6 round trips (DNS; TCP, http, spent on a redirect; GET → 301, spent on a redirect; TCP; TLS; GET → page). Typed http://, Chrome upgrades it first: 4 round trips (DNS; TCP; TLS; GET → page).Linked to the final addressDNSTCPTLSpage4 round trips, none on redirectsRedirected to wwwDNSTCPTLS301DNSTCPTLSpage8 round trips, 4 on redirectsTyped http://, no upgradeDNShttp301TCPTLSpage6 round trips, 2 on redirectsTyped http://, Chrome upgradesDNSTCPTLSpage4 round trips, none on redirectsNeeded for the pageSpent on a redirect
One page reached four ways, nothing cached. A redirect to another name repeats the whole chain; Chrome's own upgrade removes the plain-http redirect.
Read this figure as text

Round trips before the first byte of the page, modeled with nothing cached and the two names on different servers. Linked to https://www.thinkfe.dev: 4 round trips (DNS; TCP; TLS; GET → page). Linked to thinkfe.dev, redirected to www: 8 round trips (DNS, spent on a redirect; TCP, spent on a redirect; TLS, spent on a redirect; GET → 301, spent on a redirect; DNS; TCP; TLS; GET → page). Typed http://, no automatic upgrade (older browsers): 6 round trips (DNS; TCP, http, spent on a redirect; GET → 301, spent on a redirect; TCP; TLS; GET → page). Typed http://, Chrome upgrades it first: 4 round trips (DNS; TCP; TLS; GET → page).

Typing an address without https:// used to cost a redirect too. The browser asked over plain http://, which needs a connection of its own, and the server answered with a redirect to https://. That first request travelled unencrypted, so anyone on the network could read it or answer it with a fake page.

Chrome stopped doing that in version 115. For an address typed or linked as http://, it tries https:// first by itself and falls back to plain http:// only if the secure attempt fails. Addresses on your own computer, such as localhost or 127.0.0.1, were not upgraded in our test, so you will not see this while developing locally. DevTools shows this as a 307 that Chrome made itself, with the reason HttpsUpgrades. The site that keeps the browsers' shared list of HTTPS-only sites says Safari upgrades too.

The fallback is the weak spot: an attacker who can make the https:// attempt fail can still push the browser back to plain http://. HSTS (HTTP Strict Transport Security) closes that gap. It is a response header that tells the browser "for the next so many seconds, use only https:// for this site". During that time the browser never falls back: if the secure connection fails, it shows an error instead. It also protects visitors whose browsers do not upgrade by themselves.

An HSTS header sent over HTTPS
strict-transport-security: max-age=31536000; includeSubDomains

max-age is in seconds: 31,536,000 is one year. includeSubDomains covers every name under the site too.

http://www.w3.org typed twice in Chrome 156 with its default settings (our test, 24 October 2026)

First visit

Upgraded by ChromeA 307 with the reason HttpsUpgrades: Chrome tried https:// on its own and it worked. The HTTPS reply carried an HSTS header.

Second visit

Upgraded by HSTSA 307 with the reason HSTS: Chrome uses https:// because the site told it to, and it would not fall back to http://.

http://neverssl.com, a site with no HTTPS, got the same upgrade attempt, which failed, and Chrome fell back to plain http://. Browsers also ship a preload list of sites that are always HTTPS: http://github.com was upgraded with the reason HSTS on our very first visit.

Read the HTML

The browser does not wait for the whole document. It starts parsing as soon as bytes arrive: reading the HTML text and turning it into a structure it can work with. That structure is the DOM (Document Object Model), the browser's tree of the page's elements. CSS gets its own structure, the CSSOM (CSS Object Model), which holds every style rule in a form the browser can look up.

index.html
<!doctype html>
<html lang="en">
  <head>
    <link rel="stylesheet" href="/style.css" />
    <script src="/app.js"></script>
  </head>
  <body>
    <h1>Hello</h1>
  </body>
</html>

Lines 4 and 5 tell the browser it needs two more files before this page can be shown correctly.

Reading this file, the browser discovers that it needs style.css and app.js. It does not have to reach those lines before asking for them: a preload scanner reads ahead in the HTML and starts those downloads early.

Send the head first

The browser can parse HTML as it arrives, but only if the server sends it that way. Many servers build the whole page first, for example after loading products from a database, and send nothing until it is finished. Until then the browser has no bytes, so it cannot discover the stylesheet either.

The head of most pages does not depend on that data. An early flush sends the first part of the HTML, usually the head, as soon as it is ready, and the rest when the page is built. The browser starts parsing, finds the stylesheet and starts downloading it while the server is still working.

server.js (Node.js with Express)
app.get("/", async (request, response) => {
  response.setHeader("content-type", "text/html");
  response.write(`<!doctype html><html><head><link rel="stylesheet" href="/style.css"></head><body>`);
  const products = await loadProducts(); // the slow part
  response.end(`${renderProducts(products)}</body></html>`);
});

The head goes out on line 3, before the slow database call. The rest follows when it is ready.

We checked this in Chrome 156 with a local server that took one second to build the body. When it waited and sent everything at the end, the stylesheet request started after about 1,000 milliseconds. When it sent the head first, the stylesheet request started after a few milliseconds and had arrived long before the body did.

Sending the head firstA model: the server needs 1000 milliseconds to build the page and the stylesheet takes 300 milliseconds to download. Sending everything at the end: the head arrives at 1000 ms, so the stylesheet downloads from 1000 to 1300 ms and the first text appears at 1300 ms. Sending the head first: the head arrives at once, the stylesheet downloads from 0 to 300 ms while the server works, and the first text appears when the body arrives at 1000 ms.Sends everything at the endserver workHTML sentallstyle.cssfirst text 1300 msSends the head firstserver workHTML sentheadbodystyle.cssfirst text 1000 msmilliseconds →
Sending the head firstA model: the server needs 1000 milliseconds to build the page and the stylesheet takes 300 milliseconds to download. Sending everything at the end: the head arrives at 1000 ms, so the stylesheet downloads from 1000 to 1300 ms and the first text appears at 1300 ms. Sending the head first: the head arrives at once, the stylesheet downloads from 0 to 300 ms while the server works, and the first text appears when the body arrives at 1000 ms.Sends everything at the endserver workHTML sentallstyle.cssfirst text 1300 msSends the head firstserver workHTML sentheadbodystyle.cssfirst text 1000 msmilliseconds →
A model, not our measurement: one second of server work and a stylesheet that takes 300 milliseconds. Sending the head first lets the stylesheet download during that second.
Read this figure as text

A model: the server needs 1000 milliseconds to build the page and the stylesheet takes 300 milliseconds to download. Sending everything at the end: the head arrives at 1000 ms, so the stylesheet downloads from 1000 to 1300 ms and the first text appears at 1300 ms. Sending the head first: the head arrives at once, the stylesheet downloads from 0 to 300 ms while the server works, and the first text appears when the body arrives at 1000 ms.

There is a price. The status code and headers go out with the first bytes, so once the head is sent, the server can no longer answer with a redirect or an error code if something fails later. It has to show the error inside the page instead.

What blocks the first paint

Two kinds of file hold back the first paint, in different ways.

A stylesheet is render-blocking: the browser will not draw anything until the stylesheet has arrived and been processed, so you never see the page with the wrong styles. Parsing can carry on while it waits.

A plain script tag is parser-blocking: parsing stops at the tag until the script has downloaded and run, because the script might change the page. A script also waits for stylesheets above it, since it might read styles. So a plain script in the head delays the first paint too: until it has run, nothing after it is parsed, and the browser has nothing more to draw.

Two attributes change that. With defer, the script downloads while parsing continues and runs after parsing finishes, in the order the scripts appear. With async, it downloads while parsing continues and runs as soon as it arrives, in no guaranteed order.

Where parsing pauses for a scriptThree rows share one modeled timeline. No attribute: parsing stops when the script tag is reached, waits while the script downloads and runs, then continues, so parsing finishes last. async: the script downloads while parsing continues, then parsing pauses briefly while it runs as soon as it arrives. defer: the script downloads while parsing continues and runs only after parsing has finished, so parsing never pauses.No attributeparsing pausedasyncdefertime →Parsing HTMLDownloading the scriptRunning the script
Where parsing pauses for a scriptThree rows share one modeled timeline. No attribute: parsing stops when the script tag is reached, waits while the script downloads and runs, then continues, so parsing finishes last. async: the script downloads while parsing continues, then parsing pauses briefly while it runs as soon as it arrives. defer: the script downloads while parsing continues and runs only after parsing has finished, so parsing never pauses.No attributeparsing pausedasyncdefertime →Parsing HTMLDownloading the scriptRunning the script
The same script in three modes. Only defer lets parsing run without a pause.
Read this figure as text

Three rows share one modeled timeline. No attribute: parsing stops when the script tag is reached, waits while the script downloads and runs, then continues, so parsing finishes last. async: the script downloads while parsing continues, then parsing pauses briefly while it runs as soon as it arrives. defer: the script downloads while parsing continues and runs only after parsing has finished, so parsing never pauses.

Three ways to load a script
<script src="/app.js"></script>
<script src="/app.js" defer></script>
<script src="/analytics.js" async></script>
Which script tag should you use?
CriterionNo attributeasyncdefer
Pauses parsing while it downloadsYesNoNo
When it runsWhen parsing reaches it, once it has downloaded and earlier stylesheets are readyAs soon as it has downloadedAfter parsing finishes
Order between scriptsDocument orderNot guaranteedDocument order
Good forRare cases that must run before the rest of the HTMLIndependent scripts, like analyticsMost page scripts

No attribute

Pauses parsing while it downloads
Yes
When it runs
When parsing reaches it, once it has downloaded and earlier stylesheets are ready
Order between scripts
Document order
Good for
Rare cases that must run before the rest of the HTML

async

Pauses parsing while it downloads
No
When it runs
As soon as it has downloaded
Order between scripts
Not guaranteed
Good for
Independent scripts, like analytics

defer

Pauses parsing while it downloads
No
When it runs
After parsing finishes
Order between scripts
Document order
Good for
Most page scripts

For most page scripts, defer starts the download early, keeps parsing moving and keeps order.

Everything the browser must finish before it can draw for the first time is the critical rendering path. The moment it does draw is called FCP (First Contentful Paint): the first text or image appears. Making the critical rendering path shorter makes FCP earlier.

Predict firstQuestion 1 of 3The head has a slow stylesheet, then a plain script tag. What happens to the HTML after the script tag?

Not every tag blocks

Those two rules have exceptions in both directions. Some stylesheets do not block drawing, and some scripts that you might expect to be harmless do block parsing.

A stylesheet's media attribute says when it applies. With media="print", it applies only when the page is printed. The browser still downloads it, but does not wait for it before drawing the screen. In our Chrome 156 test, a print stylesheet that took two seconds did not hold back a page that drew its first text at about 220 milliseconds.

A module script is a script loaded with type="module", which lets it import other scripts. Module scripts are deferred without being asked: they download while parsing continues and run after it finishes. Only async changes that: an async module script runs as soon as it is ready.

An inline script is a script written directly inside the HTML, with no src file to download. It still pauses parsing while it runs, and it still waits for any stylesheet above it that is still loading. defer and async have no effect on it. (An inline script with type="module" is the exception: like other module scripts, it is deferred unless it is marked async.)

Four tags, four behaviours
<link rel="stylesheet" href="/screen.css" />
<link rel="stylesheet" href="/print.css" media="print" />
<script type="module" src="/app.js"></script>
<p>Before</p>
<script>document.title = "Ready";</script>
<p>After</p>

Chrome reports its own decision for every file, and DevTools shows it in the Render-blocking column. We loaded a page with each kind of tag in Chrome 156 and read the label it gave each one:

TagChrome's labelWhy
A stylesheetblockingDrawing waits for styles
A stylesheet with media="print"non-blockingIt does not apply to the screen
A plain script in the headblockingParsing stops until it has run
A script with defer or asyncnon-blockingIt downloads while parsing continues
A module scriptnon-blockingDeferred by default
An imagenon-blockingText can be drawn before the image arrives
Predict firstQuestion 2 of 3In the code above, screen.css takes one second to arrive. When does the inline script run, and how many paragraphs exist when it does?

Files found late: request chains

The preload scanner can only start downloads for files named in the HTML. Some files are named inside other files, so the browser cannot ask for them until the first file has arrived. A file found that way, and everything behind it, forms a request chain: requests that have to happen one after another because each one is found inside the one before.

A stylesheet can load another stylesheet with @import. The browser learns about the second file only when it has downloaded and read the first one. Both block drawing, so their waits add up.

a.css
@import url("/b.css");
p { color: green; }

b.css cannot be requested until a.css has arrived.

In our Chrome 156 test, each stylesheet took 400 milliseconds to arrive. With @import, b.css was requested only at about 415 milliseconds, when a.css had arrived, and the first text appeared at about 830 milliseconds. With both stylesheets linked from the HTML, both requests started within the first 10 milliseconds and the first text appeared at about 420.

A request chain made by @importMeasured in Chrome 156, each stylesheet held for 400 milliseconds by a local server. With @import: index.html from 0 to 4 ms, a.css from 9 to 414 ms, b.css (from a.css) from 416 to 818 ms; first text at 828 ms. Both linked from the HTML: index.html from 0 to 1 ms, a.css from 6 to 414 ms, b.css from 6 to 411 ms; first text at 420 ms.b.css imported by a.cssindex.htmla.cssb.cssfirst text 828 msBoth linked from the HTMLindex.htmla.cssb.cssfirst text 420 msmilliseconds →
A request chain made by @importMeasured in Chrome 156, each stylesheet held for 400 milliseconds by a local server. With @import: index.html from 0 to 4 ms, a.css from 9 to 414 ms, b.css (from a.css) from 416 to 818 ms; first text at 828 ms. Both linked from the HTML: index.html from 0 to 1 ms, a.css from 6 to 414 ms, b.css from 6 to 411 ms; first text at 420 ms.b.css imported by a.cssindex.htmla.cssb.cssfirst text 828 msBoth linked from the HTMLindex.htmla.cssb.cssfirst text 420 msmilliseconds →
The same two stylesheets. Chained with @import, the second waits for the first. Linked from the HTML, they download side by side.
Read this figure as text

Measured in Chrome 156, each stylesheet held for 400 milliseconds by a local server. With @import: index.html from 0 to 4 ms, a.css from 9 to 414 ms, b.css (from a.css) from 416 to 818 ms; first text at 828 ms. Both linked from the HTML: index.html from 0 to 1 ms, a.css from 6 to 414 ms, b.css from 6 to 411 ms; first text at 420 ms.

Fonts make the most common chain. A web font is a font file the page downloads, instead of one already installed on the device. It is named only inside a stylesheet, and the browser asks for it only when some text on the page uses it. So the font waits for the HTML, then the stylesheet, then the browser's discovery that text needs it.

While a web font is loading, browsers hide the text that uses it for a short time, so you do not see it jump from one font to another. In our Chrome 156 test, with a font that never arrived, the text stayed invisible for at least 1.5 seconds and then appeared in a fallback font: the next font in the list, one already on the device. A font that was declared but not used by any text was never requested.

Predict firstQuestion 3 of 3index.html links main.css. main.css imports theme.css, and theme.css declares the web font used by the headline. Nothing is cached. How many files must arrive, one after another, before the headline can show in its font?

The fix is to make chains shorter: link every stylesheet the first paint needs from the HTML, instead of importing it. For files that can only be named inside other files, such as fonts, the HTML can also announce them early with <link rel="preload">, which lesson 2G.2 covers.

Two milestones: DOMContentLoaded and load

The browser marks two moments in every page load with events that your scripts can wait for.

DOMContentLoaded fires when the whole HTML has been parsed and every deferred script, including module scripts, has run. It does not wait for images, and it does not wait for async scripts. Code that needs the whole DOM can start here.

The load event fires later, when everything the page started loading has finished: images, stylesheets, scripts and any pages embedded in it. Images marked loading="lazy", which load only when they come near the visible part of the page, do not hold it back.

When DOMContentLoaded and load fireMeasured in Chrome 156 on one page. The deferred script arrived and ran at 614 ms, and DOMContentLoaded fired at 614 ms. The async script arrived and ran at 1214 ms, after DOMContentLoaded. The image finished at 2013 ms, and the load event fired at 2014 ms. The first text had appeared at 40 ms.defer scriptasync scriptimageDOMContentLoaded614 msload2014 msmilliseconds →DownloadingScript runs
When DOMContentLoaded and load fireMeasured in Chrome 156 on one page. The deferred script arrived and ran at 614 ms, and DOMContentLoaded fired at 614 ms. The async script arrived and ran at 1214 ms, after DOMContentLoaded. The image finished at 2013 ms, and the load event fired at 2014 ms. The first text had appeared at 40 ms.defer scriptasync scriptimageDOMContentLoaded614 msload2014 msmilliseconds →DownloadingScript runs
One page in Chrome 156, our test. The deferred script runs just before DOMContentLoaded; the async script and the image finish long after it.
Read this figure as text

Measured in Chrome 156 on one page. The deferred script arrived and ran at 614 ms, and DOMContentLoaded fired at 614 ms. The async script arrived and ran at 1214 ms, after DOMContentLoaded. The image finished at 2013 ms, and the load event fired at 2014 ms. The first text had appeared at 40 ms.

In that test, a deferred script that took 600 milliseconds to arrive ran at about 614 milliseconds, and DOMContentLoaded fired straight after it. An async script that took 1.2 seconds ran at about 1,214, well after DOMContentLoaded. An image that took 2 seconds kept the load event waiting until about 2,014 milliseconds, although the first text had appeared at 40.

Measuring each wait with Navigation Timing

You do not need to guess where the time went. Navigation Timing is a browser feature that records a timestamp for every stage of the page load, from the redirects to the load event, in milliseconds since the navigation started.

Paste into the Console panel of DevTools, where you can run JavaScript on any page
const [page] = performance.getEntriesByType("navigation");
console.table({
  redirects: page.redirectEnd - page.redirectStart,
  dns: page.domainLookupEnd - page.domainLookupStart,
  connection: page.connectEnd - page.connectStart,
  ttfb: page.responseStart,
  html: page.responseEnd - page.responseStart,
  domContentLoaded: page.domContentLoadedEventStart,
  load: page.loadEventStart,
});

Each line subtracts two timestamps. A stage that did not happen, such as a DNS lookup answered from the cache, shows 0.

Each pair of timestamps belongs to one link in the chain:

Stage in this lessonTimestamps
RedirectsredirectStart to redirectEnd, and redirectCount for how many
Find the serverdomainLookupStart to domainLookupEnd
Open a connectionconnectStart to connectEnd; the TLS part starts at secureConnectionStart
Ask and waitrequestStart to responseStart; responseStart on its own is the TTFB
HTML arrivingresponseStart to responseEnd
The two milestonesdomContentLoadedEventStart, loadEventStart

When a redirect changes the scheme (the http:// or https:// at the start), the domain name or the port (a number that picks which service on the server answers, such as 443 for https://), the browser hides the redirect timestamps and shows them as 0, so one site cannot learn about another. Even http:// to https:// on the same name counts, so redirectCount is 0 after Chrome's own upgrade too. The Network panel still lists the redirect.

The second visit

Everything so far described a first visit. Come back a little later and most of the chain can be skipped.

The DNS answer is still in a cache until its TTL runs out. The connection may still be open, so no handshakes are needed. Files the server allowed the browser to keep can come straight from the browser's cache, and lesson 1.6 shows how a server decides that. An HSTS rule is remembered too, so a typed http:// address goes straight to https:// with no chance of falling back.

The chain on a second visitThe lesson's five stages, twice. First visit: Find the server (DNS lookup), Open a connection (TCP + TLS), Ask and wait (request), Read the HTML (fetch every file), Draw (first paint). Second visit: Find the server skipped (cached answer), Open a connection skipped (reused), Ask and wait runs (request), Read the HTML runs (kept files reused), Draw runs (first paint).First visitSecond visitFind the serverDNS lookupFind the servercached answerOpen a connectionTCP + TLSOpen a connectionreusedAsk and waitrequestAsk and waitrequestRead the HTMLfetch every fileRead the HTMLkept files reusedDrawfirst paintDrawfirst paint
The chain on a second visitThe lesson's five stages, twice. First visit: Find the server (DNS lookup), Open a connection (TCP + TLS), Ask and wait (request), Read the HTML (fetch every file), Draw (first paint). Second visit: Find the server skipped (cached answer), Open a connection skipped (reused), Ask and wait runs (request), Read the HTML runs (kept files reused), Draw runs (first paint).First visitSecond visitFind the serverDNS lookupFind the servercached answerOpen a connectionTCP + TLSOpen a connectionreusedAsk and waitrequestAsk and waitrequestRead the HTMLfetch every fileRead the HTMLkept files reusedDrawfirst paintDrawfirst paint
The same five stages on a second visit. Crossed-out stages were skipped, and files kept by the browser are reused while it reads the HTML.
Read this figure as text

The lesson's five stages, twice. First visit: Find the server (DNS lookup), Open a connection (TCP + TLS), Ask and wait (request), Read the HTML (fetch every file), Draw (first paint). Second visit: Find the server skipped (cached answer), Open a connection skipped (reused), Ask and wait runs (request), Read the HTML runs (kept files reused), Draw runs (first paint).

https://example.com opened twice in one Chrome 156 window (our test, 24 October 2026)

First visit

DNS 7 ms · connection 62 ms · TTFB 103 msA full lookup, TCP and TLS handshakes, then the request.

Second visit

DNS 0 ms · connection 0 ms · TTFB 36 msThe answer was cached and the connection was reused, so only the request was left.

These are one test's numbers on one network. Yours will differ; the zeros are the point.

The Back button can skip even more. Browsers can keep the whole page you just left in memory and show it again at once, without loading anything; lesson 1.6 explains when they may. That is also why the lab asks you to measure in a private window: there, nothing from earlier visits is reused.

Deep dive · optionalWhat senior engineers add

The preload scanner reads only HTML markup. Files that are added by JavaScript, images set as CSS backgrounds and pages built entirely by JavaScript are invisible to it, so those downloads start late. If a file matters for the first paint, put it in the HTML.

You will often hear that the first response should fit in about 14 kilobytes. Treat it as a rule of thumb: the number comes from how much data a new TCP connection may send before waiting for a reply, and real setups vary. The idea that holds is that critical HTML and CSS should arrive early and be small.

Each extra domain name can mean another DNS lookup and another connection with its own handshakes. <link rel="preconnect"> asks the browser to do the lookup and both handshakes early, for a server you know you will need; lesson 2G.2 covers it, and lesson 1.5 covers how newer versions of HTTP combine some of these steps.

A server that is still building the page can send a 103 Early Hints response first. It names files the page will need, so the browser can start fetching them before the real response arrives, which helps even when the server cannot flush its head early. They are only hints: the real response still carries its own headers. Navigation Timing's responseStart then records when the 103 arrived, not the real response. Lesson 2G.2 covers it.

Deep dive · optionalHTTPS everywhere, and pages sent in pieces

Joining the HSTS preload list needs a valid certificate (the file that proves the server is genuine during the TLS handshake), a redirect from http:// to https://, every name under the domain served over HTTPS, and a header with a max-age of at least one year, includeSubDomains and a preload flag. Leaving the list again can take months to reach every browser. The list's own site is blunt about it: "While HSTS is recommended, HSTS preloading is not recommended". With browsers upgrading http:// themselves, preloading adds protection mainly where those upgrades fail.

Turn includeSubDomains on only when every name under the domain works over HTTPS, or visitors will no longer be able to open the names that do not.

Over HTTP/1.1, a server that sends a page in pieces before it knows the total length uses chunked transfer coding: each piece is sent with its own size. HTTP/2 and HTTP/3 split every response into pieces anyway, so they need no such coding.

Tracks your place during this browser session. Sign in to save it to your account.

Next stepLab

See the chain for yourself.

Change where a script runs and watch what it can see, then measure real page loads in your own browser: the waterfall, an HSTS redirect and every stage's timestamps.

Open the lab