Skip to content

Commit 3778033

Browse files
committed
fix: feedback on layout shift troubleshooting guide
1 parent cf1b87e commit 3778033

File tree

5 files changed

+47
-145
lines changed

5 files changed

+47
-145
lines changed
Lines changed: 47 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
---
22
title: Layout shift on initial page load
3-
description: Learn how to identify and diagnose layout shifts on initial page load in your app
3+
description: Learn how to fix a layout shift on initial page load in your app
44
---
55

6-
## What is a layout shift?
6+
{/* @todo(sasha): Update with a correct link to the blog post once it is ready */}
7+
{/* <Note>
8+
For a deeper dive into diagnosing layout shifts, check out our blog post: [Diagnosing a Layout Shift](/blog/diagnosing-layout-shifts)
9+
</Note> */}
710

8-
Layout shift is a common issue in web development where elements on a page move unexpectedly,
9-
causing a poor user experience.
10-
11-
Here is an example of a layout shift:
11+
Watch the example below to recognize a layout shift caused by a component update during hydration. If your app behaves similarly, follow the steps in this guide.
1212

1313
<Frame>
1414
<video
@@ -22,168 +22,70 @@ Here is an example of a layout shift:
2222
></video>
2323
</Frame>
2424

25-
Layout shifts can occur for a variety of reasons, such as:
26-
- Components re-rendering or remounting while page is loading
27-
- Images or videos loading without a specified size
28-
- Fonts loading and causing text to shift
29-
30-
## Layout shift due to context update during hydration
31-
32-
Let's illustrate how layout shift can occur in a real-world scenario. In this example, we will
33-
investigate a layout shift caused by a context update during the hydration process.
34-
35-
### Reproducing the issue
36-
37-
Imagine you have a Makeswift site and you want to use `next-auth` for authentication capabilities.
38-
So you add `SessionProvider` to manage the session state.
39-
40-
Since `SessionProvider` is a client component, you'd probably create a wrapper component, say `Providers`,
41-
to wrap your `SessionProvider` and use it in your `layout.tsx` file.
42-
43-
Here is the code you might end up with.
44-
45-
<CodeGroup>
46-
```tsx src/app/providers.tsx
47-
'use client'
48-
49-
import { SessionProvider } from 'next-auth/react'
50-
51-
export function Providers({ children }: { children: React.ReactNode }) {
52-
return <SessionProvider>{children}</SessionProvider>
53-
}
54-
```
55-
56-
```tsx src/app/layout.tsx
57-
// ...
58-
import { Providers } from './providers'
59-
60-
export default async function RootLayout({
61-
children,
62-
}: Readonly<{
63-
children: React.ReactNode
64-
}>) {
65-
return (
66-
<html lang="en">
67-
<head>
68-
<DraftModeScript />
69-
</head>
70-
<body className={clsx(inter.className, 'bg-gray-600')}>
71-
<MakeswiftProvider previewMode={(await draftMode()).isEnabled}>
72-
<Providers>
73-
<header className="flex items-center justify-between bg-gray-600 p-4 px-10 text-white">
74-
<h1 className="text-3xl font-bold underline">header</h1>
75-
</header>
76-
77-
{children}
78-
79-
<footer className="flex items-center justify-between bg-gray-600 p-4 px-10 text-white">
80-
<h1 className="text-3xl font-bold underline">footer</h1>
81-
</footer>
82-
</Providers>
83-
</MakeswiftProvider>
84-
</body>
85-
</html>
86-
)
87-
}
88-
```
89-
</CodeGroup>
90-
91-
Nothing seems wrong with this code, right? Now let's run the app and see what happens.
92-
93-
During the initial load, you will see the header and the footer collapsing together,
94-
and then, in a moment, a page content appearing between them. Exactly as on the video above.
95-
96-
Congratulations! We have just created a site with a layout shift.
25+
## Background
9726

98-
<Note>
99-
To make the layout shift more noticeable, we added `header` and `footer` components to the layout
100-
</Note>
27+
Such shifts happen when a component's props, context, or state change mid-hydration, triggering a Suspense fallback and a visual jump.
10128

102-
### Diagnosing a layout shift
29+
Most common cause is a context provider whose value changes between server-side rendering (SSR) and client-side rendering (CSR). This can happen if the provider's value is derived from a state or prop that is initialized differently on the server and client.
10330

104-
Let's investigate the layout shift we just created. We are going to use **React Profiler** from the
105-
[React Developer Tools](https://react.dev/learn/react-developer-tools) to see what is happening.
31+
Here is the summary of how this can happen:
32+
- The page is server-side rendered (SSR).
33+
- The server sends the HTML to the client, and React begins hydration.
34+
- While hydration is in progress, the client side receives a new update.
35+
- This update interrupts hydration, causing React to discard the in-progress hydration and re-render the components on the client with the updated session value.
36+
- While client side re-rendering is happening, React replaces the server rendered content with a client side fallback found in a closest `Suspense` boundary. The fallback happens to be `null` in this case. This effectively causes the layout shift.
10637

107-
Open **DevTools** in your browser and navigate to the **Profiler** tab. Record a profile of a page reload
108-
and take a closer look at the updates:
38+
According to the React team's [comment](https://github.com/facebook/react/issues/24476#issuecomment-1127800350) in a GitHub issue, this is expected behavior and not considered a bug.
10939

110-
<Frame>
111-
<video
112-
muted
113-
playsInline
114-
controls
115-
title="React profiler"
116-
className="w-full aspect-video"
117-
src="/images/developer/guides/troubleshooting/layout-shift/react-profiler.mp4"
118-
></video>
119-
</Frame>
40+
<Info>
41+
Makeswift runtime library wraps all page components in a `Suspense` boundary when serving a page.
42+
</Info>
12043

121-
As you can see, the `SessionProvider` is updated twice. The first update corresponds to the
122-
initial render of the `SessionProvider` component:
44+
<Note>
45+
React used to log a warning for this situation when using the Next.js Pages Router (though the App Router suppressed it). That warning [was removed](https://github.com/facebook/react/pull/25692) in later versions of React 18 and in React 19.
46+
</Note>
12347

124-
<Frame>
125-
<img
126-
src="/images/developer/guides/troubleshooting/layout-shift/react-profiler-render-1.png"
127-
alt="First update to the SessionProvider"
128-
/>
129-
</Frame>
48+
## Troubleshooting Steps
13049

131-
The second update corresponds to the re-render of the `SessionProvider` component and is caused by changed props:
50+
### 1. Reproduce the Shift
13251

133-
<Frame>
134-
<img
135-
src="/images/developer/guides/troubleshooting/layout-shift/react-profiler-render-2.png"
136-
alt="Second update to the SessionProvider"
137-
/>
138-
</Frame>
52+
- Load your page fresh (SSR) and look for sudden collapses or reflows. E.g., headers, nav menus, or sections disappearing briefly—then snapping into place.
53+
- In Chrome DevTools → **Performance**, you'll see “layout-shift” entries aligned with two render passes.
13954

140-
### What's happening?
55+
### 2. Record with React DevTools Profiler
14156

142-
So what's going on here? Why is this happening?
57+
- Open the **Profiler**, start recording, then reload the page.
58+
- Stop recording after the shift. In the flame chart, find two consecutive renders of the same component (hydration then update).
14359

144-
This is related to the way React handles updates during the hydration process.
60+
### 3. Pinpoint the Problematic Update
14561

146-
Let's take a closer look at the `SessionProvider` [component](https://next-auth.js.org/getting-started/client#sessionprovider).
147-
It accepts a `session` prop that is used to determine the current session state. The problem arises when the `session` prop is
148-
updated *during the hydration process*.
62+
- Examine which props, context values, or state differ between those renders.
63+
- Common culprits:
64+
- Context providers whose value identity changes
65+
- State initialized differently on client vs. server
66+
- Data fetched in a `useEffect` (runs post-hydration)
14967

150-
Here is the summary of what is going on:
151-
- The page is server-side rendered (SSR).
152-
- The server sends the HTML to the client, and React begins hydration.
153-
- While hydration is in progress, the client side receives a new update, in this case `session` value for the `SessionProvider` component.
154-
- This update interrupts hydration, causing React to discard the in-progress hydration and re-render the components on the client with the updated session value.
155-
- While client side re-rendering is happening, React replaces the server rendered content with a client side fallback found in a closest `Suspense` boundary. The fallback happens to be `null` in this case. This effectively causes the layout shift.
68+
### 4. Isolate the Component
15669

157-
According to the React team's [comment](https://github.com/facebook/react/issues/24476#issuecomment-1127800350) in a GitHub issue, this is expected behavior and not considered a bug.
70+
- Temporarily comment out or disable the suspected update (e.g., remove the provider or delay the state change).
71+
- Confirm that the shift disappears when that update is skipped.
15872

159-
<Info>
160-
Makeswift runtime library wraps all page components in a `Suspense` boundary when serving a page.
161-
</Info>
73+
### 5. Apply a Permanent Fix
16274

163-
<Note>
164-
Diagnosing a layout shift caused by interrupted hydration can be tricky. React used to log a warning
165-
for this situation when using the Next.js Pages Router (though the App Router suppressed it). That warning
166-
[was removed](https://github.com/facebook/react/pull/25692) in later versions of React 18 and in React 19.
167-
</Note>
75+
- **Match SSR & Client:** Ensure initial props/state are identical. For example, compute or fetch values during SSR so the client sees the same markup.
76+
- **Memoize Values:** Wrap dynamic context or prop values in [`useMemo`](https://react.dev/reference/react/useMemo) to avoid identity changes during hydration.
77+
- **Defer Non-Critical Updates:** Use React's [`startTransition`](https://react.dev/reference/react/startTransition) to mark updates as non-urgent, preventing fallback rendering.
16878

169-
### Fixing a layout shift
79+
### 6. Verify the Resolution
17080

171-
Fixing a layout shift is not always straightforward. The issue we encountered in this guide could be fixed by providing `session` value to the `SessionProvider` component.
172-
This way, the session value will not change during the hydration process, and React will not need to re-render the rest of the components.
81+
- Re-run your Profiler trace: you should now see only one render with no fallback UI invoked during hydration.
82+
- Confirm that no “layout-shift” entries align with any component updates.
17383

174-
In more complex cases, you might need to use different approaches and techniques.
84+
---
17585

176-
Here are some tips that might help to narrow down the issue:
177-
- Identify the components that are causing the layout shift.
178-
- Inspect all updates that occur during page loading and the hydration process.
179-
- For client components, provide the necessary props so server-rendered content matches the client-rendered content.
86+
By following this reproduce → profile → isolate → fix workflow, you'll eliminate jumps caused by component updates during React hydration.
18087

18188
## References
18289

183-
We recommend checking out the following resources for more information on relevant topics:
184-
185-
- [Making Sense of React Server Components](https://www.joshwcomeau.com/react/server-components/)
18690
- [New Suspense SSR Architecture in React 18](https://github.com/reactwg/react-18/discussions/37)
18791
- [React Developer Tools](https://react.dev/learn/react-developer-tools)
188-
- [Cumulative Layout Shift (CLS)](https://web.dev/articles/cls)
189-
- [Debug layout shifts](https://web.dev/articles/debug-layout-shifts)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)