Sanity content loading bottom-to-top with staggered flash on first load

9 replies
Last updated: Mar 23, 2022
Hey guys, working on a NextJS project, which is live here . Noticing some strange behaviour on first load. Can anyone illuminate me as to why my page might be loading bottom-to-top in a staggered sequence? This content flash is driving me nuts!
AI Update

Based on your description of content loading "bottom-to-top in a staggered sequence," this is almost certainly a Next.js rendering issue rather than anything Sanity-specific. Here are the most likely causes and solutions:

1. React Suspense Boundaries / Streaming SSR

If you're using Next.js 13+ with the App Router, the default behavior uses streaming with React Server Components. This progressively sends HTML to the browser as components resolve, which can create that staggered loading effect.

Solutions:

  • Add a loading.tsx file at your route level to show a loading state while everything resolves
  • Wrap sections in Suspense boundaries with meaningful fallbacks
  • If you want everything to load at once, ensure all async operations complete before rendering (though this increases time-to-first-byte)

2. Client-Side Data Fetching

If you're fetching Sanity content with useEffect or client-side queries, each component renders empty first, then populates with data after hydration. This creates the exact "flash" you're describing.

Solution:

  • Move to server-side data fetching using Server Components (App Router) or getStaticProps/getServerSideProps (Pages Router)
  • This way, content is already in the HTML when it reaches the browser

3. Font Loading (FOIT/FOUT)

If you're using custom fonts without proper optimization, text might be invisible (FOIT - Flash of Invisible Text) or render with fallback fonts first (FOUT - Flash of Unstyled Text), causing layout shifts as fonts load.

Solution:

  • Use next/font to automatically optimize font loading
  • This eliminates layout shift and prevents font flashing

4. Image Loading Without Dimensions

If you're using images without specified dimensions, content below them will shift as images load and establish their height.

Solution:

  • Use next/image with explicit width and height props
  • Add priority prop to above-the-fold images
  • This reserves space and prevents cumulative layout shift

5. CSS-in-JS / Styled Components FOUC

If you're using styled-components, Emotion, or similar CSS-in-JS libraries without proper SSR setup, styles may load after HTML, causing a Flash of Unstyled Content.

Solution:

6. Dynamic Imports with ssr: false

If you're using next/dynamic with { ssr: false }, those components won't render until client-side hydration completes, causing content to pop in.

Solution:

  • Only disable SSR when absolutely necessary (like for components that require window or browser APIs)
  • Add loading states to dynamic imports

Quick Diagnostic Steps

  1. Check Network Tab: Are multiple requests loading content after initial page load? That's client-side fetching.
  2. Check Performance Tab: Record a page load and look at the rendering timeline. Chrome DevTools can show you exactly when elements appear.
  3. Disable JavaScript: If content doesn't appear at all with JS disabled, you're relying too heavily on client-side rendering.
  4. Check for Hydration Errors: Look in your browser console for any React hydration mismatch warnings.

Most Likely Culprit

Given the "bottom-to-top" staggered pattern, I'd bet on client-side data fetching combined with Suspense streaming. If you're fetching Sanity content in individual components rather than at the page level, each component will resolve independently, creating that staggered effect.

Try consolidating all your Sanity queries into a single server-side fetch at the page level, then pass that data down to child components as props. This should eliminate the staggered loading entirely.

Show original thread
9 replies
Is the green section static html? And I’m assuming that the product details is loaded with ServerSideProps or purely client side with something like a
fetch
method being used in the main function?
Would help to know more about how your site is setup and how the data is fetched.
I would guess it is because the content is fetched async (getServerSideProps) in which case you should consider if ISG may be an option (or getStaticProps) or display a loading spinner while content is fetched.
May also be a FOUC issue if the CSS is not rendered serverside properly
Thanks for replying insanity, Thomas. I’m using
getStaticProps
to fetch data from Sanity, and subscription plans from ReCharge within my standard page component
pages/[…slug].js
:

export async function getStaticProps ({ preview = false, previewData, params }) {
  const { slug } = params
  const pageData = await getStaticPage(
    last(slug),
    {
      active: preview,
      token: previewData?.token
    }
  )

  let subscriptionProductData = null
  const product = get(pageData, 'page.product', false)

  if (product) {
    subscriptionProductData = await getSellingPlans(product.handle)
    if (subscriptionProductData) {
      pageData.page.subscriptions = subscriptionProductData
    }
  }

...
Within Sanity, I let users customise a page by organising modules into ‘slices’. They’re mapped over and rendered to the page like so:

import { useMemo } from 'react'
import forEach from 'lodash/forEach'
import ErrorBoundary from '../ErrorBoundary'
import dynamic from 'next/dynamic'

const ProductHero = dynamic(() => import('./ProductHero'))
const ShopGrid = dynamic(() => import('./ShopGrid'))
const AdditionalInfoSlice = dynamic(() => import('./AdditionalInfoSlice'))
const ReviewsSlice = dynamic(() => import('./ReviewsSlice'))
const TextColumnsSlice = dynamic(() => import('./TextColumnsSlice'))
const PagePreviewSlice = dynamic(() => import('./PagePreviewSlice'))

const sliceComponentSelector = {
  shop_grid_slice: ShopGrid,
  additional_info_slice: AdditionalInfoSlice,
  reviews_slice: ReviewsSlice,
  text_columns_slice: TextColumnsSlice,
  page_preview_tiles: PagePreviewSlice
}

const Slices = ({ page, settings, slices, summary }) => {
  const sliceComponents = useMemo(() => {
    const components = []
    forEach(slices, (slice, i) => {
      const Component = sliceComponentSelector[slice._type]
      if (Component) {
        components.push(
          <ErrorBoundary key={`slice-${slice._key}`}>
            <Component data={slice} page={page} summary={summary} first={i === 0} />
          </ErrorBoundary>
        )
      }
    })
    return components
  }, [slices, page])

  if (page.pageType === 'product') {
    return (
      <>
        <ProductHero page={page} settings={settings} />
        {sliceComponents}
      </>
    )
  }

  return sliceComponents
}

export default Slices
What is the output from
next build
?
It could be the dynamic import of the component, you may need to provide a ‘loading’ state ie. https://nextjs.org/docs/advanced-features/dynamic-import#with-custom-loading-component
Good shout, thanks for the lead 🕵️‍♂️
let us know how it goes

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?