Where layout shifts come from
Cumulative Layout Shift (CLS) is an aggregate score, but it doesn't tell you when or why the shift happened. To fix instability you need to look at individual shift events. The Layout Instability API makes that possible by exposing an array of every shift not preceded by user input:
new Promise(resolve => {
new PerformanceObserver(list => {
resolve(list.getEntries().filter(entry => !entry.hadRecentInput));
}).observe({type: "layout-shift", buffered: true});
}).then(console.log);
In this example a single tiny shift of 0.01% occurred at 210ms. That tells you the timing and severity of each event, which narrows down what could have caused it. From there you can move into a lab environment for more control.
Capturing layout shifts in WebPageTest
WebPageTest requires a custom metric to capture individual layout shifts. With Chrome 77 and later the Layout Instability API is enabled by default, so no command line flags or Canary builds are needed. To get the data as a WebPageTest custom metric, wrap the API call in a promise that resolves to a JSON string:
[LayoutShifts]
return new Promise(resolve => {
new PerformanceObserver(list => {
resolve(JSON.stringify(list.getEntries().filter(entry => !entry.hadRecentInput)));
}).observe({type: "layout-shift", buffered: true});
});
The promise resolves to a JSON representation of the array rather than the array itself because custom metrics can only return primitive values.
Finding the culprit in the filmstrip
Running this against ismyhostfastyet.com returned a single layout shift: 34.2% at 3087ms. The filmstrip view around the three-second mark shows exactly what caused it. The page fetches a JSON file asynchronously and renders it into a table. The table starts empty, and populating it after the fetch completes shifts the layout by over a third of the viewport.
There was also a secondary issue visible at visual completion around 4.3 seconds: the page's <h1> text appeared only after the web font loaded. The layout didn't shift in this case, but users still had to wait several seconds to read the title.
Fixing the table shift with placeholders
You can't know the table contents before the JSON loads, but you can pre-render the table with placeholder rows so the layout is stable from the initial paint. The placeholder data is randomly generated before being sorted:
function getRandomFiller(maxLength) {
var filler = '█';
var len = Math.ceil(Math.random() * maxLength);
return new Array(len).fill(filler).join('');
}
function getRandomDistribution() {
var fast = Math.random();
var avg = (1 - fast) * Math.random();
var slow = 1 - (fast + avg);
return [fast, avg, slow];
}
// Temporary placeholder data.
window.data = [];
for (var i = 0; i < 36; i++) {
var [fast, avg, slow] = getRandomDistribution();
window.data.push({
platform: getRandomFiller(10),
client: getRandomFiller(5),
n: getRandomFiller(1),
fast,
avg,
slow
});
}
updateResultsTable(sortResults(window.data, 'fast'));
The generator includes the block character repeated a random number of times to simulate text and a random distribution of the three main values. Desaturating the placeholder styling makes it clear the content hasn't loaded yet.
The specific appearance of placeholders doesn't affect layout stability. What matters is that the screen real estate is reserved, so users aren't left guessing whether content is missing.
Fixing the font delay
The web font issue is simpler. The site loads Google Fonts, so adding display=swap to the CSS request is enough. That maps to font-display: swap in the font declaration and lets the browser render fallback text immediately:
<link href="https://fonts.googleapis.com/css?family=Chivo:900&display=swap" rel="stylesheet">
Verifying the result
Rerunning through WebPageTest and comparing filmstrips shows a much smaller shift. The custom metric still records an event at around 3071ms, but the severity dropped from 34.2% to 0.005%:
[
{
"name": "",
"entryType": "layout-shift",
"startTime": 3070.9349999997357,
"duration": 0,
"value": 0.000050272187989256116,
"hadRecentInput": false,
"lastInputTime": 0
}
]
The filmstrip also confirms the <h1> now renders with a fallback system font earlier in the load.
Checking real user impact
Lab tests prove the optimization works in a controlled environment, but they don't confirm real users benefit. Layout instability experienced in the field is the signal that matters for the feedback loop. Collecting your own data is one option; the Chrome UX Report also includes CLS from real user experiences across millions of sites, so you can benchmark yourself or explore the broader state of layout stability on the web.



