Learn Optimizing responses

Your server can shorten or customize its responses by reading the request headers that Unpoly sends, like the targeted selector or the mode of the updating layer. This is optional: Unpoly only takes the targeted fragment from any response and discards the rest, except hungry elements and the <head>, so a full HTML page is always a valid answer.

Rendering only the targeted fragment

Unpoly sends the target selector in an X-Up-Target request header. When a link targets a .menu element, the server sees a request like this:

GET /sitemap HTTP/1.1
X-Up-Target: .menu

The server may answer with a full page and let Unpoly pick out the .menu element. Or it may skip the rest of the page and render only what was asked for. A response that was tailored to the target must list the header in a Vary header, so the cache does not reuse it for other targets:

HTTP/1.1 200 OK
Vary: X-Up-Target

<div class="menu">...</div>

A typical use is to skip expensive parts of the layout, like a sidebar or a navigation bar, when they are not part of the target.

X-Up-Target does not list hungry elements like flashes. They are updated whenever a response happens to contain them.

A request may target multiple fragments. The header then lists all selectors, separated by commas, and the response should contain an element for each:

X-Up-Target: .menu, .breadcrumbs

A request made without a target, like a plain up.request('/sitemap') call, has no X-Up-Target header. Treat a missing header as a request for the full page.

Setting the title without a <head>

A shortened response usually has no <head> and therefore no <title>. To still update the document title, send it as an X-Up-Title response header, encoded as a relaxed JSON string:

HTTP/1.1 200 OK
Vary: X-Up-Target
X-Up-Title: "Sitemap"

<div class="menu">...</div>

A response without a <head> has two more limits: Unpoly keeps the current meta tags, and it cannot detect a new deployment.

Error responses target a different fragment

When the server responds with an error code, Unpoly renders a different target. Form submissions use this to update the <form> with validation errors instead of the success target. The fail target is sent as a separate X-Up-Fail-Target header:

POST /users HTTP/1.1
X-Up-Target: .content
X-Up-Fail-Target: #signup-form

A server that optimizes its responses should render for X-Up-Target on success, and for X-Up-Fail-Target when it responds with an error status like 422.

Rendering different content for overlays

Unpoly sends the mode of the updating layer in an X-Up-Mode request header. A link that opens a drawer overlay produces a request like this:

GET /users/5 HTTP/1.1
X-Up-Mode: drawer
X-Up-Target: main

The server may render a leaner version for overlays, e.g. without the site navigation. As before, the Vary header tells Unpoly that the response depends on the mode:

HTTP/1.1 200 OK
Vary: X-Up-Mode

<main>...</main>

For the root layer the header is X-Up-Mode: root. The mode of the layer that renders an error response is sent as X-Up-Fail-Mode.

Detecting a full page load

Every request made through Unpoly carries an X-Up-Version header with the frontend's version:

X-Up-Version: 3.15.0

A request without this header is a full page load, where the browser renders the entire page. Server-side libraries use this to answer questions about the request safely. For example, unpoly-rails answers up.target?('.sidebar') with true for every request that isn't from Unpoly, so a template never omits content from a full page load.

To omit content from a response, check X-Up-Target instead. It tells you exactly which fragments Unpoly needs.

Rendering content that depends on layer context

A layer can carry a context object, e.g. to reuse a screen in an overlay with a different heading. Unpoly sends it as JSON in an X-Up-Context request header:

X-Up-Context: { "title": "Choose a company contact" }
X-Up-Target: main

A response that depends on the context lists it in its Vary header. See Layer context for the full exchange.

Caching optimized responses

Unpoly caches responses and reuses them for later requests to the same URL. A response that was tailored to a request header must not be reused for a request with a different header value: a response that only contains .menu cannot serve a request for .sidebar.

Request headers that influenced a response should therefore be listed in a Vary response header. This tells Unpoly to partition its cache for that URL, so that each header value gets a separate cache entry:

HTTP/1.1 200 OK
Vary: X-Up-Target, X-Up-Mode

<div class="menu">...</div>

After seeing Vary: X-Up-Target, a cached response for .menu is no longer a cache hit for a request targeting a different selector.

Vary is a standard HTTP header, so it also keeps the browser's HTTP cache and shared caches like CDNs from serving a shortened response to a full page load. Check that your CDN honors Vary for custom headers, or don't let it cache these responses.

You can set the Vary header from your own server code. Some protocol implementations, like unpoly-rails, set it for you once you read a header.

How cache entries are matched

By default cached responses will match all requests to the same URL.

When a response has a Vary header, matching requests must additionally have the same values for all listed headers:

🠦 X-Up-Target: .foo
🠤 Vary: X-Up-Target
🠦 X-Up-Target: .foo ✔️ cache hit
🠦 X-Up-Target: .bar ❌ cache miss
🠦 No X-Up-Target ❌ cache miss

When a response has no Vary header, that response is a cache hit for all requests to the URL, regardless of target:

🠦 X-Up-Target: .foo
🠤 No Vary
🠦 X-Up-Target: .foo ✔️ cache hit
🠦 X-Up-Target: .bar ✔️ cache hit
🠦 No X-Up-Target ✔️ cache hit

Requests can target multiple fragments by separating selectors with a comma. If the server replies with Vary: X-Up-Target, that response is a cache hit for each individual selector:

🠦 X-Up-Target: .foo, .bar
🠤 Vary: X-Up-Target
🠦 X-Up-Target: .foo ✔️ cache hit
🠦 X-Up-Target: .bar ✔️ cache hit
🠦 X-Up-Target: .foo, .bar ✔️ cache hit
🠦 X-Up-Target: .bar, .foo ✔️ cache hit
🠦 X-Up-Target: .baz ❌ cache miss
🠦 X-Up-Target: .foo, .baz ❌ cache miss
🠦 No X-Up-Target ❌ cache miss