Layout shifts are a cause of bad user experience during the page load, to eliminate it, the following actions are required:

1. Locate a layout shift to resolve

To begin optimizing layout shifts, first locate one. Prefer starting with shifting elements a top of the page first, because most often cases of layout shifts issues are when elements at top are rendered while below elements already present on page, hence causing them to shift.

Follow this article to select one:

How to locate the layout shifts?

For instance, let’s consider the layout shift from previous article:

First layout shift happened when application has rendered Header component at top of the page.

With pausing scripts execution it is visible what location had element main before header was Arendered.

Untitled

When Header component is rendered, element main is shifted to bottom.

Untitled

Element main it has style property top with value of header height.

Untitled

At first location of element main is calculated depending on beginning of page, but when Header component appears on page, property top is moving element main depending on header layout block end.

Untitled

Untitled

To solve this layout shift issue it is required to investigate how Header element is rendered.

2. How to create placeholder for element

From investigating the code, Header component is rendered in Router component with using lazy load, which means that this component will be loaded with delay depending on how long it is required to load chunk where .

// Lazy load of Header component
export const Header = lazy(
		() => import(
				/* webpackMode: "lazy", webpackChunkName: "header" */
				'Component/Header'
		),
);

// Map with components declaration
[BEFORE_ITEMS_TYPE] = [
        ...
        {
            component: <Header />,
            position: 20,
            name: HEADER
        },
        ...
    ];

While chunk for Header component is loading, Router component is using Suspense with provided fallback to render all components from BEFORE_ITEMS_TYPE array. From code it is observable that fallback in this case is a tag main without loader.

	
// Suspense of lazy load element with fallback
renderSectionOfType(BEFORE_ITEMS_TYPE) {
      return (
					// Suspense component with provided fallback and
          <Suspense fallback={ this.renderFallbackPage() }>
              { this.renderComponentsOfType(BEFORE_ITEMS_TYPE) }
          </Suspense>
      );
  }

// Fallback for Suspense
renderFallbackPage(showLoader = false) {
      return (
          <main block="Router" elem="Loader">
              { showLoader && <Loader isLoading /> }
          </main>
      );
}

From application rendering perspective it is shown as empty tag main without any height. Because there is not proper styles for it.

Untitled

Since Suspense component is wrapping each component from array BEFORE_ITEMS_TYPE it is required to create separate fallback for each element and for header its own to not break other elements behavior.

Lets create a new fallback and assign it to Header component in array.

// Map with components declaration
[BEFORE_ITEMS_TYPE] = [
      ...
      {
          component: <Header />,
          position: 20,
          name: HEADER,
					// Assign fallback for Header component
					fallback: this.renderHeaderFallback(),
      },
      ...
];

// Header component 
renderHeaderFallback() {
	    return (
	        <div block="Router" elem="HeaderPlaceholder" />
	    );
}

Now it is possible to adjust rendering of BEFORE_ITEMS_TYPE elements to provide for each element its own Suspense wrapper with fallback provided from array or default one.

renderComponentsOfType(type) {
    return this.getSortedItems(type)
        .map(
            ({ position, component, fallback }) => {
                const componentToRender = (
                    <Suspense fallback={ fallback || this.renderFallbackPage() }>
                        { component }
                    </Suspense>
                );

                return cloneElement(componentToRender, { key: position });
            }
        );
  }

Also it is required to add styles for our fallback on Header component, otherwise it won’t be visible and have any impact on improving layout shift. Lets provide simple animation properties and height for our block.

.Router {
    &-HeaderPlaceholder {
        height: var(--header-total-height);
        background-image: var(--placeholder-image);
        background-size: var(--placeholder-size);
        animation: var(--placeholder-animation);
    }
}

In result, when Header component will be loaded, it will replace simple placeholder and visually page will be loading without critical layout shifts. (To make it perfect, it is required also to adjust breadcrumbs placeholder styles).

Peek 2023-02-05 22-00.gif

Peek 2023-02-05 12-46.gif