Speed and Caching
Web pages are fast because templates compile once, an action sends back only what changed, an unchanged page is a 304, and what the browser can judge never leaves the browser. Set cache seconds on a public page and shared caches do the rest.
Templates compile once
A template is compiled the first time its text is seen, and the compiled form is reused until the text changes. Rendering it reads your data directly. A 50-row product table, with a link, an image, a price and a condition in each row, compares like this on one laptop:
| Engine | One render | Allocations |
|---|---|---|
| Web templates | about 16 µs | 39 |
| The Mustache engine mail templates used | about 43 µs | 1,623 |
| Go's own HTML templates | about 130 µs | 3,274 |
That makes it more than twice as fast as the first and about eight times as fast as the second, while it also escapes each value for its place in the HTML.
An action renders only its answer
Editing a row renders that row, not the page, and sends only that row back. The script that applies it is small too: under 4 KB compressed, cached for good under a name that changes when its contents do.
An unchanged page is a 304
Every page answer carries a weak ETag. A browser that already has the page sends it back, and gets a 304 with no body.
Every response has a fresh nonce for its Content Security Policy, which would make every page look different. So the ETag is computed with the nonce taken out, and the 304 carries no policy: the browser keeps the one that matches the body it already has. This holds for GET and HEAD answers with status 200.
Cache seconds let shared caches keep a page
The cache pin of web_render says how long a shared cache may keep the page, in seconds.
| The request | Cache-Control |
|---|---|
Anonymous, status 200, cache above 0 | public, max-age=N, stale-while-revalidate=N, with Vary: Accept-Language, Cookie |
Anonymous, without cache | no-cache: browsers revalidate with the ETag every time |
A signed-in visitor, whatever cache says | private, no-cache |
The answer to a POST, and any redirect | no-store |
- Never for a signed-in visitor, so a page rendered for one person cannot end up in a shared cache.
- It varies on
Accept-LanguageandCookie, because an anonymous visitor's language can come from either. A site that sets cookies on anonymous visitors gets a copy per cookie value. - A cached copy carries one nonce for everyone who gets it. That is sound: its body is fixed, so nothing injected later can be in that copy.
- Your own
Cache-Controlin theheaderspin wins over all of this.
Keep a page cacheable by leaving personal parts out of it: render the page for everyone, and fetch a cart count or a greeting with a data-trigger="load" action, as the webshop tutorial does.
Links load before the click
Every page carries speculation rules: links on your own site that a visitor hovers are fetched ahead of the click, in browsers that support this. Others ignore it. These links are never fetched ahead:
- a link with
data-action, because it runs an action; - a link with
rel="nofollow"; - a link with
data-no-prefetch.
Mark a link that changes something on a plain GET, such as a sign-out link, with data-no-prefetch. Better yet, make it an action.
Typing costs nothing
Validation, loading states, toggles and clearing errors happen in the browser. A form field does not talk to the server while the visitor types, unless you ask for that with data-trigger="input". Even then, only the last keystroke after the pause sends a request, and an earlier one still running is cancelled.
Next steps
Escaping and Security The policy and the nonce behind that ETag. Markup Reference Triggers, debounce and prefetch attributes. SEO and Sharing The other pins on the render block.Frequently asked
Can RUAL web pages be cached?
Yes. Every page carries an ETag, so an unchanged page answers 304, and setting cache seconds on web_render lets shared caches keep it. A page rendered for a signed-in visitor is never cached publicly.