Definition
You use this concept when you want to prevent the screen from jolting — where you show a gray skeleton during loading, and the moment the data arrives the content below is abruptly pushed up or down.
Cumulative Layout Shift (CLS, a cumulative score of how suddenly visible elements move without user input) is a layout-stability metric that accumulates whenever visible elements shift without user input. A loading skeleton (a gray placeholder UI that holds the space before content arrives) is not mere decoration but a layout placeholder (an empty box reserving the spot) for the space the final content will occupy. If the skeleton's row count, height, or width differs from the post-load UI, the content below is pushed the moment the data arrives, producing CLS.
Why it matters
Tables, lists, and card grids change DOM height before and after asynchronous data. If the skeleton is short (e.g., 10 rows) while the real data is long (e.g., 22 rows), shift accumulates not only below the fold but also around the summary bar and pagination. When SSR prefetch delivers data quickly, the skeleton→content transition becomes more noticeable, so prefetch and skeleton design must be treated as a set. A good CLS is generally 0.1 or below.
How it works
CLS adds, for each layout shift, impact fraction (the proportion of the viewport area the moved element occupied) × distance fraction (how far that element moved relative to the viewport height). If the skeleton occupies a smaller box than the real content, the impact grows on swap. A placeholder like animate-pulse that changes not only opacity but also width easily becomes a shift culprit in number and count areas.
| Cause | Typical symptom |
|---|---|
| Row-count mismatch | skeleton 10 rows → actual N rows, body height jumps |
| Insufficient fixed min-height | min-h-[600px] < actual table height |
| Variable-width text | loading bar → difference in actual digit count |
| Late mount | empty area → whole table appears suddenly |
In practice
- skeleton row count = pagination page size.
- Number slots:
min-width+tabular-nums; consider a static bar instead of pulse. - Verification: Performance trace
Layout Shifts/ CLS, median of 3 reloads in the same environment.
Trade-offs
| Choice | Benefit | Cost |
|---|---|---|
| skeleton as long as page size | CLS ↓ | temporary increase in DOM nodes while loading |
| large fixed min-height | stable | empty space when data is sparse |
| remove skeleton (hydrate only) | no shift | blank screen on cache miss |
| spinner only | fewer DOM | possible 0→N height shift |
When not to use it
- When height is fundamentally unpredictable (infinite editor, unmeasured dynamic content).
- When loading is extremely short and prefetch always succeeds so the skeleton is barely visible (still, if you target a good CLS, keeping the reservation is recommended).
Common mistakes
- "If there's a skeleton, CLS is safe" — if the size doesn't match, it actually gets worse.
- Using only a fixed
min-heightwhile ignoring the row count. - "No need to worry about the skeleton after SSR prefetch" — a query-key miss brings back the flash and CLS.
Related concepts
- critical-rendering-path — layout/paint order
- largest-contentful-paint — LCP can be fine while CLS is separate
- ssr-hydration-mismatch — consistency between the first client render and the server HTML
- ssr-prefetch-query-cache-hydration — reduce skeleton exposure itself via prefetch