To make recommendations load instantly and avoid hurting Core Web Vitals, reserve space in advance, show lightweight placeholders, and replace them with data atomically. Below is a step-by-step guide for a web developer to prevent layout shift (CLS) and keep LCP/INP in the green.
Goal: The browser must know an element's dimensions before its content loads.
| Step |
Action |
Recommendations |
| 1.1. Reserve dimensions |
Always set the width and height attributes for asynchronously loaded elements (images, videos, ad slots). |
Use aspect-ratio. If only the aspect ratio is known, use the "padding hack" for responsive layouts. |
<tr>
<td style={{width: '33.33%', textAlign: 'center', padding: '8px', border: '1px solid #ddd'}}><strong>1.2. Container styling</strong></td>
<td style={{width: '33.33%', textAlign: 'left', padding: '8px', border: '1px solid #ddd'}}>Create a fixed container (wrapper) for dynamic content that does not change its size.</td>
<td style={{width: '33.33%', textAlign: 'left', padding: '8px', border: '1px solid #ddd'}}>Set an expected height via <code>min-height</code> for elements with variable text length (product cards, comments, etc.).</td>
</tr>
Goal: Fill the reserved space with a visual mockup until data arrives.
| Step |
Action |
Recommendations |
| 2.1. Show the skeleton |
Inside the container from step 1.2, render a visual skeleton screen immediately. |
The skeleton should match the final content's size and proportions (e.g., areas for a title, avatar, and several lines of text). |
<tr>
<td style={{width: '33.33%', textAlign: 'center', padding: '8px', border: '1px solid #ddd'}}><strong>2.2. Style the skeleton</strong></td>
<td style={{width: '33.33%', textAlign: 'left', padding: '8px', border: '1px solid #ddd'}}>Style the skeleton to mimic the upcoming blocks (light-gray rectangles, subtle animation).</td>
<td style={{width: '33.33%', textAlign: 'left', padding: '8px', border: '1px solid #ddd'}}>Use plain CSS or ready-made skeleton libraries. <strong>Important:</strong> the skeleton block sizes must match the final sizes to avoid shifts.</td>
</tr>
Goal: Smoothly replace the placeholder with real data.
| Step |
Action |
Recommendations |
| 3.1. Fetch data |
Send an API request to retrieve real data. |
Use modern mechanisms (e.g., fetch or libraries like Axios). |
<tr>
<td style={{width: '33.33%', textAlign: 'center', padding: '8px', border: '1px solid #ddd'}}><strong>3.2. Render data</strong></td>
<td style={{width: '33.33%', textAlign: 'left', padding: '8px', border: '1px solid #ddd'}}>Once data is received, build the real content element (e.g., a product card) in memory (or as a hidden element).</td>
<td style={{width: '33.33%', textAlign: 'left', padding: '8px', border: '1px solid #ddd'}}>Ensure the real content occupies the same space as the skeleton, or at least does not exceed the space reserved in step 1.2.</td>
</tr>
<tr>
<td style={{width: '33.33%', textAlign: 'center', padding: '8px', border: '1px solid #ddd'}}><strong>3.3. Atomic swap</strong></td>
<td style={{width: '33.33%', textAlign: 'left', padding: '8px', border: '1px solid #ddd'}}>Replace the skeleton with the real content as soon as it is ready.</td>
<td style={{width: '33.33%', textAlign: 'left', padding: '8px', border: '1px solid #ddd'}}>Use a single DOM operation (node replacement or a fast class toggle) to minimize the time between removing the skeleton and showing the data.</td>
</tr>
Goal: Prevent shifts if dynamic content is empty.
| Step |
Action |
Recommendations |
| 4.1. No data returned |
If the API returns an empty dataset and the element isn't needed, choose one of the options below. |
Select an option: |
<tr>
<td style={{width: '33.33%', textAlign: 'center', padding: '8px', border: '1px solid #ddd'}}><strong>4.2. Option A: Remove</strong></td>
<td style={{width: '33.33%', textAlign: 'left', padding: '8px', border: '1px solid #ddd'}}>Remove the reserved container (from Stage 1) and the skeleton.</td>
<td style={{width: '33.33%', textAlign: 'left', padding: '8px', border: '1px solid #ddd'}}><strong>CLS risk:</strong> If the element was above the fold, removal may cause a small CLS for elements below. Do this only if the container is small and impact is minor.</td>
</tr>
<tr>
<td style={{width: '33.33%', textAlign: 'center', padding: '8px', border: '1px solid #ddd'}}><strong>4.3. Option B: Fallback</strong></td>
<td style={{width: '33.33%', textAlign: 'left', padding: '8px', border: '1px solid #ddd'}}>Replace the skeleton with a "no data" message (e.g., "No products available" or "No comments yet").</td>
<td style={{width: '33.33%', textAlign: 'left', padding: '8px', border: '1px solid #ddd'}}><strong>Best option:</strong> Preserves the reserved space (Stage 1), prevents jumps, and provides helpful feedback. The message should occupy the same area.</td>
</tr>
- Avoid inserting content above existing elements: Never inject dynamic content (banners, pop-ups) at the top of the page after the main content has loaded. If necessary, use modals or floating, fixed-position elements (
position: fixed) that do not affect layout.
- Web fonts: Use
font-display: swap, and if possible load a local version of the font or use size-adjust to reduce FOUT (Flash of Unstyled Text) or FOIT (Flash of Invisible Text), which can also cause CLS.
- Transforms instead of layout properties: For motion, prefer
transform: translate() over layout-triggering properties (top, left, width, height).