
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.
Not a breakthrough
I'm sure I'm not the first one to come up with the idea of "just instantly display the list item data again on the detail page". But it might be a first article about that. Let me know if you saw any content with related topic.
Prefetching and bandwidth fees
Prefetching can be wasteful if the preloaded page is never visited or if the prefetched data become stale once used for rendering. Aggressive prefetching drains battery life, wastes internet traffic, and increases hosting costs.
Users with a mouse/trackpad can send fewer network requests if prefetching starts once the link is hovered. Libraries like ForesightJS and Guess.js help reduce redundant network requests even further by predicting user intentions based on cursor movements.
To ensure quick navigation for mobile users, prefetching should work more aggressively, preloading the page as soon as their link enters the viewport. That prefetching strategy increases the number of network requests, especially when users scroll a page with an infinite scrolling listing.
Without prefetching, you can scale to 1 million views per month for just $20 (on a single-user account), while prefetching could cost around $300-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 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. This translates to optimistic UI updates in web development. 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. When implementing, follow these guidelines:
- Synchronize with Server Response: Always update the UI to match the actual response, even if it causes brief flashes.
- Avoid doing predictions for sensitive or rapidly changing data; render a skeleton instead.
- 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 response.
That optimization allows server response slower while the user is distracted with predicted content, which can be useful for load balancing.
Interaction to next paint
React is pretty fast; soft navigation between two pages can take around 32-48 ms on a mid-tier mobile phone (try to navigate through the demo with a throttled CPU via dev tools). With Solid.js, INP can be even lower (16-24 ms).
Real-world apps are heavy, and navigation can still take more than 100 ms to complete, even if network delays are eliminated. To reduce INP, try to make the opened page gradually interactive.
Add icing on the cake with the View Transition API, and navigation will feel seamless, like flipping through a magazine, boosting engagement and exploration.

Drawbacks of that approach
No solution is perfect. Trade-offs include:
- Flash of outdated Content: can be seen if listing page displays stale content. Minimize those risks by setting staleTime to one minute or use WebSockets/SSE for rapidly changing data.
- 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.






