Table of contents

When Pre-Loading Beats Streaming: The Caching Advantage

Introduction

Web pages are often composed of:

Recently released JavaScript frameworks, and the pioneering JS framework Marko, optimize page loading by progressively streaming different parts of the page to the client. Although this kind of streaming is an effective optimization, I argue in this article that we should consider it only after taking care of a more important optimization: Caching.

In my article, How to make fast web frontends, I classified optimization techniques broadly into two categories:

Although relying on HTTP response streaming allows the server to start fetching resources earlier than pre-loading, it is not a perfect solution: Streaming all page parts together is a form of bundling and therefore hinders caching: The highly cacheable semi-static page parts cannot benefit from the HTTP cache as they are bundled with the dynamic parts in a single URL.

In this article, I compare the performance of page loading when the full page is streamed to the client versus when dynamic page parts are pre-loaded as separate resources. Using diagrams generated by simulation, I show that both Full-Page Streaming and Split-Page Pre-Loading can achieve similar performance with the latter being more effective at reducing overall work thanks to better compatibility with caching.

Table of contents

Simulation settings

For the remainder of the article, I’ll be showing timeline charts (or Gantt charts) of the page loading of multiple versions of the same web page. These charts were generated by simulating the client, the server, the network, and the database holding the page data.

The page of interest is composed of 2 parts: One semi-static part which is cacheable, and a dynamic part which is not. The page also loads a script file. The page is considered completely loaded once both page parts are loaded, the script is loaded and executed, and both page parts are hydrated (if needed).

The simulation uses the parameters:

ParameterValue
Database Query Duration50 milliseconds
Database Query Response Size25 KB
Render Data To HTML Duration50 milliseconds
Render From HTML Duration50 milliseconds
Render From JSON Duration100 milliseconds
Execute Script Duration250 milliseconds
Hydration Duration50 milliseconds
Request Size250 Bytes
Head Size1 KB
Semi-Static HTML Part Size25 KB
Dynamic HTML Part Size25 KB
Script Size250 KB
Dynamic JSON Data Size25 KB
Client To Server Network Latency200 milliseconds
Client To Server Network Bandwidth2.5 MB/s
Client To Edge Network Latency50 milliseconds
Client To Edge Network Bandwidth2.5 MB/s
Edge To Server Network Latency150 milliseconds
Edge To Server Network Bandwidth10 MB/s

Additionally, the simulation assumes that:

You can generate timeline charts with different parameters by visiting the simulation playground.

The baseline pages to compare: Full-Page Streaming vs Split-Page with Pre-Loading

Let’s see the page loading timeline diagrams for two versions of our web page:

In the first round of simulation, which omits caching, both full-page and split-page achieved identical First Contentful Paint latency, with the former completely loading 50ms earlier than the latter.

Full-Page Streaming without caching

Full-Page Streaming version: Notice how the server sends requests to get both the semi-static and the dynamic page parts as soon as it receives the full-page request (at T=200ms). The page is fully loaded at T=1249ms.

Split-Page with Pre-loading without caching

Split-Page with Pre-loading version: When the server receives the request for the page, it only fetches the semi-static page part at T=200ms. As for the dynamic page part, it is fetched by a separate client request which the server starts processing at T=600ms (400ms later than in the streamed full-page version). That said, the full page loading finishes only 50ms later in this particular example (at T=1309ms).

Thanks to pre-loading, the client requests the dynamic page part as soon as it receives the page's head element (at T=400ms - twice the network's client to server latency). Without pre-loading, the dynamic page part wouldn't be requested by the client until the script is loaded and executed, which delays full page loading until T=1718ms.

The effect of server-side and edge caching

Now let’s add caching at two levels:

Both the full-page and the split-page versions benefit from caching on the server and the edge. Their page load times improved by 290ms and 610ms respectively.

The split-page version benefited more from caching. Compared to the full-page version, it got a 300ms earlier First Contentful Paint and a 260ms earlier page load.

Full-Page Streaming with server and edge caching

Full-Page Streaming with server and edge caching: Thanks to server-side caching of the semi-static page part, the First Contentful Paint arrives earlier than without caching (at T=461ms instead of T=569ms). And thanks to the edge, the script file is loaded with reduced latency. The page loads fully at T=959ms.

Split-Page with Pre-loading with server and edge caching

Preloaded Split-Page with server and edge caching: The semi-static page part is delivered from the edge with very reduced latency, leading to a First Contentful Paint as soon as T=160ms (300ms earlier than the full-page version) and a full page load at T=699ms (260ms faster than the full-page version, and 610ms faster than the split-page version without caching).

Pre-loading significantly impacts performance. Without it, the split-page takes 421ms longer to fully load (T=1120ms) even with caching enabled.

Assembling the Full-Page version on the edge for better caching

As we saw in the previous section, the streamed full-page version cannot take full advantage of edge caching: each page request must reach the origin server, which re-sends the otherwise cacheable semi-static page part to the client every time.

It is possible to address this problem with edge-side page assembly, which involves caching semi-static parts at the edge and streaming them to the client as dynamic parts are fetched from the origin server.

Edge-side page assembly has some drawbacks:

Full-Page Streaming with edge page assembly

Full-Page Streaming with edge page assembly: Thanks to edge-side page assembly, the semi-static page part is now delivered from the edge, leading to a First Contentful Paint at T=160ms and a full page load at T=699ms (identical to the split-page version with edge caching example).

Page loading from a returning user with warm client cache

Lastly, let’s examine a less representative but still interesting scenario: how fast each page loads for returning users who have fresh cached resources in their browser cache.

Full-Page Streaming with edge and client caching (returning user)

Full-Page Streaming for a returning user: The client requests full-page which is not cacheable, and receives it very rapidly (because of edge-side page assembly from the previous section). First Contentful Paint is delayed until T=400ms because the script started executing before the page's semi-static part arrived. The page is fully loaded at T=621ms.

Split-Page with Pre-loading with server, edge, and client caching (returning user)

Pre-loaded Split-Page for a returning user: The semi-static page part is immediately available from the browser cache. Thanks to this, the First Contentful Paint arrives at T=50ms (350ms earlier than the full-page version) and the page is fully loaded at T=571ms (50ms earlier).

Conclusion

In this article, we explored the performance trade-offs between two approaches for loading mixed semi-static and dynamic web pages:

In our simulation, we observed that:

Recent JavaScript frameworks make Full-Page Streaming easy, which works well for dynamic content but hinders edge and browser caching of semi-static content. Split-Page with Pre-loading avoids this problem and can be implemented without framework support. That said, framework support is needed to use this pattern alongside DX-enhancing features like server functions/actions in various frameworks, which abstract away endpoint creation and invocation. Among mainstream JS frameworks, Astro’s server islands implement the Split-Page approach with arguably the best developer experience.