
Boost Web Performance with client-side prediction
Slow websites drive users away. Even well-optimized sites can suffer from sluggish navigation due to poor internet connections or server overload. Achieving consistently fast navigation is challenging. Techniques like prefetching or AI-driven prerendering can help, but they are often inconsistent and resource-intensive. The approach described in this article takes a different path: it creates the illusion that content is already loaded, rather than wastefully preloading it.
TL;DR
Navigation from an entity list to a detail page can feel instant if the page is rendered optimistically with predicted data that fills the entire viewport, hiding unloaded sections further down. By reusing data from the entity list as predicted content, you avoid additional network requests. Here's an implementation of this idea.
Motivation
My goal was to build a site with consistently fast soft (client-side) navigation, regardless of the user's internet speed or CPU performance. Prefetching comes close, but it lacks reliability—if a user clicks a link before the content preloads, they experience a delay. Moreover, prefetching at scale causes significant costs, both financial and environmental.
Without prefetching, you can scale to 1 million views per month for just $20 (on a single-user account), while prefetching could cost around $500. Based on Pro plan limits:
- Available Bandwidth: 1024 GB / 1TB
- Available amount of edge requests: 10,000,000 (10 M)
Extra bandwidth costs $0.15 per GB over the limit—that's the primary expense (≈90%). Extra Edge Requests cost $2 per million over the limit.
The Problem: Slow Navigation and Unresponsiveness
Imagine clicking a link and waiting one second for a server response—the UI freezes, and the site feels broken. For clarity, I've created a simple demo app simulating this delay. In the demo below, click the links to experience the lag. It includes links to a "Lorem Ipsum" article and the "Home" page.
Solution: Client-side prediction
To defeat slow navigation, techniques from the gamedev can be used. Client-side prediction enhances responsiveness by letting the client react instantly to inputs, minimizing perceived lag. While it may cause temporary client-server desync, smoothing techniques help. In web development, this translates to optimistic UI updates. Here's a typical flow:
- User clicks a link or button (e.g., "Like").
- The UI updates instantly with a predicted outcome (e.g., Like count +1).
- A background request is sent to the server.
- The UI syncs with the server response, adjusting as needed.
Use this only if you're at least 97% confident the prediction will succeed and the server won't error. Avoid doing predictions for sensitive or rapidly changing data. When implementing, follow these guidelines:
- Synchronize with Server Response: Always update the UI to match the actual response, even if it causes brief flashes.
- Handle Errors Gracefully: On server errors, show an appropriate message—either instead of or after the predicted content.
Performance illusion
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 that illusion in action.
In the demo, app clicking an article card leads to a detail page that shows the same thumbnail, title, and description. The home page fetches a list of articles with partial data; the detail page fetches the article object with full data.
Reuse data from list requests to render the detail page optimistically—the more data list requests have, the more content can be rendered optimistically.
It's like a waiter serving an aperitivo while the main course prepares.
- Traditional Navigation: Click → Wait → Render.
- Prefetching: Preload → Click → Render.
- Optimistic UI Navigation: Click → Render Predicted Data → Sync with Server.
That optimization allows server response slower while the user is distracted with predicted content, which can be useful for load balancing.
View Transitions at fingertips
Client-side predictions eliminate network delays, allowing React to render in ~16ms. This enables the View Transitions API to animate smoothly right after a click. Such navigation feels seamless, like flipping through a magazine, boosting engagement and exploration.

Exposing Heavy Code
With network delays gone, software delays become noticeable. Heavy libraries or components often cause them.
Refactor if "Interaction to Next Paint" exceeds 32ms in production (check in Chrome DevTools > Performance tab during navigation).
Drawbacks of that approach
No solution is perfect. Trade-offs include:
- Flash of outdated Content: If an entity updates between listing load and detail click, users may see a flash of outdated content. Minimize this by setting listing request staleTime to one minute. For rapidly changing content via WebSockets, use the latest WebSocket data for predictions.
- Increased Entity List Response Size: More predicted data in lists means larger responses.
- RAM usage. Storing predicted data consumes memory; avoid for pages with thousands of links.
Implementation
In this starter, I aimed for seamless navigation from listings to detail pages. The project includes numerous micro-optimizations:
- Seamless navigation: optimistic UI with client-side prediction for synchronous transitions.
- Optimized responsive images: Using ThumbHash for placeholders and preloading strategies to speed up loading.
- SSR to SPA transition: During soft navigation, route data requests run through React Query for efficient caching. Achieved by using createIsomorphicFn with empty actions on the client as the route loader.
- Route chunk preloading: Automatically preloads JS route chunks during initial load to reduce navigation latency.
- Granular optimistic UI: Leverages React Query for precise management of optimistic updates.
- View Transitions API: Demonstrates complex animations of multiple elements during soft navigation.
Bellow I will describe key features of this starter.
Isomorphic loader function
Loaders use `createIsomorphicFn`. On server, they await requests for HTML generation; on client, they skip to let React Query handle fetching. This keeps Node.js idle during soft navigations, improving sustainability.
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
}Custom Link component
This wraps TanStack's Link, adding a placeholderData prop. On click, it updates a singleton with predicted data 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 UI with predicted data creates the illusion of fast navigation. When successful, it delights users, increases engagement, and improves Web Vitals. But failed predictions can confuse and harm reputation. Risk of failed predictions can be reduced with 60-second staleTime or WebSockets.






