---
area: "Learn"
parent: "Live fragments chapter"
url: "https://unpoly.com/learn/lazy-loading"
---
<nav aria-label="Unpoly docs">[All docs](https://unpoly.com/index.md) · [Live fragments chapter](https://unpoly.com/learn/live-fragments.md)</nav>

# Lazy loading content

Unpoly can load parts of a page after the page itself has rendered. A deferred element with an [`[up-defer]`](https://unpoly.com/api/up-defer.md) attribute fetches its content from a separate URL, either right away, when it is scrolled into view, or when your JavaScript decides. Expensive or rarely seen fragments no longer delay the first paint.

<nav aria-label="Contents">

#### Contents

- [Deferring a fragment](#on-insert)
- [Loading when scrolled into view](#on-reveal)
- [Loading from JavaScript](#scripted)
- [Showing a fallback while loading](#pending)
- [Loading multiple fragments from one URL](#loading-multiple-fragments-from-the-same-url)
- [Updating other fragments](#distant)
- [Caching](#caching)
- [Performance considerations](#performance)
- [SEO considerations](#seo)
- [Preventing a deferred element from loading](#events)

</nav>

## Deferring a fragment {#on-insert}

Look for fragments that are expensive to render on the server, but aren't needed immediately. For example, a large navigation menu that only appears once the user clicks a menu icon:

```html
<div id="menu">
  Hundreds of links here
</div>
```

Move the menu's content to its own route, like `/menu`. In the page, leave only a _deferred element_ with an [`[up-defer]`](https://unpoly.com/api/up-defer.md) attribute. An [`[up-href]`](https://unpoly.com/api/up-href.md) attribute says where to load the content from:

```html
<div id="menu" up-defer up-href="/menu">
  Loading...
</div>
```

As soon as the deferred element is inserted into the page, Unpoly requests `/menu`:

```http
GET /menu HTTP/1.1
X-Up-Target: #menu
```

The server responds with a page containing the `#menu` element with its full content:

```html
<div id="menu">
  Hundreds of links here
</div>
```

Unpoly swaps the deferred element with the element from the response. Apart from [hungry elements](https://unpoly.com/learn/hungry-elements.md), other elements in the response are discarded, so the server is free to send a full HTML document.

Note the following:

- The deferred element [targets itself](https://unpoly.com/learn/targeting-fragments.md) by default, so it must have a [derivable target selector](https://unpoly.com/learn/target-derivation.md) like a unique `[id]`.
- The `#menu` element in the response must _not_ have an [`[up-defer]`](https://unpoly.com/api/up-defer.md) attribute, or it would load itself again, forever.
- The deferred element's children (`Loading...`) are shown until the content arrives. See [showing a fallback](#pending) below.

## Loading when scrolled into view {#on-reveal}

Instead of loading right away, a deferred element can wait until it is scrolled into its [viewport](https://unpoly.com/api/up.viewport.md). For this set an `[up-defer="reveal"]` attribute:

```html
<div id="comments" up-defer="reveal" up-href="/posts/5/comments">
  Loading...
</div>
```

A deferred element that is already visible when inserted loads immediately.

To load some pixels before the deferred element becomes visible, set an `[up-intersect-margin]` attribute. A positive margin loads earlier, a negative margin requires the user to scroll into the element.

> #### Note
>
> Safari requires an `[up-defer="reveal"]` element to have a non-zero width and height to track its scrolling position.

## Loading from JavaScript {#scripted}

With an `[up-defer="manual"]` attribute, a deferred element does not load on its own:

```html
<div id="menu" up-defer="manual" up-href="/menu">
  Loading...
</div>
```

Load it whenever you like by passing it to [`up.deferred.load()`](https://unpoly.com/api/up.deferred.load.md). The [compiler](https://unpoly.com/learn/enhancing-elements.md) below loads the menu after two seconds:

```js
up.compiler('#menu[up-defer]', function(element) {
  setTimeout(() => up.deferred.load(element), 2000)
})
```

[`up.deferred.load()`](https://unpoly.com/api/up.deferred.load.md) returns a promise for the [render result](https://unpoly.com/api/up.RenderResult.md), so you can wait for the content to arrive.

## Showing a fallback while loading {#pending}

The initial children of an [`[up-defer]`](https://unpoly.com/api/up-defer.md) element are shown until its content has loaded:

```html
<div id="menu" up-defer up-href="/menu">
  Loading...
</div>
```

While the content is loading, the deferred element is also assigned an [`.up-loading`](https://unpoly.com/api/up-loading.md) class. All [loading state](https://unpoly.com/learn/loading-state.md) techniques for regular links, like [placeholders](https://unpoly.com/learn/placeholders.md) and [previews](https://unpoly.com/learn/previews.md), work with [`[up-defer]`](https://unpoly.com/api/up-defer.md) elements as well.

A slow deferred request also shows the [progress bar](https://unpoly.com/learn/progress-bar.md). To [load quietly](https://unpoly.com/learn/background-requests.md), set an [`[up-background]`](https://unpoly.com/api/up-defer.md#up-background) attribute.

When the server responds with an error code, nothing is rendered and the fallback stays. To show the error, set an [`[up-fail-target]`](https://unpoly.com/learn/error-responses.md#fail-options) attribute.

> #### Note
>
> When the deferred content is already [cached](#caching), the deferred element is replaced immediately and its fallback children are never shown.

## Loading multiple fragments from one URL {#loading-multiple-fragments-from-the-same-url}

Deferred elements may be scattered across the page, but load from the same URL:

```html
<div id="editorial-controls" up-defer up-href="/articles/123/deferred"></div>

... other HTML ...

<div id="analytics-controls" up-defer up-href="/articles/123/deferred"></div>
```

Unpoly [sends a single request](https://unpoly.com/api/X-Up-Target.md#merging) with both targets:

```http
GET /articles/123/deferred HTTP/1.1
X-Up-Target: #editorial-controls, #analytics-controls
```

The response must contain both elements.

## Updating other fragments {#distant}

Instead of replacing itself, a deferred element can target one or [multiple](https://unpoly.com/learn/targeting-fragments.md#multiple) other fragments. For this set an [`[up-target]`](https://unpoly.com/api/up-defer.md#up-target) attribute.

The following deferred element updates `#editorial-controls` and `#analytics-controls` when it is scrolled into view:

```html
<a href="/articles/123/controls" up-defer="reveal" up-target="#editorial-controls, #analytics-controls">
  Load controls
</a>
```

[Infinite scrolling](https://unpoly.com/learn/infinite-scrolling.md) is built on this technique: a link at the end of a list appends the next page to the list, then replaces itself with a link to the page after that.

## Caching {#caching}

### Cached partials are rendered instantly {#cached-partials-are-rendered-instantly}

Unpoly caches [responses to `GET` requests](https://unpoly.com/learn/caching.md) on the client. When deferred content is already cached, it is rendered synchronously and the [`[up-defer]`](https://unpoly.com/api/up-defer.md) element is replaced before the browser paints it.

This means Unpoly caches complete pages, including their lazy-loaded fragments. Navigating back to such a page renders it instantly, without a flash of [fallback state](#pending). The page also stays accessible during [network issues](https://unpoly.com/learn/network-issues.md).

Deferred content rendered from the cache is [revalidated](https://unpoly.com/learn/caching.md#revalidation) unless you also set an [`[up-revalidate=false]`](https://unpoly.com/api/up-follow.md#up-revalidate) attribute.

### Improving cacheability on the server {#server-cache}

Many apps have a server-side cache for HTML that is expensive to render. Pages that mix content shared by all users with content specific to one user are hard to cache. Complex cache keys that capture all the differences duplicate logic and cause frequent cache misses.

For example, the `<article>` below has the same content for all users. Only administrators get buttons to edit or delete the article:

```html
<article>
  <h1>Article title</h1>
  <nav id="controls">
    <% if current_user.admin? %>
      <a href="...">Edit</a>
      <a href="...">Delete</a>
    <% end %>
  </nav>
  <p>Lorem ipsum dolor sit amet ...</p>
</article>
```

The example uses Ruby on Rails, but works similarly with any other backend.

By extracting the admin-only buttons into a deferred fragment, the entire `<article>` element becomes the same for everyone, and can be cached as a whole:

```html
<article>
  <h1>Article title</h1>
  <nav id="controls" up-defer up-href="/articles/123/controls"></nav>
  <p>Lorem ipsum dolor sit amet ...</p>
</article>
```

## Performance considerations {#performance}

Deferring expensive but non-[critical](https://developer.mozilla.org/en-US/docs/Web/Performance/Critical_rendering_path) fragments paints critical content earlier. This generally improves metrics like [First Contentful Paint](https://developer.chrome.com/docs/lighthouse/performance/first-contentful-paint) (FCP).

When lazy-loaded content is inserted later, it may cause [layout shift](https://web.dev/articles/cls) by pushing down subsequent elements in the [flow](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Normal_Flow). This forces the browser to re-layout parts of the page.

Layout shift is rarely a problem when lazy-loaded content appears below the fold, or when it is positioned absolutely and removed from the flow. Giving the deferred element the height of its coming content also avoids it.

## SEO considerations {#seo}

Search engines may not reliably index lazy-loaded content. Avoid lazy loading heavily optimized keywords, or [use Google's URL inspection tool to test your implementation](https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading).

To let crawlers discover and index lazy-loaded content _as a separate URL_, use [`[up-defer]`](https://unpoly.com/api/up-defer.md) on a standard hyperlink:

```html
<a id="menu" up-defer href="/menu">Load menu</a>
```

Since this indexes `/menu` as a separate page, it should render with a full application layout. You can omit the layout when an [`X-Up-Target`](https://unpoly.com/learn/optimizing-responses.md) header is present on the request.

## Preventing a deferred element from loading {#events}

Before a deferred element loads its content, an [`up:deferred:load`](https://unpoly.com/api/up:deferred:load.md) event is emitted on it. Prevent the event to stop the request:

```js
up.on('up:deferred:load', '#menu', function(event) {
  if (!navigator.onLine) event.preventDefault()
})
```

The loading will not be attempted again on its own. You can still load the element later by passing it to [`up.deferred.load()`](https://unpoly.com/api/up.deferred.load.md).

<nav aria-label="Reading order">

- [Previous: Live fragments](https://unpoly.com/learn/live-fragments.md)
- [Next: Infinite scrolling](https://unpoly.com/learn/infinite-scrolling.md)

</nav>

