Skip to content

Commit

Permalink
Feat: Adds intersection (ViewportObserver) component (#2498)
Browse files Browse the repository at this point in the history
## What's the purpose of this pull request?

This PR introduces the `ViewportObserver` component, which will be used
in future tasks as part of our performance improvement initiative.

## How it works?

We'll primarily use it on mobile devices to prevent sections outside the
viewport from being rendered initially. It checks whether a component or
section is within the viewport, based on a specified height. We're using
Moto G Power devices as the reference for this.

## How to test it?

To facilitate the test for our future tasks, I have added a `debug` mode
prop, so we can enable it for testing.
In this case, we can identify if the component is IN or OUT the
viewport. **This is only for the initial render.**

1. go to `core/src/components/product/ProductGrid/ProductGrid.tsx`, add
`debug` prop to following component:

```
  <ViewportObserver name="UIProductGrid-out-viewport" debug>
```

2. navigate to core, run `yarn dev`

3. go to the PLP, or http://localhost:3000/office?page=0

4. set the browser device to  Moto G Power
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/b66c48fe-35de-4062-9265-6ca7801f935e">

5. checking via console.log when the component is IN or OUT
- Only the first row of the ProductGrid is inside the viewport, so you
should see `OUT` in the console.log. Scrolling up the next row should
appear, and you will be able to see `IN`

<img width="500" alt="image"
src="https://github.com/user-attachments/assets/e780ee08-dcaa-4899-8721-4eb12d9d8e7d">

6. checking visually when the component is IN or OUT
- Go to `Network` tab, selects `Slow 4G` as preset, refresh the page
- You will able to see a quick visual feedback (pink rectangle and blue
border) in the first ProductCard of the second row.


https://github.com/user-attachments/assets/87f902b6-2e5a-47bd-8b21-3b7897de9bdd

7. checking if the first two ProductCard image is loading=`eager` and
the rest `loading=`lazy`

<img width="500" alt="loading-images"
src="https://github.com/user-attachments/assets/76d72d87-757c-48b9-9634-c4394a93858b">


### Starters Deploy Preview


## References

https://vtex-dev.atlassian.net/browse/SFS-1507
#2404

---------

Co-authored-by: Larícia Mota <laricia.mota@vtex.com.br>
  • Loading branch information
hellofanny and lariciamota authored Oct 11, 2024
1 parent 397a6ee commit 3885116
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 26 deletions.
88 changes: 88 additions & 0 deletions packages/core/src/components/cms/ViewportObserver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { PropsWithChildren } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'

// Mobile height to prevent sections outside the viewport from being rendered initially.
// We are using the Moto G Power device measurement as a reference, as used by PageSpeed Insights.
const VIEWPORT_SIZE = 823

type ViewportObserverProps = {
/**
* Identify the store section
*/
sectionName?: string
/**
* Debug/test purposes: enables visual debugging to identify the visibility of the section.
*/
debug?: boolean
} & IntersectionObserverInit

function ViewportObserver({
sectionName = '',
threshold = 0,
root = null,
rootMargin,
children,
debug = false,
}: PropsWithChildren<ViewportObserverProps>) {
const [isVisible, setVisible] = useState(false)
const ref = useRef<HTMLDivElement | null>(null)

const observerCallback = useCallback(
([entry]: IntersectionObserverEntry[], obs: IntersectionObserver) => {
if (entry.isIntersecting) {
if (debug) {
console.log(`section '${sectionName}' VISIBLE`)
document.body.style.border = '2px solid green'
}
setVisible(true)
if (ref.current) {
obs.unobserve(ref.current)
}
} else {
setVisible(false)
if (debug) {
console.log(`section '${sectionName}' NOT VISIBLE`)
document.body.style.border = '2px solid red'
document.body.style.height = `${VIEWPORT_SIZE}px`
document.body.style.boxSizing = 'border-box'
}
}
},
[debug, sectionName]
)

useEffect(() => {
const observer = new IntersectionObserver(observerCallback, {
root,
rootMargin,
threshold,
})

if (ref.current) {
observer.observe(ref.current)
}

return () => observer.disconnect()
}, [observerCallback, root, rootMargin, threshold])

return (
<>
{!isVisible && (
<div
data-store-section-name={sectionName}
ref={ref}
style={{
border: debug ? '2px solid red' : undefined,
backgroundColor: debug ? 'red' : undefined,
height: VIEWPORT_SIZE, // required to make sections out of the viewport to be rendered on demand
width: '100%',
}}
></div>
)}

{isVisible && children}
</>
)
}

export default ViewportObserver
103 changes: 82 additions & 21 deletions packages/core/src/components/product/ProductGrid/ProductGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ProductGridSkeleton from 'src/components/skeletons/ProductGridSkeleton'
import { ProductCardProps } from '../ProductCard'

import { memo } from 'react'
import ViewportObserver from 'src/components/cms/ViewportObserver'
import { useOverrideComponents } from 'src/sdk/overrides/OverrideContext'

interface Props {
Expand All @@ -26,45 +27,105 @@ interface Props {
ProductCardProps,
'showDiscountBadge' | 'bordered' | 'taxesConfiguration'
>
/**
* Identify the number of firstPage
*/
firstPage?: number
}

function ProductGrid({
products,
page,
pageSize,
productCard: { showDiscountBadge, bordered, taxesConfiguration } = {},
firstPage,
}: Props) {
const { __experimentalProductCard: ProductCard } =
useOverrideComponents<'ProductGallery'>()
const aspectRatio = 1

// TODO: Check if is also isMobile
const isFirstPage = firstPage === page

return (
<ProductGridSkeleton
aspectRatio={aspectRatio}
loading={products.length === 0}
>
<UIProductGrid>
{products.map(({ node: product }, idx) => (
<UIProductGridItem key={`${product.id}`}>
<ProductCard.Component
aspectRatio={aspectRatio}
imgProps={{
width: 150,
height: 150,
sizes: '30vw',
loading: idx === 0 ? 'eager' : 'lazy',
}}
{...ProductCard.props}
bordered={bordered ?? ProductCard.props.bordered}
showDiscountBadge={
showDiscountBadge ?? ProductCard.props.showDiscountBadge
}
product={product}
index={pageSize * page + idx + 1}
taxesConfiguration={taxesConfiguration}
/>
</UIProductGridItem>
))}
{isFirstPage ? (
<>
{products.slice(0, 2).map(({ node: product }, idx) => (
<UIProductGridItem key={`${product.id}`}>
<ProductCard.Component
aspectRatio={aspectRatio}
imgProps={{
width: 150,
height: 150,
sizes: '30vw',
loading: 'eager',
}}
{...ProductCard.props}
bordered={bordered ?? ProductCard.props.bordered}
showDiscountBadge={
showDiscountBadge ?? ProductCard.props.showDiscountBadge
}
product={product}
index={pageSize * page + idx + 1}
taxesConfiguration={taxesConfiguration}
/>
</UIProductGridItem>
))}
<></>
<ViewportObserver sectionName="UIProductGrid-out-viewport">
{products.slice(2).map(({ node: product }, idx) => (
<UIProductGridItem key={`${product.id}`}>
<ProductCard.Component
aspectRatio={aspectRatio}
imgProps={{
width: 150,
height: 150,
sizes: '30vw',
loading: 'lazy',
}}
{...ProductCard.props}
bordered={bordered ?? ProductCard.props.bordered}
showDiscountBadge={
showDiscountBadge ?? ProductCard.props.showDiscountBadge
}
product={product}
index={pageSize * page + idx + 1}
taxesConfiguration={taxesConfiguration}
/>
</UIProductGridItem>
))}
</ViewportObserver>
</>
) : (
<>
{products.map(({ node: product }, idx) => (
<UIProductGridItem key={`${product.id}`}>
<ProductCard.Component
aspectRatio={aspectRatio}
imgProps={{
width: 150,
height: 150,
sizes: '30vw',
loading: idx === 0 ? 'eager' : 'lazy',
}}
{...ProductCard.props}
bordered={bordered ?? ProductCard.props.bordered}
showDiscountBadge={
showDiscountBadge ?? ProductCard.props.showDiscountBadge
}
product={product}
index={pageSize * page + idx + 1}
taxesConfiguration={taxesConfiguration}
/>
</UIProductGridItem>
))}
</>
)}
</UIProductGrid>
</ProductGridSkeleton>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ import ProductGridSkeleton from 'src/components/skeletons/ProductGridSkeleton'
import { ProductCardProps } from 'src/components/product/ProductCard'
import { FilterSliderProps } from 'src/components/search/Filter/FilterSlider'
import { SortProps } from 'src/components/search/Sort/Sort'
import { useDelayedFacets } from 'src/sdk/search/useDelayedFacets'
import { useDelayedPagination } from 'src/sdk/search/useDelayedPagination'
import { useOverrideComponents } from 'src/sdk/overrides/OverrideContext'
import {
PLPContext,
SearchPageContext,
usePage,
} from 'src/sdk/overrides/PageProvider'
import { useProductsPrefetch } from 'src/sdk/product/useProductsPrefetch'
import { useOverrideComponents } from 'src/sdk/overrides/OverrideContext'
import { useDelayedFacets } from 'src/sdk/search/useDelayedFacets'
import { useDelayedPagination } from 'src/sdk/search/useDelayedPagination'

const ProductGalleryPage = lazy(() => import('./ProductGalleryPage'))
const GalleryPageSkeleton = <ProductGridSkeleton loading />
Expand Down Expand Up @@ -237,6 +237,7 @@ function ProductGallery({
title={title}
productCard={productCard}
itemsPerPage={itemsPerPage}
firstPage={pages[0]}
/>
))}
</Suspense>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import ProductGrid from 'src/components/product/ProductGrid'
import Sentinel from 'src/sdk/search/Sentinel'

import { ProductCardProps } from 'src/components/product/ProductCard'
import { memo } from 'react'
import { ProductCardProps } from 'src/components/product/ProductCard'
import { useGalleryPage } from 'src/sdk/product/usePageProductsQuery'

interface Props {
Expand All @@ -13,9 +13,16 @@ interface Props {
'showDiscountBadge' | 'bordered' | 'taxesConfiguration'
>
itemsPerPage: number
firstPage: number
}

function ProductGalleryPage({ page, title, productCard, itemsPerPage }: Props) {
function ProductGalleryPage({
page,
title,
productCard,
itemsPerPage,
firstPage,
}: Props) {
const { data } = useGalleryPage(page)

const products = data?.search?.products?.edges ?? []
Expand All @@ -33,6 +40,7 @@ function ProductGalleryPage({ page, title, productCard, itemsPerPage }: Props) {
page={page}
pageSize={itemsPerPage}
productCard={productCard}
firstPage={firstPage}
/>
</>
)
Expand Down

0 comments on commit 3885116

Please sign in to comment.