Blazingly Fast Data Fetching That Scales

Client ⇄ server state synchronization is pretty hard, right? Since the dawn of time, we have been trying to solve it in various ways. Before I show you my little experiment, let’s first look at the problem and the solutions we already have.

The problem

In this article, we are looking at dynamic data fetching. I’m not talking about primarily static content occasionally updated like blog posts, documentation, etc. I’m talking about things that are custom per session or do change very often, like authenticated user data, analytics data, IoT data, etc.

Solution #1: Traditional server-side rendering

Traditional server-side rendering is the oldest solution, and we are constantly going back to it since how good of an experience it provides for the first render.

Traditional server-side rendering sequence diagram

Pros

  1. Fast first render - second only to static content
  2. SEO - it doesn’t matter for the very dynamic data since it is not going to be indexed by anyone anyway, mainly because it is behind authentication

Cons

  1. Expensive to develop - client hydration1 is done manually and imperatively
  2. Expensive to scale - increasingly needs more computational power
  3. Global scaling is hard - scaling computational power in different regions is challenging, but edge computing2 is making significant progress here
  4. Subsequent data fetching is slower since the client will fetch more data (all HTML)
  5. User experience of screen transitions is not that great - handled either by the browser itself or some HTML over-the-wire library

Conclusion

With this solution, we are trading future scalability on the operational and engineering sides for an excellent first impression. It is the best solution for many small and medium-sized web apps.

Solution #2: Traditional client-side fetching

Now we are on the other extreme side. This is the typical single-page application area and is very popular among most dynamic apps.

There are a lot of recent innovations bringing the loading experience and caching capabilities to the masses thanks to libraries like TanStack Query and SWR.

Traditional client-side fetching sequence diagram

Pros

  1. The user gets some initial feedback faster - a nice loading indicator
  2. Cheaper to develop - client and server engineers can independently develop and use best technology for the job (UI can be easily declarative 🎉)
  3. Scales better - client code can be cached, and the server only takes responsibility for returning raw data, which requires less computational power
  4. Full control over screen transitions - totally controlled by the client app (amazing things can be achieved if utilized properly)

Cons

  1. First render is a lot slower - although the user sees nice loading indicator, it still takes substantially more time to render the asynchronously fetched data
  2. Slightly more expensive to operate - client and server code are decoupled, and for scalability, most of the time, they are deployed independently
  3. Too much JavaScript - more assets means more code executed on the client-side

Conclusion

This time the trading is reversed—we’re giving up fast first render in exchange for scalability. As mentioned previously, this is the most popular solution used by giants like Gmail, Facebook (with React Server Components 3), Twitter, Instagram, etc.

Solution #3: Server-side rendering with JavaScript UI frameworks

While the previous options were the extremes, this might be the middle ground. Popularized by frameworks like Next.js, Nuxt, Remix, SvelteKit, etc., this technique solves all problems mentioned above while providing a good developer experience. Sounds great, but let’s look at it more profoundly for a better understanding.

Server-side rendering with JavaScript UI frameworks sequence diagram

Pros

  1. Fast first render - almost as fast as traditional server-side rendering
  2. Cheaper to develop - same mental model (declarative UI) is used for developing server-side rendering and JavaScript that hydrates the client
  3. Island architecture4 - helps to reduce JavaScript sent to the client
  4. Subsequent data fetching is at least as good as traditional client-side apps, and most of the time, thanks to the framework optimizations, even superior
  5. Full control over screen transitions - same as the traditional client-side rendering
  6. SEO - same as the traditional server-side rendering

Cons

  1. The application takes substantially more time to hydrate since it needs to parse all the JavaScript and HTML, read the embedded JSON data, reconstruct the Virtual DOM and attach all the events
  2. Even more expensive to scale - most of the popular UI frameworks are not well optimized for server-side rendering; thus, they require more computational power than traditional server-side rendering template engines
  3. Even more expensive to operate - server-side is divided even more (rendering and data layers), which requires more tooling for deployment and care (things can work differently locally and on the server)
  4. Global scaling is still hard - maybe the best use of edge computing for web apps is server-side rendering JavaScript-heavy applications

Conclusion

It’s fascinating how much this solution helps. No wonder why these frameworks are so popular—the benefits are great.

While I’m a big advocate of using them, I still think there is room for improvement.

What can we learn from the existing solutions?

  1. The first render should happen on the server
  2. Rendering JavaScript UI frameworks on the server is more expensive than traditional server-side and client-side rendering
  3. Hydrating JavaScript UI frameworks from server-side rendering is a slow operation
  4. Frameworks and tools can help achieve good user and developer experience

What can we improve?

1. Static components for static parts

If we look at the user interfaces of most applications, there are some static-content components—headers, footers, filters, side navigations, etc.

Wireframe that highlights static components

What if we pre-render static version of these screens so we can globally distribute and serve them as fast as possible? This is exactly what static generation5 is for.

While this is a great idea, it limits how dynamic the data is since it will require the revalidation of generated static HTML files on every data change. This is not something we want to do at all—remember Phil Karlton’s quote about cache invalidation6.

2. Skeleton components for dynamic parts

What if we use skeleton components instead of dynamic content components while pre-rendering pages? This way, generated static HTML files are not tied to the dynamic content and can be cached easily. Then we can hydrate skeleton components with dynamic content components and fetch the data totally on the client-side.

This is a big improvement but we sacrificed user’s experience in exchange for scalability. I think we can improve even further to claim the Blazingly fast title.

3. Preload

Server-side rendering is great because it sends data and rendered HTML in a single request. Thus, the data and interface are available simultaneously on the client.

Currently, we are doing this with at least two sequential requests, and we cannot reduce the number of requests; otherwise, scalability will be affected.

While researching this, I stumbled upon a concept called Resource Hints7. We can tell the browser what to preload right after the HTML is delivered to the client without waiting JavaScript to be downloaded, parsed and executed. And it happens in parallel!

Angora Fetch sequence diagram

Summary of what we can improve

  1. First render for static parts could easily happen at the build stage using excellent frameworks like Next.js
  2. Dynamic parts can be deferred to client-side fetching—thus no rendering of JavaScript UI frameworks by the server on every request
  3. Fetching data on the client-side makes hydrating super fast since not much data is sent to the client on the first request. Preload makes these requests happen in parallel while waiting for JavaScript execution
  4. Wouldn’t it be awesome to have a library or framework that helps and guides developers to use this approach?

Meet Angora Fetch

Keeping track of all pages and what endpoints they fetch can be a hustle to maintain. That’s why I believe that good tooling is a must for making this approach mainstream.

I’ve worked on a new library for some time to smooth things out for developers. It’s still a proof of concept and I would love to hear your feedback so we can build it together.

I’m calling it Angora Fetch and you can find it on GitHub and npm.

Warning

The package is not production ready, yet.

Angora Fetch in action with Next.js

I started with Next.js integration for the experiment, but you should keep in mind that it can be integrated into any framework. So let’s see how it works!

1. Install package

npm install @angora/fetch

2. Update config

Update next.config.js to add withAngoraFetch:

// next.config.js
const { withAngoraFetch } = require('@angora/fetch/build'); 

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
};

module.exports = withAngoraFetch(nextConfig); 

3. Middleware

Update or create middleware.js (or middleware.ts if you use TypeScript) to call setAngoraFetchHeaders:

// middleware.ts
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';

import { setAngoraFetchHeaders } from '@angora/fetch'; 

export async function middleware(req: NextRequest) {
  const res = NextResponse.next();
  await setAngoraFetchHeaders(req, res); 
  return res;
}

export const config = {
  matcher: ['/((?!api|_next/static|favicon.ico|sw.js).*)'],
};

4. Provider

Update or create pages/_app.js (or pages/_app.tsx if you use TypeScript) to add AngoraFetchProvider:

// pages/_app.tsx
import type { AppProps } from 'next/app';

import { NextAngoraFetchProvider } from '@angora/fetch'; 

export default function App({ Component, pageProps }: AppProps) {
  return (
    <NextAngoraFetchProvider> 
      <Component {...pageProps} />
    </NextAngoraFetchProvider> 
  );
}

That’s it! You can now use it on any page

// pages/index.tsx
import { getFetchHooks } from '@angora/fetch';
// Same rules as Next.js' `config` applies to the `angora` constant.
export const angora = {
  fetch: ['/api/hello-world'],
};
// `getFetchHooks` returns React.js hooks
// inspired by TanStack Query and SWR.
const [useHelloWorld] = getFetchHooks(angora);

type HelloWorldResponse = { greeting: string };

export default function HomePage() {
  const helloWorldData = useHelloWorld<HelloWorldResponse>();
  const { body, error, isFetching, isOK, status } = helloWorldData;

  if (isFetching) return <p>Loading...</p>;
  if (error) return <p>{error.message}</p>;
  if (!isOK) return <p>{status?.text ?? 'Oops!'}</p>;

  return <p>{body.greeting}</p>;
}

CodeSandbox

I also created a CodeSandbox that you can fork and start experimenting faster: https://codesandbox.io/p/sandbox/next-js-with-angora-fetch-example-z5m10e

Results

Batman Slapping Robin Meme


Without Angora Fetch 🐌

https://next-without-angora-fetch.vercel.app

Next.js app without Angora Fetch


With Angora Fetch 🔥

https://next-with-angora-fetch.vercel.app

Next.js app with Angora Fetch


So what do you think? I would be more than happy to hear your feedback on this solution. You can hit me on Twitter or the contact form of my website.

That’s for now, ladies and gentlemen 🎩.


  1. Client hydration - technique in which client-side JavaScript converts a static HTML web page, delivered either through static hosting or server-side rendering, into a dynamic web page by attaching event handlers to the HTML elements (source: Wikipedia)

  2. Edge computing - distributed computing paradigm that brings computation and data storage closer to the sources of data (source: Wikipedia)

  3. React Server Components allow developers to build apps that span the server and client, combining the rich interactivity of client-side apps with the improved performance of traditional server rendering (source: RFC)

  4. Island architecture - a paradigm that aims to reduce the volume of JavaScript shipped through “islands” of interactivity that can be independent delivered on top of otherwise static HTML (source: Patterns.dev)

  5. Static generation - static HTML file is generated ahead of time corresponding to each route. These static HTML files may be available on a server or a CDN and fetched as and when requested by the client (source: Patterns.dev)

  6. Phil Karlton - “There are only two hard things in Computer Science: cache invalidation and naming things.”

  7. Resource Hints - primitives that enable the developer, and the server generating or delivering the resources, to assist the user agent in the decision process of which origins it should connect to, and which resources it should fetch and preprocess to improve page performance (source: W3C)



Blog by Mertin Dervish.
I enjoy developing extraordinary user experiences.