When Unpoly reloads a fragment that has not changed, your server can answer with an empty 304 Not Modified.
The exchange then costs about 1 KB and no rendering time.
This works for every reload, whether from polling, cache revalidation or up.reload(),
and uses standard HTTP conditional requests.
Supporting conditional requests is optional. Reloading, polling and revalidation also work with a server that always renders the full response.
The first response for a fragment carries a version of its content, as an ETag or Last-Modified header.
Unpoly remembers that version on the rendered fragment.
When it later reloads the fragment, it sends the version back in an If-None-Match or If-Modified-Since request header.
The server compares it with the current version of its data. When nothing changed, it responds with
304 Not Modified and an empty body, and Unpoly finishes the render pass without changes.
The headers of the initial page load are not visible to scripts. To make the first reload of such a fragment
conditional, render its version into an [up-etag] or [up-time] attribute.
Tip
Many web servers add a default
ETagby hashing the response body. With such a server, identical HTML already results in a short304 Not Modifiedresponse, without any changes to your application code. This only works when your pages render identical HTML for unchanged data. Rails, for example, renders a differently masked CSRF token into every response, so the body hash never matches.
An ETag is a string that identifies the rendered content, e.g. a hash of the underlying records
and their last update. The server sends it with the first response:
HTTP/1.1 200 OK
ETag: "x234dff"
<html>
...
<div class="messages">
...
</div>
...
</html>
Note
The double quotes are part of the ETag's format.
After rendering, Unpoly remembers the ETag header in an [up-etag] attribute on the fragment:
<div class="messages" up-etag='"x234dff"'> <!-- mark: up-etag='"x234dff"' -->
...
</div>
When the fragment is reloaded, Unpoly echoes the ETag in an If-None-Match request header:
GET /messages HTTP/1.1
If-None-Match: "x234dff"
The server compares the ETag from the request with the ETag of its current data.
When they match, it skips rendering and responds with an empty 304 Not Modified:
HTTP/1.1 304 Not Modified
When the data has changed, the server renders the new content with a new ETag, as for any other request.
Instead of a hash, the server can send the time when the rendered data was last changed,
as a Last-Modified header:
HTTP/1.1 200 OK
Last-Modified: Wed, 15 Nov 2000 13:11:22 GMT
<html>
<div class="messages">...</div>
</html>
After rendering, Unpoly remembers the time in an [up-time] attribute on the fragment:
<div class="messages" up-time="Wed, 15 Nov 2000 13:11:22 GMT"> <!-- mark: up-time -->
...
</div>
When the fragment is reloaded, Unpoly echoes the time in an If-Modified-Since request header:
GET /messages HTTP/1.1
If-Modified-Since: Wed, 15 Nov 2000 13:11:22 GMT
When no newer data exists, the server responds with an empty 304 Not Modified.
A server can send both Last-Modified and ETag. Unpoly then sends both conditional headers,
and the server should give If-None-Match precedence.
An ETag is the more flexible choice. It is easy to mix additional data into the hash,
like the ID of the signed-in user or the hash of the deployed commit,
so that a change in either produces new content.
Tip
Many frameworks compare versions and send the
304response for you. In Rails, usefresh_whenorstale?. In Django, use the@conditiondecorator. In Symfony and Laravel, call$response->isNotModified($request).
A large response may contain several fragments that are later reloaded individually.
Instead of one version for the whole response, the server can render each fragment
with its own [up-etag] or [up-time] attribute:
<div class="messages" up-time="Wed, 21 Oct 2015 07:28:00 GMT">
<div class="message" id="message1">...</div>
<div class="message" id="message2">...</div>
</div>
<div class="recent-posts" up-time="Thu, 3 Nov 2022 15:35:02 GMT">
<div class="post" id="post1">...</div>
<div class="post" id="post2">...</div>
</div>
When a fragment is reloaded, Unpoly uses the version from the closest
[up-etag] or [up-time] attribute, so a nested element inherits the version of its container:
up.reload('.messages') // If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
up.reload('.recent-posts') // If-Modified-Since: Thu, 3 Nov 2022 15:35:02 GMT
up.reload('#post1') // If-Modified-Since: Thu, 3 Nov 2022 15:35:02 GMT
The server may still send ETag or Last-Modified response headers.
They are only used for fragments without a closer [up-etag] or [up-time] attribute.
To prevent a fragment from inheriting a version from an ancestor,
set an [up-etag=false] or [up-time=false] attribute. The fragment is then reloaded without conditional headers.
A 304 Not Modified is one of three responses that finish a render pass without changes.
The server can send any of them, with an empty body:
X-Up-Target: :none
204 No Content
304 Not Modified
A 304 counts as a successful response, not as an error response, even though its status is not 2xx.
When nothing is rendered, no up:fragment:loaded event is emitted and no { onRendered } callback is called.
The up.render() promise still fulfills, with an empty up.RenderResult.
Loading state is reverted and { onFinished } callbacks run as usual.
To skip a response on the client after it was loaded, see Render lifecycle: Skipping a loaded response.
Tip Your coding agent can search these docs locally with the Unpoly agent skill