|
| 1 | +--- |
| 2 | +title: Investigating a layout shift |
| 3 | +description: Learn how to identify and diagnose layout shifts 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 | +## Reproducing a layout shift |
| 31 | + |
| 32 | +Let's illustrate how layout shift can occur in a real-world scenario. |
| 33 | + |
| 34 | +Imagine you have a Makeswift site and you want to use `next-auth` for authentication capabilities. |
| 35 | +So you add `SessionProvider` to manage the session state. |
| 36 | + |
| 37 | +Since `SessionProvider` is a client component, you'd probably create a wrapper component, say `Providers`, |
| 38 | +to wrap your `SessionProvider` and use it in your `layout.tsx` file. |
| 39 | + |
| 40 | +Here is the code you might end up with. |
| 41 | + |
| 42 | +<CodeGroup> |
| 43 | +```tsx src/app/providers.tsx |
| 44 | +'use client' |
| 45 | + |
| 46 | +import { SessionProvider } from 'next-auth/react' |
| 47 | + |
| 48 | +export function Providers({ children }: { children: React.ReactNode }) { |
| 49 | + return <SessionProvider>{children}</SessionProvider> |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +```tsx src/app/layout.tsx |
| 54 | +// ... |
| 55 | +import { Providers } from './providers' |
| 56 | + |
| 57 | +export default async function RootLayout({ |
| 58 | + children, |
| 59 | +}: Readonly<{ |
| 60 | + children: React.ReactNode |
| 61 | +}>) { |
| 62 | + return ( |
| 63 | + <html lang="en"> |
| 64 | + <head> |
| 65 | + <DraftModeScript /> |
| 66 | + </head> |
| 67 | + <body className={clsx(inter.className, 'bg-gray-600')}> |
| 68 | + <MakeswiftProvider previewMode={(await draftMode()).isEnabled}> |
| 69 | + <Providers> |
| 70 | + <header className="flex items-center justify-between bg-gray-600 p-4 px-10 text-white"> |
| 71 | + <h1 className="text-3xl font-bold underline">header</h1> |
| 72 | + </header> |
| 73 | + |
| 74 | + {children} |
| 75 | + |
| 76 | + <footer className="flex items-center justify-between bg-gray-600 p-4 px-10 text-white"> |
| 77 | + <h1 className="text-3xl font-bold underline">footer</h1> |
| 78 | + </footer> |
| 79 | + </Providers> |
| 80 | + </MakeswiftProvider> |
| 81 | + </body> |
| 82 | + </html> |
| 83 | + ) |
| 84 | +} |
| 85 | +``` |
| 86 | +</CodeGroup> |
| 87 | + |
| 88 | +Nothing seems wrong with this code, right? Now let's run the app and see what happens. |
| 89 | + |
| 90 | +During the initial load, you will see the header and the footer collapsing together, |
| 91 | +and then, in a moment, a page content appearing between them. Exactly as on the video above. |
| 92 | + |
| 93 | +Congratulations! We have just created a site with a layout shift. |
| 94 | + |
| 95 | +<Note> |
| 96 | +To make the layout shift more noticeable, we added `header` and `footer` components to the layout |
| 97 | +</Note> |
| 98 | + |
| 99 | +## Diagnosing a layout shift |
| 100 | + |
| 101 | +Let's investigate the layout shift we just created. We are going to use **React Profiler** from the |
| 102 | +[React Developer Tools](https://react.dev/learn/react-developer-tools) to see what is happening. |
| 103 | + |
| 104 | +Open **DevTools** in your browser and navigate to the **Profiler** tab. Record a profile of a page reload |
| 105 | +and take a closer look at the updates: |
| 106 | + |
| 107 | +<Frame> |
| 108 | + <video |
| 109 | + muted |
| 110 | + loop |
| 111 | + playsInline |
| 112 | + controls |
| 113 | + title="React profiler" |
| 114 | + className="w-full aspect-video" |
| 115 | + src="/images/developer/guides/troubleshooting/layout-shift/react-profiler.mp4" |
| 116 | + ></video> |
| 117 | +</Frame> |
| 118 | + |
| 119 | +As you can see, the `SessionProvider` is updated twice. The first update corresponds to the |
| 120 | +initial render of the `SessionProvider` component: |
| 121 | + |
| 122 | +<Frame> |
| 123 | + <img |
| 124 | + src="/images/developer/guides/troubleshooting/layout-shift/react-profiler-render-1.png" |
| 125 | + alt="First update to the SessionProvider" |
| 126 | + /> |
| 127 | +</Frame> |
| 128 | + |
| 129 | +The second update corresponds to the re-render of the `SessionProvider` component and is caused by changed props: |
| 130 | + |
| 131 | +<Frame> |
| 132 | + <img |
| 133 | + src="/images/developer/guides/troubleshooting/layout-shift/react-profiler-render-2.png" |
| 134 | + alt="Second update to the SessionProvider" |
| 135 | + /> |
| 136 | +</Frame> |
| 137 | + |
| 138 | +## What's happening? |
| 139 | + |
| 140 | +So what's going on here? Why is this happening? |
| 141 | + |
| 142 | +This is related to the way React handles updates during the hydration process. |
| 143 | + |
| 144 | +Let's take a closer look at the `SessionProvider` [component](https://next-auth.js.org/getting-started/client#sessionprovider). |
| 145 | +It accepts a `session` prop that is used to determine the current session state. The problem arises when the `session` prop is |
| 146 | +updated *during the hydration process*. |
| 147 | + |
| 148 | +Here is the summary of what is going on: |
| 149 | +- The page is server-side rendered (SSR). |
| 150 | +- The server sends the HTML to the client, and React begins hydration. |
| 151 | +- While hydration is in progress, the client side receives a new update, in this case `session` value for the `SessionProvider` component. |
| 152 | +- 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. |
| 153 | + |
| 154 | +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. |
| 155 | + |
| 156 | +<Note> |
| 157 | +Diagnosing a layout shift caused by interrupted hydration can be tricky. React used to log a warning |
| 158 | +for this situation when using the Next.js Pages Router (though the App Router suppressed it). That warning |
| 159 | +[was removed](https://github.com/facebook/react/pull/25692) in later versions of React 18 and in React 19. |
| 160 | +</Note> |
| 161 | + |
| 162 | +## Fixing a layout shift |
| 163 | + |
| 164 | +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. |
| 165 | +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. |
| 166 | + |
| 167 | +In more complex cases, you might need to use different approaches and techniques. |
| 168 | + |
| 169 | +Here are some tips that might help to narrow down the issue: |
| 170 | +- Identify the components that are causing the layout shift. |
| 171 | +- Inspect all updates that occur during page loading and the hydration process. |
| 172 | +- For client components, provide the necessary props so server-rendered content matches the client-rendered content. |
| 173 | + |
| 174 | +## References |
| 175 | + |
| 176 | +We recommend checking out the following resources for more information on relevant topics: |
| 177 | + |
| 178 | +- [Making Sense of React Server Components](https://www.joshwcomeau.com/react/server-components/) |
| 179 | +- [New Suspense SSR Architecture in React 18](https://github.com/reactwg/react-18/discussions/37) |
| 180 | +- [React Developer Tools](https://react.dev/learn/react-developer-tools) |
| 181 | +- [Cumulative Layout Shift (CLS)](https://web.dev/articles/cls) |
| 182 | +- [Debug layout shifts](https://web.dev/articles/debug-layout-shifts) |
0 commit comments