Skip to content

Commit

Permalink
feat: load out of viewport sections after TTI (#2604)
Browse files Browse the repository at this point in the history
## What's the purpose of this pull request?

This PR aims to load the `out of viewport` sections after the TTI,
enhancing interactivity and performance. The key changes include the
addition of a new hook for Time To Interactive (TTI) measurement,
updates to the `LazyLoadingSection` and `ViewportObserver` components to
know when the browser isInteractive and load the other sections.

This PR also improves debug.

## How to test it?

You can see the sections loaded after the TTI in the Network tab of
DevTools using the [preview
link](https://starter-261g6dokj-vtex.vercel.app/).

### Starters Deploy Preview

https://starter-261g6dokj-vtex.vercel.app/

- vtex-sites/starter.store#641

### References
https://web.dev/articles/tti?hl=pt-br
https://web.dev/articles/tbt?hl=pt-br#how_does_tbt_relate_to_tti

https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
https://caniuse.com/?search=requestIdleCallback
https://caniuse.com/mdn-api_performanceobserver
  • Loading branch information
eduardoformiga authored Jan 15, 2025
1 parent b692b17 commit 98f8109
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 9 deletions.
63 changes: 55 additions & 8 deletions packages/core/src/components/cms/RenderSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import {
useMemo,
} from 'react'

import { useUI } from '@faststore/ui'
import { Section } from '@vtex/client-cms'
import dynamic from 'next/dynamic'
import useTTI from 'src/sdk/performance/useTTI'
import SectionBoundary from './SectionBoundary'
import ViewportObserver from './ViewportObserver'
import COMPONENTS from './global/Components'
Expand All @@ -16,6 +18,7 @@ interface Props {
components?: Record<string, ComponentType<any>>
globalSections?: Array<{ name: string; data: any }>
sections?: Array<{ name: string; data: any }>
isInteractive?: boolean
}

const SECTIONS_OUT_OF_VIEWPORT = ['CartSidebar', 'RegionModal']
Expand Down Expand Up @@ -49,20 +52,47 @@ const useDividedSections = (sections: Section[]) => {
export const LazyLoadingSection = ({
sectionName,
children,
debug = false,
isInteractive = false,
}: {
sectionName: string
children: ReactNode
debug?: boolean
isInteractive?: boolean
}) => {
const { cart: displayCart, modal: displayModal } = useUI()
if (SECTIONS_OUT_OF_VIEWPORT.includes(sectionName)) {
return <>{children}</>
const shouldLoad =
isInteractive ||
(sectionName === 'CartSidebar' && displayCart) ||
(sectionName === 'RegionModal' && displayModal)

if (debug) {
console.log(
`section SECTIONS_OUT_OF_VIEWPORT '${sectionName}' shouldLoad:`,
shouldLoad
)
}

return shouldLoad ? <>{children}</> : null
}

return (
<ViewportObserver sectionName={sectionName}>{children}</ViewportObserver>
<ViewportObserver
sectionName={sectionName}
debug={debug}
isInteractive={isInteractive}
>
{children}
</ViewportObserver>
)
}

const RenderSectionsBase = ({ sections = [], components }: Props) => {
const RenderSectionsBase = ({
sections = [],
components,
isInteractive,
}: Props) => {
return (
<>
{sections.map(({ name, data = {} }, index) => {
Expand All @@ -79,7 +109,10 @@ const RenderSectionsBase = ({ sections = [], components }: Props) => {

return (
<SectionBoundary key={`cms-section-${name}-${index}`} name={name}>
<LazyLoadingSection sectionName={name}>
<LazyLoadingSection
sectionName={name}
isInteractive={isInteractive}
>
<Component {...data} />
</LazyLoadingSection>
</SectionBoundary>
Expand All @@ -99,21 +132,35 @@ function RenderSections({
globalSections ?? sections
)

const { isInteractive } = useTTI()

return (
<>
{firstSections && (
<RenderSectionsBase sections={firstSections} components={components} />
<RenderSectionsBase
sections={firstSections}
components={components}
isInteractive={isInteractive}
/>
)}
{sections && sections.length > 0 && (
<RenderSectionsBase sections={sections} components={components} />
<RenderSectionsBase
sections={sections}
components={components}
isInteractive={isInteractive}
/>
)}
{children}
<LazyLoadingSection sectionName="Toast">
<LazyLoadingSection sectionName="Toast" isInteractive={isInteractive}>
<Toast />
</LazyLoadingSection>

{lastSections && (
<RenderSectionsBase sections={lastSections} components={components} />
<RenderSectionsBase
sections={lastSections}
components={components}
isInteractive={isInteractive}
/>
)}
</>
)
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/components/cms/ViewportObserver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type ViewportObserverProps = {
* Debug/test purposes: enables visual debugging to identify the visibility of the section.
*/
debug?: boolean
isInteractive?: boolean
} & IntersectionObserverInit

function ViewportObserver({
Expand All @@ -23,6 +24,7 @@ function ViewportObserver({
rootMargin,
children,
debug = false,
isInteractive = false,
}: PropsWithChildren<ViewportObserverProps>) {
const [isVisible, setVisible] = useState(false)
const ref = useRef<HTMLDivElement | null>(null)
Expand Down Expand Up @@ -80,7 +82,7 @@ function ViewportObserver({
></div>
)}

{isVisible && children}
{(isVisible || isInteractive) && children}
</>
)
}
Expand Down
35 changes: 35 additions & 0 deletions packages/core/src/sdk/performance/useTTI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useEffect, useState } from 'react'

const TTI_TIMEOUT = 5000 // 5 seconds without long tasks as a criterion for Time To Interactive - https://web.dev/articles/tti
export default function useTTI() {
const [isInteractive, setIsInteractive] = useState(false)

useEffect(() => {
if ('PerformanceObserver' in window) {
let lastTaskEnd = 0

const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
lastTaskEnd = entry.startTime + entry.duration
}
})

observer.observe({ type: 'longtask', buffered: true })

// Monitoring when TTI might have been reached
const checkTTI = () => {
const now = performance.now()
if (now - lastTaskEnd >= TTI_TIMEOUT) {
observer.disconnect()
setIsInteractive(true) // Sets the state to true when TTI is estimated
} else {
requestIdleCallback(checkTTI) // Keeps checking while the browser is idle
}
}

requestIdleCallback(checkTTI)
}
}, [])

return { isInteractive }
}

0 comments on commit 98f8109

Please sign in to comment.