Unpoly lets you load additional fragments when a placeholder enters the DOM or viewport.
By deferring the loading of non-critical fragments with a separate URL, you can paint important content earlier.
Identify fragments that are expensive to render on the server, but aren't immediately required. For example, you may have a large navigation menu that only appears once the user clicks a menu icon:
<div id="menu">
Hundreds of links here
</div>
To remove the menu from the initial render pass, extract its contents to its own route, like /menu
.
In the initial view, only leave a placeholder element and mark it with an [up-defer]
attribute.
Also set an [up-href]
attribute with the URL from which to load the deferred content:
<div id="menu" up-defer up-href="/menu"> <!-- mark-phrase "up-defer" -->
Loading...
</div>
The placeholder content can show a pending state while the full content is loading.
When the [up-defer]
placeholder is rendered, it will immediately make a request to fetch
its content from /menu
:
GET /path HTTP/1.1
X-Up-Target: #menu
Note
By default the placeholder is targeting itself (
#menu
). For this the element must have a derivable target selector.
The server is now expected to respond with a page containing #menu
with content:
<div id="menu">
Hundreds of links here
</div>
The element in the response should no longer have an [up-defer]
attribute. This would cause infinite reloading.
The server is free to send additional elements or even a full HTML document.
Only #menu
will be updated on the page. Other elements from the response will be discarded.
Instead of loading deferred content right away, you may also wait until the placeholder is scrolled into its viewport.
For this set an [up-defer="reveal"]
attribute:
<div id="menu" up-defer="reveal" up-href="/menu"> <!-- mark-phrase "reveal" -->
Loading...
</div>
To shift the load timing to an earlier or later moment, set an [up-intersect-margin]
attribute.
You can use the loading of deferred placeholders to implement infinite scrolling without custom JavaScript.
Note
Safari requires an
[up-defer="reveal"]
element to have a non-zero width and height to be able to track its scrolling position.
By setting an [up-defer="manual"]
attribute, the deferred content will not load on its own:
<div up-defer="manual" id="menu" up-href="/menu">
Loading...
</div>
You can now control the load timing by calling up.deferred.load()
from your own JavaScripts.
The code below uses a compiler to load the fragment after two seconds:
up.compiler('#menu[up-defer]', function(placeholder) {
setTimeout(() => up.deferred.load(placeholder), 2000)
})
By deferring the loading of expensive but non-critical fragments, you can paint critical content earlier. This will generally improve metrics like First Contentful Paint (FCP) and Interaction to Next Paint (INP).
Note, however, that when lazy loaded content is inserted later, it may cause layout shift by pushing down subsequent elements in the flow. This can force a browser to re-layout parts of the page.
Layout shift is rarely a problem if lazy loaded content appears below the fold, or when it has absolute positioning that removes it from the flow.
Search engines may not realiably index lazy loaded content. Avoid lazy loading heavily optimized keywords, or use Google's URL inspection tool to test your implementation.
To allow crawlers to discover and index lazy loaded content as a separate URL, you can use [up-defer]
on a standard hyperlink:
<a id="menu" up-defer href="/menu">load menu</a>
Since this will index /menu
as a separate page, it should render with a full application layout. You can optionally
omit the layout if an X-Up-Target
header is present on the request.
Unpoly caches responses to GET requests on the client. When deferred content is already cached, it is rendered synchronously.
The [up-defer]
placeholder will never appear in the DOM.
This means Unpoly will cache complete pages, including any lazy-loaded fragments. Navigating to such pages will render them instantly, without showing a flash of fallback state. Such pages will also remain accessible in the event of network issues.
Deferred content that is rendered from the cache will be revalidated unless you also set an
[up-revalidate=false]
attribute.
Many apps have a server-side cache with HTML that is expensive to render.
It can be challenging to cache pages that mix content specific to a some users with content shared by many users. While you can use complex cache keys to capture all the differences, this may duplicate logic and cause frequent cache misses. By extracting user-specific fragments into deferred partials, you can improve the cacheability of a larger page or component.
For example, the <article>
below almost has the some content for all users. However, only administrators
get buttons to edit or delete the article:
<article>
<h1>Article title</h1>
<nav id="controls"> <!-- mark-line -->
<% if current_user.admin? %> <!-- mark-line -->
<a href="...">Edit</a> <!-- mark-line -->
<a href="...">Delete</a> <!-- mark-line -->
<% end %> <!-- mark-line -->
</nav> <!-- mark-line -->
<p>Lorem ipsum dolor sit amet ...</p>
</article>
By extracting the admin-only buttons into a separate fragment, we can easily cache the entire <article>
element:
<article>
<h1>Article title</h1>
<nav id="controls" up-defer up-href="/articles/123/controls"></nav> <!-- mark-line -->
<p>Lorem ipsum dolor sit amet ...</p>
</article>
The initial children of an [up-defer]
element are shown while its deferred content is loading:
<div id="menu" up-defer up-href="/menu">
Loading... <!-- mark-phrase "Loading..." -->
</div>
Note
If the deferred content is already cached, the fallback will immediately be replaced by the cached content.
A progress bar will show while deferred content is loading. This can be disabled by setting an [up-background=true]
attribute.
The [up-defer]
placeholder is assigned an .up-active
class while its content is loading.
Deferred placeholders may be scattered throughout the page, but load from the same URL:
<div id="editorial-controls" up-defer up-href="/articles/123/deferred"></div> <!-- mark-phrase "#editorial-controls" -->
... other HTML ...
<div id="analytics-controls" up-defer up-href="/articles/123/deferred"></div> <!-- mark-phrase "#analytics-controls" -->
When loading the deferred content, Unpoly will send a single request with both targets:
GET /articles/123/deferred HTTP/1.1
X-Up-Target: #editorial-controls, #analytics-controls
Instead of replacing itself, a placeholder can target one or multiple other fragments.
To do so, set an [up-target]
attribute matching the elements you want to update.
The following placeholder would update the fragments #editorial-controls
and #analytics-controls
when
the placeholder is scrolled into view:
<a href="/articles/123/controls" up-defer="reveal" up-target="#editorial-controls, #analytics-controls"> <!-- mark-phrase "up-target" -->
load controls
</div>
See infinite scrolling for another example of this technique.
Before a placeholder loads its deferred content, an up:deferred:load
event is emitted.
The event can be prevented to stop the network request.
The loading will not be attempted again, but you can use up.deferred.load()
to manually load afterwards.