This repo is a SolidStart port of this TanStack example. It demonstrates the pattern of server-side rendering publicly-cacheable page content, while using <link rel="preload"> tags to accelerate the fetching of non-cacheable user-specific content.
<link rel="preload"> tags allow preloading dynamic page data as soon as the client loads the page's head element and before any script is loaded. This gives performance similar to and sometimes better than streaming the whole page content due to better cache efficiency. See comparison article.
sequenceDiagram
participant Client
participant ClientCache as Client Cache
participant SharedCache as Shared Cache
participant Server
Client->>ClientCache: GET /page
ClientCache->>SharedCache: GET /page
SharedCache-->>ClientCache: Page Content
ClientCache-->>Client: Page Content
Client->>ClientCache: GET /api/dynamic (preload)
ClientCache->>SharedCache: GET /api/dynamic
SharedCache->>Server: GET /api/dynamic
Client->>ClientCache: GET /script.js
ClientCache->>SharedCache: GET /script.js
SharedCache-->>ClientCache: Script Content
ClientCache-->>Client: Script Content
Server-->>SharedCache: /api/dynamic Content
SharedCache-->>ClientCache: /api/dynamic Content
Client->>Client: Execute Script
Client->>ClientCache: GET /api/dynamic (fetch from script)
ClientCache-->>Client: /api/dynamic Content (from cache)
If the server takes a long time to respond to the preloading fetch, and the script ends up fetching the same URL before the preload is finished, the browser does not send a second request. Instead, it waits for the preload to finish and reuses its response. All major browsers conform to this behavior, which the spec describes in opaque terms:
To consume a preloaded resource [...]
- If entry's response is null, then set entry's on response available to onResponseAvailable.
- Otherwise, call onResponseAvailable with entry's response.
This repo contains 2 versions:
- One using classic API routes to get dynamic content, on the branch main, and
- One using server functions to get them, on the branch preload-server-functions.
- The app defines two API routes for getting dynamic user-specific information:
- /api/user fetches user name and profile pic
- /api/post/$postId/like fetches whether the user likes a given post
- Both endpoints:
- use cookies to get the user session,
- use a 2-second setTimeout to simulate slow network loading, and
- are accessed through query wrapper from Solid Router for request deduplication.
- The page's /(layout).tsx inserts a preload tag to the head of the page to preload
/api/userwhen rendered on the server. On the client, it renders the UserInfo component which fetches/api/userreusing the already preloaded content. - Likewise, the page /(layout)/posts/[postId].tsx inserts a preload tag to the head of the page to preload
/api/post/$postId/likewhen rendered on the server. On the client, it renders the UserLike component which fetches/api/post/$postId/likereusing the already preloaded content. - On client-side navigation, dynamic page data is loaded by route loaders, instead of relying on
<link rel="preload">tags. This way, page prefetching on link hover does take into account the dynamic data. - All pages set the Cache-Control header to
public, max-age=600using the HttpHeader component.
-
The component HttpHeader does not set page headers, neither in dev (
npm run dev) nor in production mode (npm run preview). -
When the server is just started, if we visit
/posts/1, the HTML contains twice the preload link tag withrel="preload" href="/api/user"and the preload for/api/post/1/likeis absent. On subsequent requests to/posts/1(for post ID1or any other post ID), the problem clears itself up and both preload link tags (for/api/userand/api/post/1/like) are present in the HTML.
From your terminal:
npm install
npm run devThis starts your app in development mode, rebuilding assets on file changes.
To build the app for production:
npm run build