---
title: "Speed and Caching · RUAL Documentation"
description: "Why web pages are fast: templates that compile once, answers that hold only what changed, ETags, shared caching and links that load before the click."
canonical: https://docs.rual.nl/web/speed
language: en
---

# 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`](https://docs.rual.nl/block-types/web/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-Language` and `Cookie`,** 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-Control`** in the `headers` pin 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](https://docs.rual.nl/web/build-a-shop#templates) 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](https://docs.rual.nl/web/security): The policy and the nonce behind that ETag.

- [Markup Reference](https://docs.rual.nl/web/markup): Triggers, debounce and prefetch attributes.

- [SEO and Sharing](https://docs.rual.nl/web/seo): 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.
