Optimistic Navigation: Boost Perceived Performance

Slow websites drive users away. Even well-optimized sites can suffer from sluggish navigation. Such behavior is often due to poor internet connections or server overloads. Achieving consistently fast navigation can be challenging. While optimizations like prefetching or AI-driven prerendering can improve the navigation experience, they are often inconsistent and resource-intensive. Optimistic navigation goes completely the opposite way and, instead of preloading, creates the illusion that content is already loaded.

Motivation

My goal was to create a site with consistently fast navigation, regardless of the user's internet speed or CPU performance. Prefetching almost fits as a solution, but it lacks consistency; if a user clicks the link before the content has preloaded, they will experience a delay. Besides, prefetching at scale results in significant costs—both financial and environmental.

Vercel Hosting Cost
100K - 5M page views per month
Carbon Footprint
100K - 5M page views per month

Without prefetching, you can scale up to 1 million views per month and pay only $20, while with prefetching you would pay around $520.

The Problem: Slow Navigation and Unresponsiveness

Picture this: you click a link and await one second for a server response; the UI is frozen, and the website feels broken. To make this article clearer, I made a simple demo app that imitates such navigation.

In the demo app below, try clicking the links to experience the delay in navigation. The app includes two links: one to the 'Lorem Ipsum' article and another to the 'Home' page.

The demo app above imitates the default behavior of meta-frameworks during client (soft) navigation: the interface is frozen, waiting for the async function that loads data (getServerSideProps/loader).

Solution: Client-side prediction

To address the problem of sluggish navigation, we can borrow techniques from other fields, like gaming. Client-side prediction is a technique that enhances gameplay by letting the client respond instantly to inputs, reducing the feel of lag. While it causes a temporary desync between client and server, smoothing techniques mitigate the delay, balancing responsiveness and accuracy (first time used in Duke Nukem 3D).

In web apps, client-side predictions can be used within optimistic UI updates. Here's a scenario:

  1. User clicks a link or button (e.g., "Like").
  2. The UI instantly updates with a predicted outcome (e.g., Like count increases by 1).
  3. A request is sent to the server in the background.
  4. The UI syncs with the server response, adjusting if necessary.

Use it only if you are at least 97% sure that prediction will succeed and the server won't return an error. Don't use it with sensitive or rapidly changing data. When implementing optimistic UI updates, adhere to these guidelines:

  • Synchronize with Server Data: Update the UI to reflect the actual server response.
  • Handle Errors Gracefully: If the server returns an error, revert the UI to its previous state or display an appropriate error message.

Performance illusion

You can achieve the illusion of instant navigation by rendering the opened page optimistically and filling the entire viewport with predicted data. It allows users to interact with the page immediately; for example, adding a product to the cart or simply scrolling and reading content. I achieved such an illusion in that Tanstack-Start starter; reduce your internet speed to 3G (via network throttling in dev tools) and test it yourself.

That technique hides loading sections by putting them deeper into page view. In most cases, users never see loading sections unless they scroll very quickly. Even if they do notice, engaging with predicted content beats staring at a blank screen. The demo app below shows optimistic navigation in action.

Optimistic navigation is like a waiter serving you an aperitivo while the main course cooks.

  • Traditional Navigation: Click → Wait → Render.
  • Prefetching: Preload → Click → Render.
  • Optimistic Navigation: Click → Render Predicted Data → Sync with Server.

Recycling Data: Utilizing What’s Already There

In the demo app above, clicking on an article card takes you to the article page, which displays the same thumbnail, title, and description. From a data perspective, on the home page we make a request for a list of articles, and on the article page we request the article detail object.

Typically, a query for a list of entities returns partial data, while a query for an entity object returns its full data. Use the data from the list of entities to render the detail page optimistically. The more data you get in the entity list, the more content can be rendered instantly.

Optimistic navigation doesn’t speed up the server but keeps users engaged, which may reduce bounce rates. A well-designed optimistic navigation can hide 2-10 seconds of network lag from the user, depending on how fast they are scrolling the page.

View Transitions at fingertips

Optimistic navigation eliminates network delays and lets React render the opened page in 16 milliseconds. It makes View Transitions API shine, making it possible to run transition animation without delays, right after user presses the link.

Such navigation feels seamless, like flipping through a glossy magazine, it's increases user engagement and encourage exploring more website content.

Exposing Heavy Code

After eliminating network delays during navigation, software delays will be more visible. Heavy libraries/components often cause these delays.

Consider refactoring if the "Interaction to Next Paint" metric takes more than 32 milliseconds in a production build (you can find that metric in Chrome developer tools on the "Performance" tab; it runs on navigation).

Drawbacks of Optimistic Navigation

No solution is flawless. Here are the trade-offs of optimistic navigation:

Potential confusion

In some scenarios, users can experience confusion because of rendering with predicted data.

  • When a user's session is expired but the opened page requires authentication, the user will briefly see the opened page with predicted data and then be redirected to the login page. To improve UX, avoid redirects and show the login form or error on the opened page after the predicted data.
  • If the page content is changed while the user is deciding to open it (~30-60 sec time window), they will see stale data while the server response is loading. Users can see stale content briefly, but there's a low chance that it'll ever happen.

Increase entity list data.

The main idea behind this optimization is to use data from the entity list to optimistically render the opened entity detail page. The more predicted data you receive in an entity list request, the more page content can be rendered optimistically. It is similar to prefetching, but instead of creating N prefetching requests for each entity, we load more fields inside a single entity list request.

RAM Usage

In my implementation, every link stores a predicted data object (via the placeholderData prop); once clicked, it's used for rendering. When the page displays 1,000 links with predicted data (≈ 4.5 kb), the app spends around 5 MB of RAM. If you have a custom sitemap page that shows thousands of links, avoid using optimistic navigation for it.

Implementation

My implementation makes Tanstack Router skips calling of router data requests during client (soft) navigation and runs them through react-query. It gives tons of features, including optimistic UI updates.

Bellow I will describe main features of this starter.

Isomorphic loader function

Every loader is created through createIsomorphicFn. It lets loader await for network requests only on server side (to generate HTML), while on client-side it skips all the work to let react-query fetch route data. By doing that you can make Node.js layer run only during hard navigation and IDLE during soft navigations, it'll make your website more sustainable and energy efficient.

const myLoader = createIsomorphicFn()
  .server(async ({ params: { postId }, context }) => {
    const data = await context.queryClient.ensureQueryData(
      blogItemPageOptions(postId),
    )
    return {
      title: data.attributes.title,
    }
  })
  .client(() => {})

Error handler HOC

By skipping network calls during soft navigation, we need to explicitly handle network errors on the client. To achieve it, I created WithErrorHandler component, that wraps page view.

import { ParentComponent } from '@/types/general';
import React from 'react';
import { ErrorRouteComponent, NotFoundRouteComponent } from '@tanstack/react-router';

interface Props {
  notFoundComponent?: NotFoundRouteComponent;
  errorComponent?: ErrorRouteComponent | false | null;
  error: Error & { isNotFound: boolean } | Error | null;
}
export const WithErrorHandler: ParentComponent<Props> = ({ errorComponent: ErrorComponent, notFoundComponent: NotFoundComponent, error, children }) => {
  if (error) {
    if ('isNotFound' in error && error.isNotFound) {
      return NotFoundComponent ? <NotFoundComponent data={{}} /> : null;
    }
    return ErrorComponent ? <ErrorComponent reset={() => {}} error={error} /> : null;
  }
  return children
}

I made a Link component that wraps @tanstack/react-router Link and extend it by adding placeholderData prop. With that component website links can store predicted data of the page they open. When Link is clicked it updates placeholderData skeleton, that will be used for optimistic rendering.

import React, { PropsWithChildren } from 'react';
import { Link as TanstackLink, LinkProps } from '@tanstack/react-router';
import { setPlaceholderData } from '@/singletones/placeholderData';

type Props = LinkProps & React.AnchorHTMLAttributes<HTMLAnchorElement> & {
  placeholderData?: object;
}

export const Link: React.FC<PropsWithChildren<Props>> = ({ children, onClick, placeholderData, ...props }) => {
  const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
    if (onClick) {
      onClick(e);
    }

    setPlaceholderData(placeholderData);
  }

  return (
    <TanstackLink
      onClick={handleClick}
      {...props}
    >
      {children}
    </TanstackLink>
  )
}

Conclusion

Optimistic navigation has a positive impact on user engagement, SEO, and hosting bills. But to implement it, tight cooperation between the backend, frontend, and design teams is required. It might be useless for small projects but can save thousands of USD per month for a big company.