Experiments with Browser Preconnects
Executive summary
Modern web browsers employ a suite of performance optimization techniques to improve user experiences. Preconnect hint is one such optimization and allows browsers to discover critical hostnames and proactively establish a connection to them for serving requests in the near future. In this blog post, I discuss some characteristics of connections established via preconnect hints as observed from analyzing several large-scale datasets collected from Akamai’s infrastructure and some in-lab experimentation.
The key findings from this work are:
When browsers establish a pre-connection, the first HTTP request on the connection is often sent a few hundred milliseconds after the connection is established because a request may not be available when preconnections happen. As a result, the browser must spend time to parse the HTML, and other resources to discover a request that could be sent on the connection.
If the time gap between when the connection is established and when the first request is sent is larger than 10 seconds, the browser closes the connection and thus, defeats the purpose of sending preconnect hints. Developers must ensure that preconnect hints are used within the first 10 seconds.
Occasionally, the pre-connections may never be used to send HTTP requests. In such situations, there may be a minimal CPU load on the server infrastructure.
Introduction
Modern web pages utilize dozens of hostnames to download hundreds of resources. For each of these resources, the browser performs a quick lookup into its TCP cache to check whether a connection to the associated hostname already exists, and also whether the connection is available for use. If the TCP connection is not available, the browser performs a lookup into its DNS cache to check if a DNS entry exists for the hostname in question. If both DNS and TCP entries are not available in the cache, the browser performs a DNS lookup and establishes a new TCP connection, followed by a TLS handshake wherever required. When DNS entries and connections are not already available, the page load time could inflate, especially if they are needed to load a resource that lies on the web page's critical path.
To keep these preliminary tasks from happening on the web page's critical path, many web developers make use of preconnect hints that let browsers perform a DNS lookup and establish a TCP/TLS session with the host as soon as the hint is available. A good web development practice sends preconnect hints either in the HTTP response headers of the requested base page HTML, such as
HTTP/1.1 200 OK
Link: <https://www.foundry.systems>; rel=preconnect;
or, as <link rel="preconnect" href="https://www.foundry.systems" /> tags embedded in the HTML.
In the above example, as soon as the above hint is available to a browser that supports preconnect hints, the browser would perform a DNS lookup and establish a connection to www.foundry.systems, even if there was no pending HTTP request.
Receiving preconnect hints is not the only reason why a web browser would preconnect to hostnames. Google Chrome, for example, has a built-in predictor that learns the structure of web pages navigated by the user and performs a speculative preconnect to various hostnames as soon as the user navigates to a page.
For example, if the predictor knows that previous visits of the user to the page https://www.example.com/index.html required resources from img.example.com and css.example.com, the next time the user navigates to the same page, the browser could proactively establish connections to img.example.com and css.example.com before it even discovers resources to be downloaded on these connections. In this blog post, I plan to discuss some characteristics of connections established either via web developer preconnect hints or web browser speculative preconnect hints.
Unused preconnects
In some cases, proactively established connections are not used by browsers to send any HTTP requests. This may happen because of any of the four scenarios mentioned below:
The predictor suggests opening a connection to a host based on the user’s previous navigation, but the web page has changed and does not require any resource from the proactively connected hostname.
An HTTP request was canceled and the established connection remains unused.
A request was ready to be sent and the browser started to establish a connection for it, but before the connection establishment was completed, some other connection to the same host became available and the request was transferred to that connection.
A browser may not remember that a server is HTTP/2-capable and thus it opens multiple parallel connections in an HTTP/1.1 fashion but only uses one of the connections after negotiating HTTP/2.
The experiment
Given the above scenarios, I next investigated how Chrome (Version 64) treats such connections when they remain idle for some time. For experimental purposes, I set up three test pages to instruct the browser to preconnect to a host and load a resource on that host after different intervals.
#1
In the first test page, https://dev.utkarshgoel.in/preconnect.html, I added a preconnect hint in the HTML <head> tag to connect to an HTTP/2 capable host www.foundry.systems. Note that this page has nothing else in the HTML. Running a Wireshark instance in the background as I load the page shows that Chrome established a TCP and TLS handshake to www.foundry.systems. I also ran a capture at chrome://net-internals/#http2 in the background. However, the connection did not get registered as an HTTP/2 connection in the net-internals, and net-internals does not show a SETTINGS frame being sent on the connection.
#2
In the second test page, https://dev.utkarshgoel.in/preconnect_with_delayed_request.html, in the <head> tag, I added a preconnect hint for www.foundry.systems along with an external JS that blocks execution of any other JS on the page for five seconds. Inside the body of the HTML, I added an img tag with an empty src attribute. The HTML then has an inline JS that sets the src attribute of the image to an image pointing to www.foundry.systems. The purpose of this experiment is to load a resource from www.foundry.systems five seconds after the connection has been established.
For this experiment, I see in the Wireshark capture that similarly to the previous experiment, Chrome establishes a TCP and a TLS session to www.foundry.systems, but this time the net-internals registers the connection as an HTTP/2 connection. However, the connections show up in net-internals five seconds after establishment, at which time the SETTINGS frame is sent.
This, and the previous experiment show that Chrome sends the HTTP/2 SETTINGS frame (as this marks the start of an HTTP/2 connection) only once there is an HTTP request to be sent out on the connection.
#3
In the third test page, https://dev.utkarshgoel.in/preconnect_with_delayed_request_12s.html, I cloned the second test page and modified the external JS to block the execution of any other JS for 12 seconds. Similar to the second experiment, I observed that the connection in net-internals was registered after 12 seconds. However, in the wireshark capture, I observed two connections being established, instead of just one. As shown in the screenshot below, the two connections were about 12 seconds apart.
After loading the test page with different blocking values for the external JS, I found that Chrome discards any connection state where a connection is not used within the first 10 seconds after its establishment. In my experiment, the inline JS loaded the image 12 seconds after the preconnect, so Chrome established a new connection because the 10-second limit had been exceeded.
Therefore, one recommendation here is to ensure that when preconnect hints are advertised with the goal of eliminating DNS and TCP/TLS handshakes from the web page critical path, the browser must be able to discover a resource that needs that connection within 10 seconds.
Another observation I made in this experiment was that even when the server sent a TLS session ticket for when the client connected to the server the first time, the client did not advertise the session ticket in its clientHello when it connected the server the second time. To observe this behavior, take a look at the second red box in the screenshot below, highlighting the size of session tickets advertised in the clientHello.
#4
The above behavior motivated me to set up my fourth experiment, where in the test page (https://dev.utkarshgoel.in/preconnect_with_repeated_delayed_requests.html) I cloned the third test page and added an external JS and an inline JS in the body of the HTML. The purpose of the second external JS was to block the execution of the second inline JS for an additional 70 seconds since that’s how long I discovered Chrome takes to terminate the previous HTTP/2 connections via net-internals. To summarize, the flow of the page load will be:
HTML load -> Preconnect -> wait 12 seconds -> Reconnect -> load image -> wait 70+ seconds -> Reconnect -> load image
As shown in the above screenshot, running such an experiment results in establishing three connections. In the Wireshark capture, I see that Chrome advertises the session ticket only in the third clientHello (as indicated in the red box). This indicates that session tickets are pulled from the buffer/passed to the upper layer only if an HTTP request was sent on the connection the last time.
Amount of traffic generated by unused connections
After learning about the above characteristics of the connections opened proactively by Chrome, I was interested in finding out how often servers receive connection requests that don’t get used for serving any HTTP requests. This is important because if browsers open too many such connections, they might be putting too much load on the servers.
For every TLS session, servers have to perform CPU-intensive public key cryptography, whether requests are served or not. To find an answer to the above question, I studied stats of over 1.7 million TCP connections established to Akamai’s distributed infrastructure for content delivery. I found out that up to 6% of the TLS connections are never used for HTTP requests.
Used preconnects
While there are a few cases where proactively established connections don’t get used, in most cases, such connections are used for serving HTTP requests. However, given that preconnects happen early in the page navigation and it might take some time before the browser could discover a request to be sent on the connection, I was also interested in investigating the time-gap between when the connection establishment finishes and when the first HTTP request arrives on the server. This time-gap will tell us the time for which proactively established connections remain idle right after they are established.
The experiment
For this analysis, I utilized over 500 Akamai edge servers to collect stats of over 7.3 million HTTP requests generated by Chrome browsers over HTTP/2 connections. The x-axis in Figure 1 shows time-gap distribution observed across 200 hostnames. The y-axis shows the time-gap in milliseconds.
As shown in the figure, on a proactively established connection the first HTTP request could arrive on the server as late as four seconds in the median case after the connection establishment. However, for most hostnames, the first request arrives about 50 milliseconds after the end of connection establishment. Such large time-gaps are equivalent to several round trips in wired broadband networks and fewer round trips in fast mobile networks. Additionally, I discovered that this behavior applies only to hostnames associated to subresources embedded in the HTML.
Further, since the HTTP/2 protocol does not allow the server to push anything before the client makes its first request on the connection, the servers lack the ability to act upon such time-gaps in favor of improving performance. Theoretically, one could apply the experimental unbound-server-push proposal to push critical resources while the connection is idle. However, as shown in the previous section, Chrome is not reading incoming data on these idle connections, so this technique cannot be used without a change to how Chrome handles network sockets.
TL;DR
While there are many minor and major findings from this study, I’ll highlight a few that I think are most important for web developers:
Since preconnect hints are advertised to remove time-consuming DNS lookups and TCP/TLS handshakes from the web page critical path, when developing web pages we should ensure that such connections are used within the first 10 seconds. Simply put, ensure that the website does not have a JS that may prevent the browser from discovering resources that need those pre-connections. Additionally or alternatively, make sure to compare your website performance with and without preconnect hints to verify that the hints do not hurt the performance.
There is an additional CPU load incurred on the servers from unused connections. In this blog post, I have discussed one approach to reduce this load.
For connections established to most hostnames that are associated to subresources on the HTML, the connection remains idle for about 50 milliseconds after it is established. The experimental unbound-server-push proposal submitted to the IETF might be one way to make use of the times during which the connections remain idle.
Acknowledgments
Thanks to Akamai’s Mike Bishop, Moritz Steiner, and Stephen Ludin for brainstorming some research ideas and providing feedback on an early version of this blog post.