Skip to content

Commit 6c7e38d

Browse files
committed
feat: layout shift troubleshooting guide
1 parent 7b6e2bc commit 6c7e38d

File tree

7 files changed

+193
-1
lines changed

7 files changed

+193
-1
lines changed

.DS_Store

0 Bytes
Binary file not shown.
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
---
2+
title: Layout shift on initial page load
3+
description: Learn how to identify and diagnose layout shifts on initial page load in your app
4+
---
5+
6+
## What is a layout shift?
7+
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:
12+
13+
<Frame>
14+
<video
15+
muted
16+
loop
17+
playsInline
18+
controls
19+
title="Layout shift example"
20+
className="w-full aspect-video"
21+
src="/images/developer/guides/troubleshooting/layout-shift/layout-shift.mp4"
22+
></video>
23+
</Frame>
24+
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.
97+
98+
<Note>
99+
To make the layout shift more noticeable, we added `header` and `footer` components to the layout
100+
</Note>
101+
102+
### Diagnosing a layout shift
103+
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.
106+
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:
109+
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>
120+
121+
As you can see, the `SessionProvider` is updated twice. The first update corresponds to the
122+
initial render of the `SessionProvider` component:
123+
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>
130+
131+
The second update corresponds to the re-render of the `SessionProvider` component and is caused by changed props:
132+
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>
139+
140+
### What's happening?
141+
142+
So what's going on here? Why is this happening?
143+
144+
This is related to the way React handles updates during the hydration process.
145+
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*.
149+
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.
156+
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.
158+
159+
<Info>
160+
Makeswift runtime library wraps all page components in a `Suspense` boundary when serving a page.
161+
</Info>
162+
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>
168+
169+
### Fixing a layout shift
170+
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.
173+
174+
In more complex cases, you might need to use different approaches and techniques.
175+
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.
180+
181+
## References
182+
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/)
186+
- [New Suspense SSR Architecture in React 18](https://github.com/reactwg/react-18/discussions/37)
187+
- [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.
497 KB
Loading
352 KB
Loading
Binary file not shown.

mint.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,10 @@
167167
},
168168
{
169169
"group": "Troubleshooting",
170-
"pages": ["developer/guides/troubleshooting/builder-is-not-loading"]
170+
"pages": [
171+
"developer/guides/troubleshooting/builder-is-not-loading",
172+
"developer/guides/troubleshooting/layout-shift"
173+
]
171174
},
172175
"developer/guides/environments",
173176
{

0 commit comments

Comments
 (0)