Rendering
This page follows the main scenarios and describes where each step gets its content, and why Gemkit works that way.
Participants
| Name | Description |
|---|---|
| layout / page | Elements in src/pages, <gemkit-outlet> matches and renders them from the root route down. |
load option |
Isomorphic data loading, runs on the server and on client navigation. |
*.server.ts |
Server-only load, actions and paths; the client gets the result from a data file. |
| Server element | An element marked with @server(), renders HTML only; the client neither loads nor hydrates it. |
| Lazy element | An element marked with @lazy(), shows server HTML and hydrates when its code is loaded on demand. |
__data.json |
<page path>/__data.json, the server data of the page and all layouts. |
__elements.json |
<page path>/__elements.json, the HTML of server and lazy elements, only when prerendering. |
POST /__gemkit_element |
Renders elements on demand; elements in the same round share one request. |
Deployment
Development (rsbuild dev) |
gemkit start |
Static host | |
|---|---|---|---|
| Page HTML | Rendered per request | Prerendered file first, otherwise rendered per request | Prerendered pages only |
__data.json |
Generated per request | Prerendered file first | Prerendered pages only |
__elements.json |
Not requested by the client | Only for prerendered pages, otherwise {} |
Prerendered pages only |
POST /__gemkit_element |
Available | Available | Not available |
Form actions |
Available | Available | Not available |
- Requests with a query:
gemkit startdoesn't use the prerendered file, becauseloadmay readurl.searchParamsand the result differs from prerendering. - Which pages are prerendered: no
prerender = false,handledidn't readrequestorcookies, and no redirect or error. Personal content can't be written to a static file.loadmust not read the request or cookies either; exportprerender = falsein that case. - Development and
prerender: false: no__elements.jsonis generated, and the client requests elements from the server. This is fixed at build time (process.env.GEMKIT_PRERENDER), so the client skips a request that would always be empty.
First load (server rendering and prerendering)
Prerendering runs the same render for every path at build time and writes the files.
- Run
handle: runshandlefromsrc/app.server.ts, for example to read the current user.- On redirect, respond with the redirect.
- On error, the error page is rendered at the position of the first route with a server module. The client requests data for the same route, so both sides show the same result.
- Render the app:
<gemkit-outlet>matches routes from the root, running layout and pageload. Server data calls*.server.tsdirectly, without a data file. - Wait before outputting the document: output starts after the page data and title are known.
- The status code (404, error) and
<title>and<meta>are in the document head, so they must be known first. - A redirect in
loadoutputs nothing and responds with the redirect.
- The status code (404, error) and
- Stream the rest: later async content, such as
@server()methods, streams without blocking the content before it. - Output the data: layout and page data (
<script type="application/json">) and stores updated on the server are output at the end. Hydration uses them without a request. - Server elements: server element and lazy element HTML is output in the page with the
ssr-serverattribute.- When prerendering, it is also collected into
__elements.json. - On client navigation the elements are created on the client and have no server content, so they get it from this file.
- When prerendering, it is also collected into
Hydration
- Prepare the routes: the client entry sets the routes before defining
<gemkit-outlet>.- The server
<gemkit-outlet>upgrades as soon as it is defined, so the routes must exist first.
- The server
- Render the routes: routes load layout and page modules.
loadtakes the data output in the page (takeSSRData) and sends no request.- A route that failed on the server renders the error page, matching the server result.
- The first load data is also written to the history cache, see Back and forward.
- Hydrate elements: normal elements reuse the server nodes and bind events.
- Elements with restored server state skip
@server()methods in the first effects, because they already ran on the server.
- Elements with restored server state skip
- Server elements: keep the server content and don't hydrate.
- Lazy elements: keep the server content and don't load the code yet. Load and hydrate by the
onoption, the default is on user interaction.- The code is loaded on the first interaction (
pointerdown,click,focusin,keydown,input). - After loading, the element upgrades, hydrates and replays the triggering event on the original target.
- The reason is that rarely used parts, such as a like button, don't need to be downloaded at first.
- The code is loaded on the first interaction (
- Scroll position: keep the position restored by the browser, for example after a reload.
Client navigation to a new history entry
Clicking a <gem-link>, calling history.push or replace creates a new history entry and always gets
fresh content.
Prefetch: starts when the link is hovered or focused, or at navigation time if not prefetched yet. Prefetch includes:
- the layout and page modules of the target route
__data.json, only when needed. If every route with a server module is a reusable layout, it is not requested.__elements.json, only when prerendering is enabled. It is always requested because the client only knows whether the page has server elements after rendering.
A prefetch result is used once by a navigation within 30 seconds. This lets modules, data and elements load in parallel instead of waiting for rendering to reach them.
Render routes layer by layer: the outlet matches from the root. After a layout renders, its inner outlet matches the child route.
- When leaving a layout, the outlet that is about to be removed also reacts to the route change, but it can't match the current path, so it doesn't load. Otherwise it would load the 404 page and overwrite the title.
Layout data: when navigating between child routes, reuse the rendered layout data if its params didn't change. After a form submission, don't reuse.
Page data: layouts and the page share one
__data.jsonrequest, preferring the prefetched result.loadreceives the server data asdata. Withoutload, the server data is used directly.- A redirect in the data file replaces the current navigation.
- An error route in the data file renders the error page at that route, and other route data can still be used.
Title and meta: the title is updated after the page loads. Layout and page meta are merged into
<head>.Page element: when params change (for example
/blog/ato/blog/b), the page element is recreated. It is reused when only the query changes or after a form submission reloads the page.- Element state, such as data from
@mounted()or input values, belongs to the previous page. Reusing the element with new params would show the wrong content. The query is usually a filter or page number of the same page, and a form submission also stays on the same page, so keeping the state is useful. - Implementation: params select a different wrapper template, and lit doesn't reuse elements across different templates. The two templates differ (the second has an extra empty interpolation), because HMR in development treats identical templates as the same one. The server and hydration always use the first one, matching the SSR output.
- Element state, such as data from
Get server element HTML: after the page renders, new server and lazy elements get their HTML in this order:
- the current history entry cache, hit on back and forward
__elements.json, when prerenderedPOST /__gemkit_element, elements in the same round share one request
If a lazy element's code has already been loaded, it is a normal element and renders on the client without fetching HTML. After a lazy element gets its server content, the code loads by the
onoption as on the first load.Scroll: scroll to the top, unless the URL has a hash, which is left to the anchor.
Back and forward
The state of each history entry when leaving is cached in memory by key, keeping the latest 50 entries.
- Cached content: layout and page data, including first load data and client
loadresults, and the HTML of server and lazy elements. - Back and forward: use the cache directly, no prefetch and no request.
- New navigation:
pushandreplacecreate new keys, so they never hit the cache. - After a form submission: clear the whole cache. The submission may have changed the login state, so previous content can't be used.
- Scroll position: restore the position from when the entry was left. Wait for server element HTML
to fill in first, otherwise the page is too short and the position is clipped.
- Wait for the current task to finish first. Reused elements, such as a page element with only a query
change, update in a microtask, and
MutationObservercallbacks also run in microtasks. Only after that have all elements that need fetching started fetching.
- Wait for the current task to finish first. Reused elements, such as a page element with only a query
change, update in a microtask, and
The reason: going back means returning to the state you saw, not getting fresh data. Browser back-forward caches work the same way, so no request means identical content and the best speed.
The first load server element HTML is in the DOM and not written to the cache, so going back to the first page fetches those elements once more.
Query and hash changes
- Query change: creates a new history entry and reloads the page data. The data file URL includes the query.
- Hash only: doesn't re-render routes or load data.
Form submission (actions)
- With JS:
submitActionposts the form to<page path>?/<name>.- The form has
aria-busyduring the request. - After submission, clear prefetch results and the history cache, and layouts don't reuse data.
- On success, re-render the current page and keep the scroll position, since it is the same entry.
- On redirect, navigate to the target on the client.
- On error, render the error page at the page position.
- The form has
- Without JS: native form submission.
- On success or redirect, respond
303and redirect to the target or the current page. - On error, respond with the error page.
- On success or redirect, respond
Redirects and errors
| Source | Server rendering | Client navigation |
|---|---|---|
handle redirect |
Respond with the redirect | Data or element response contains the location, replace navigation |
load redirect |
Respond with the redirect and no content | Data file contains the location, replace navigation |
error() / notFound() |
Error page at the failing route, response uses its status | Error page at the same position |
| No matched route | Error page with status 404 | Error page |
On a static host, redirects are written as <meta http-equiv="refresh"> pages and a redirect in the
data file.
Switch language
Language links are normal <a href> links, switching language reloads the whole page.
The base path, language pack and all content change, so a reload is the simplest and most consistent behavior, and it matches the server-rendered result.
Content sources
Look from top to bottom and stop at the first hit.
Layout and page data (loadData)
- Data of an already rendered layout: layouts only, when params didn't change and no form was submitted.
- The current history entry cache: hit on back and forward.
- Data output in the page: on first load hydration.
- Prefetched
__data.json. - Request
__data.json.
Server and lazy element HTML (loadServerElement)
- The current history entry cache: hit on back and forward.
__elements.json: requested when prerendering, may be prefetched.POST /__gemkit_element.
Trade-offs
- Layout
loadruns again on the server: when navigating below the same layout,__data.jsonstill contains layout data and the server runs the layoutloadagain. A data file must be prerenderable as a static file, so its content can't depend on client state. - Two
handlecalls for dynamically rendered pages: on client navigation,__data.jsonandPOST /__gemkit_elementare two requests, each runshandle. - Empty request with prerendering: when prerendering is enabled, non-prerendered pages also request
__elements.json, receive{}and thenPOST. The client doesn't know which pages were prerendered.
