YLD has served as a long-term partner for Ledger, a platform known for overseeing digital assets, offering seamless avenues to buy, sell, and trade crypto. Ledger recognised the need to embrace new technologies and turned to YLD for our expertise. They aimed for web compatibility, allowing their secure wallet and services to integrate into decentralised apps (dApps). The goal was to have their buying, selling, and trading crypto services available across the web and accessible on any device.
As part of our collaboration with Ledger, we helped rebuild a legacy project repo to modern standards with React Server Components in Next.js. One of the challenges we faced was implementing a caching mechanism to achieve the key criteria of performance and privacy. In this instance, performance is referred to as a brief First Contentful Paint (FCP) time, a well-established factor in web development that has a significant impact on sales. Privacy was a key priority for us, as user-specific data such as wallet balances would pose a serious security risk if exposed to other users. This required us to deliver a solution that could serve static content that remained consistent for all users, whilst dynamically rendering user-specific content.
Technical Problem
When developing an application that uses highly sensitive data, great care must be taken to avoid accidentally storing client data in locations that can be accessed by or shared with other users. Due to the very sensitive nature of the data we were working with at Ledger, it was critical that user data, such as wallet addresses or balances of holdings, was not stored in caches or accidentally served to other users.
Our team did a lot of research on how caching works in the Next.js environment, and they found that the necessary information was scattered across multiple sources and in part buried deep within the documentation. The Next.js team provided excellent documentation, but the novelty of React Server Components introduced added complexity. Ensuring consistent caching at the project and application layers required extra effort to properly configure the application to meet our specific requirements.
Driven by this challenge, we had a closer look at the caching process, focusing on how it functions in Next.js and how to configure our application for the functionality needed to meet our technical goals.
Technical Background
Technologies used:
- Next.js version 14
- Vercel
React Server Components
At the time of writing, React Server Components (RSC) is a novel paradigm for writing React applications. Previously, React components could be rendered on the client side (CSR) or Server-Side Rendering (SSR) with different strategies created to optimise for speed and performance. For example, a Single Page Application (SPA) approach packages up the whole application to be completely rendered on the client side, whereas with a Progressive Web App (PWA) approach the app is divided into individual chunks to be served up by the server as a mixture of CSR and SSR. Fundamentally, the application code must be bundled to be served by the client.
React Server Components (RSC) change the game by running completely on the server, without adding any JavaScript to what’s sent to the client. Unlike client-side components, RSC do not hydrate — meaning they aren't initialised with JavaScript after being rendered as HTML. Additionally, RSC only renders once, so they don't support hooks like useState or useEffect that rely on local state changes and trigger re-renders.
A key improvement with React Server Components is that data fetching, such as third-party API calls or database queries, can now be colocated with the component code. This allows for faster rendering of pages with data. Another major advantage of RSC is the ability to use large JavaScript libraries that would normally be too heavy for client-side bundling, as they now only run on the server.
Caching with Next.js React Server Components
This guide from the Next.js website provides a comprehensive overview of the various caching mechanisms that Next.js implements automatically:
Next.js offers multiple caching mechanisms to improve data and payload retrieval for applications, with default settings aimed at reducing response times. However, it's important to ensure that only the data that’s not specific to an individual user is cached. Otherwise, data may be leaked across other user requests.
Points to highlight
- The Data Cache is persistent across all requests, meaning that fetch requests made with the GET method and the same options are cached. This can lead to stale data if updates occur in a short timeframe.
- A route is dynamically generated, rather than statically generated, if it uses any of these three functions: headers(), cookies(), or searchParams()
- Static routes are rendered at build time and re-rendered when data revalidation occurs
- The default behaviour of Next.js is to cache the rendered result (React Server Component Payload and HTML) of a route.
- The rendered result is cached based on the shortest revalidation time among all fetch requests within that route
- To opt out of caching, you can explicitly set “export const dynamic = ‘force dynamic’” or “export const revalidate = ‘0’” on the route segment. This will make the components re-render and data fetched on each request.
- The ‘use client’ directive in a component file tells Next.js that the component is a client boundary and that the component and its children components are to be dynamically rendered on the client.
Vercel Caching
Currently, most Next.js deployments are made on Vercel, which has specifically built infrastructure to integrate Next.js. Vercel’s Edge Network is a Content Delivery Network (CDN) and a globally distributed computing network for computations at the edge, essentially a CDN with colocated computing. The benefit is a reduction in latency as a user will be closer to a server to render their content. Vercel, like many other CDN platforms, provides a cache layer to reduce latency. These cache layers have default settings that can be overridden by setting the cache headers on the request for each route.
Points to highlight:
- You can use “next.config.js” or “vercel.json” to set the cache headers. Vercel defaults to the framework configuration, so it is advised to set these headers in “next.config.js”
- Static files are cached for the duration of the deployment instance. This means that if a new deployment is made, the cache is invalidated and the new deployment version will be cached.
Cache Control Headers
The cache control headers are fundamental to controlling how your application is cached.
From top to the lowest priority:
- Vercel-CDN-Cache-Control — this is only set in vercel.json (or can be set in Vercel Function Response). This is solely for the Vercel Edge Cache, and it is not forwarded to any other intermediary CDN or the client.
- CDN-Cache-Control — Important to note that this header overrides the lower-priority Cache-Control header. It sets the cache setting for the Vercel Edge Cache, unless the Vercel-CDN-Cache-Control header is present, and also sets any intermediary CDN caches.
- Cache-Control — this is forwarded unchanged to the client if either of the other two headers is present. If only one header is set, then the s-max-age directive is stripped, which means none of the intermediary CDNs are set.
The following directives for the cache headers determine the behaviour of the cache:
- ‘private’ or ‘public’ — private will stop all the caching that is not on the client, whilst public allows shared caches to be used.
- ‘Immutable’ — the data will not be updated when it’s fresh.
- ‘max-age’ — time in seconds until the data is considered stale in the cache, both private and public.
- ‘s-max-age’ — this directive sets the time, in seconds, until the shared cache (CDN or Vercel Edge Cache) is considered fresh. Once the time has elapsed, the response is considered stale. This only applies to shared caches and overrides ‘max-age’.
- ‘stale-while-revalidate’ refers to the number of seconds until the data is considered stale. Once the data is considered stale, it can be refetched in the background whilst the stale data is served.
Summary
- Browser Cache — This is known as the Router Cache in Next.js. The caching of content is determined by the Cache Headers returned from the server, as well as the route settings configured at the application level.
- CDN Cache — This may refer to any intermediary cache you have between your server and the client, such as Cloudflare. In this case, we consider it separate from Vercel’s cache layer. The cache header directives ‘max-age’ and ‘s-max-age’ will determine how long the data will be considered fresh in the cache.
- Vercel Edge Cache — Vercel’s Edge service cache layer. Same as the CDN cache, this cache uses ‘max-age’ and ‘s-max-age’, and additionally can be set using ‘Vercel-CDN-Cache-Control’ header. This extra header means we can configure the Vercel cache separately from any intermediary caches.
- Full Route Cache — Next.js caches the response depending on whether the route is static or dynamic and if the data has been marked as revalidated.
- Data Cache — This is a data cache layer of Next.js, using ‘fetch’ or ‘cache’ functions in the application. The revalidate option for fetch will determine when the cache becomes invalidated.
Technical Solution
To start with, when dealing with React Server Components, it’s best to delineate which components come under the client boundary and which components are to be rendered on the server.
In most cases, the splitting of components can happen organically and by composing the components you can avoid having them fall within a client boundary. The import of components at the module level will determine if a component is within a client boundary. Typically, the layout of the application will remain consistent from user to user, meaning that the majority of the application can be rendered statically for the parts that remain consistent. We can then separate components that display client-specific data, such as balance, or derived data which is dependent on it. One example could be a potential yield given the balance amount from layout components that can be statically rendered, as these will not change between users.
In our project, for the components with sensitive data, we determined it would be best to render them on the client side to avoid sending client data to the server or storing any client data in shared caches. We then were able to set the cache control headers with a large cache time, as the RSC payload and HTML could be used for every user. This vastly improved our response times, as the response time from the cache was significantly shorter than from the server. The data we worked with included both fast and slow-changing components. We thus opted for a ‘no-cache’ revalidation policy to accommodate fast-changing data and a large revalidation time for slow-changing data. The revalidation time can be made with educated guesses of how long the data is okay to be stale.
Conclusion
React Server Components in Next.js is an exciting and fresh way of delivering your React application. The framework is well-documented, with clear and concise explanations, demonstrating the team's commitment to making it accessible to developers.
We initially chose this feature as it allowed consistency across the teams, enabling quick project setup with established structures. Additionally, frequent code improvements and an active developer community were essential requirements for efficiently addressing issues when launching a new project. The novelty of React Server Components and the complex technical implementation details for caching in Next.js with Vercel resulted in us investing time to adequately evaluate the best approach for our client’s requirements.
As a result of this small time investment, we achieved a significant impact in delivering Ledger’s application with a comparably faster contentful paint time and enhanced security for the end user. With YLD's talented experts by your side, you can reach your objectives and advance your business. Contact us to discuss how we can support you.