Live demo: https://nextjs-nested-layouts-ruby.vercel.app/
I created this repo to understand a performance issue when using only React Client Components on a page (+ App Router). It's a list-detail page. When I click on different items in the list, the client hits the Next.js server for an RSC Payload on every click. I don't understand why.
I would have assumed that all code for that page should be on the client after the first render, and from then on, the client would not have to load anything from the server. Can anyone help me understand this behavior and a potential workaround?
Please see details below.
- Application data is changing frequently – data freshness is important. So data caching should be disabled.
- Data is obtained from an external service that exposes a REST or GraphQL API.
- FCP, TTI are not important (not a marketing or shopping site).
- Responsiveness to user interactions is most important - as the user clicks on items in the list, we want to show the LATEST detail as soon as possible – the application should not feel sluggish.
- Application is to be used in the field. Assume a Fast 3G connection at best.
Here's a screenshot of my POC – a movie list on the left and the selected movie's detail on the right. Granted this is not fast changing data, but it will do for the POC 😃
I have implemented the requirements 4 ways to compare the performance:
- Server Components: The entire page is built using React Server Components. Both list and detail data are fetched on the server.
- Client Components: The entire page is built using React Client Components. Both list and detail data are fetched on the client. This is the preferred option give the requirements.
- Fake Child: A variation of option 2, to improve performance (not
successful). Here we are faking the nested child component by returning
undefined(see here). This allows us to maintain the URL structure as/movies/id, but serve the detail as a hard-coded child (see here). - Query Params: Abandon the nested layout approach. Use a query parameter to get the detail. This approach also did not improve the performance.
Also, to meet the requirements, I have done the following:
Given that Client Components is my preferred option (option 2), let's first demonstrate the issue.
- Point your browser to the live demo, which is the production build.
- Open the Chrome Dev Tools (right click >
Inspect). - Click on the
Networktab. - Change network speed to Fast 3G.
- In the application, click on the
Client Componentstab. The movie list appears on the left. The initial display may be slow because the API server may be cold starting, however subsequent loads should be fast. - Now start clicking on different movies in the movie list. You will see a loading indicator and then the movie detail appearing after ~1200 ms. This is the interaction that I am trying to speed up.
Again, this analysis is focused on the Client Components option (option 2).
- The parent layout is marked as a client component using
'use client'– see here. - The nested child is also marked as a client component using
'use client'– see here.
In spite of this entire page consisting of client components only, the browser is making two network calls on each movie click. Looking at the calls for the 2nd click:
GET /movies/tt0071562?\_rsc=101fc: Call to the Next.js server to get the RSC Payload. This is because of a route change from/movies/tt0468569to/movies/tt0071562. The client wants to render only the route segment that has changed (see partial rerendering), i.e. the movie detail. Hence it is asking for the RSC Payload for the[id]segment from the server (see code splitting).GET {{API_URL}}/movies/tt0071562: Call to the external API to get the movie data
See the Chrome Dev Tools snapshot below for clarity:
I can understand if the RSC Payload is requested just the first time a movie is clicked. However, for subsequent clicks, we shouldn't have to download the RSC payload again because we are rendering the same client component repeatedly (just with different data). We are wasting 572 ms per click!
I would love to get (1) an explanation of why this behavior is happening and (2) a workaround where all we see is one call to the API server for each click.
This is hampering my ability to deliver a responsive user interface for this use case with Next + RSC. This would be a no-brainer to implement as a traditional React SPA.
npm ci
npm run devNow point your browser to http://localhost:3000
npm ci
npm run build
npm start
