Improving Web Responsiveness
INP fixes from real-world cases.
Andrea Verlicchi @verlok andreaverlicchi.eu
performance.sync() 2025
😣
🤩
How to avoid the most common mistakes �that slow down UI responsiveness.
In 15 minutes…
Andrea�Web perf geek
Web → Frontend → Ecomm → Web Perf
@verlok
5
Behold,�people,
a PWA!
London, 2019
Make your website load instantly
Preloading and prerendering �with AI click prediction
Dynamic caching and acceleration �of personalized content
Zero infrastructure changes �for the existing website setup
Seamless plug & play integration �within days, instead of weeks
What we'll cover
Have you improved INP?
Never really tried
Tried, but failed
Yes, successfully!
Forced reflow
Repeatedly inducing style recalculation or layout/reflow.
Sync analytics
Sending analytics beacons upon user interactions. Synchronously.
Too much JS
Executing too much UI-related JavaScript at once.
Common mistakes
🏆
The most common mistake.
Sync Analytics
Fancy T-shirt
Available colors
Choose your size
S
M
XS
XL
L
Express delivery in 1 day
1100 ms
Fancy T-shirt
Available colors
Choose your size
S
M
XS
XL
L
Express delivery in 1 day
1100 ms
Problematic calls
function selectSizeHandler() {
selectSize();
pushToTagManager();
}
function selectSizeHandler() {
selectSize();
pushToTagManager();
}
async function selectSizeHandler() {
selectSize();
pushToTagManager();
}
async function selectSizeHandler() {
selectSize();
await yieldToMain();
pushToTagManager();
}
async function selectSizeHandler() {
selectSize();
await yieldToMain();
pushToTagManager();
}
function yieldToMain() {
}
async function selectSizeHandler() {
selectSize();
await yieldToMain();
pushToTagManager();
}
function yieldToMain() {
return scheduler.yield();
}
async function selectSizeHandler() {
selectSize();
await yieldToMain();
pushToTagManager();
}
function yieldToMain() {
if ("scheduler" in window && "yield" in scheduler) {
return scheduler.yield();
}
}
async function selectSizeHandler() {
selectSize();
await yieldToMain();
pushToTagManager();
}
function yieldToMain() {
if ("scheduler" in window && "yield" in scheduler) {
return scheduler.yield();
}
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
Always yield to the main thread before calling tracking functions
(or any other long-running function)
🎯
🏆
“But Andrea, the synchronous tracking code is secretly hidden deep within
Google Tag Manager!”
Remove.
Understand what’s in there but not used anymore. Remove them if this is the case.
Audit.
Get access to your GTM container and check what code is running in there.
Slow GTM code
Update.
Make sure you’re using the latest version of the tags.
Learning
Synchronous tracking functions frequently slow down user interactions.
Recommendation
Check how your website handles event tracking calls to analytics platforms.
🏆
Inducing style recalculation
Wow text
Nullam dictum lobortis blandit. Fusce a sapien et dolor laoreet dignissim eget vitae metus. Etiam quis orci sollicitudin, interdum arcu eu, hendrerit nisl.
Maecenas vitae lacinia nisi. In porta, nunc
Landing page
Install our app
It’s the best
Let’s go
250 ms
Wow text
Nullam dictum lobortis blandit. Fusce a sapien et dolor laoreet dignissim eget vitae metus. Etiam quis orci sollicitudin, interdum arcu eu, hendrerit nisl.
Maecenas vitae lacinia nisi. In porta, nunc nec sodales suscipit, diam nunc placerat odio, at posuere turpis sapien a lacus. Integer varius ornare lectus egestas commodo.
Landing page
250 ms
Presentation delay
<html>
<head>
...
</head>
<body
class="..."
>
...
</body>
</html>
<html>
<head>
...
</head>
<body
class="..."
style="--app-header-height: 223px; --app-header-offset-top: 223px"
>
...
</body>
</html>
“Custom CSS properties … slower style recalc … as all children are affected”
“We have optimizations in place [..] but if you're adding new custom properties [..] we have to go down the slow path. So this is expected.”
Issue 342214592
<html>
<head>
...
</head>
<body
class="..."
>
...
</body>
</html>
#0
Could we use classes instead?
<html>
<head>
...
</head>
<body
class="... app-header__visible"
>
...
</body>
</html>
<body
class="..."
style="--app-header-height: 223px; --app-header-offset-top: 223px"
>
<header
class="..."
>
...
</header>
<main
class="..."
>
...
</main>
</body>
#1
Assign custom props to lower levels of the DOM tree
<body
class="..."
>
<header
class="..."
style="--app-header-height: 223px; --app-header-offset-top: 223px"
>
...
</header>
<main
class="..."
>
...
</main>
</body>
<body
class="..."
>
<header
class="..."
>
...
</header>
<main
class="..."
style="--app-header-height: 223px; --app-header-offset-top: 223px"
>
...
</main>
</body>
<body
class="..."
style="--app-header-height: 223px; --app-header-offset-top: 223px"
>
<header
class="..."
>
...
</header>
<main
class="..."
>
...
</main>
</body>
#2
Avoid CSS properties inheritance
<body
class="..."
style="--app-header-height: 223px; --app-header-offset-top: 223px"
>
<header
class="..."
>
...
</header>
<main
class="..."
>
...
</main>
</body>
Javascript
CSS.registerProperty({
name: "--app-header-height",
});
<body
class="..."
style="--app-header-height: 223px; --app-header-offset-top: 223px"
>
<header
class="..."
>
...
</header>
<main
class="..."
>
...
</main>
</body>
Javascript
CSS.registerProperty({
name: "--app-header-height",
});
CSS.registerProperty({
name: "--app-header-offset-top",
});
<body
class="..."
style="--app-header-height: 223px; --app-header-offset-top: 223px"
>
<header
class="..."
>
...
</header>
<main
class="..."
>
...
</main>
</body>
Javascript
CSS.registerProperty({
name: "--app-header-height",
syntax: "<length>",
});
CSS.registerProperty({
name: "--app-header-offset-top",
syntax: "<length>",
});
<body
class="..."
style="--app-header-height: 223px; --app-header-offset-top: 223px"
>
<header
class="..."
>
...
</header>
<main
class="..."
>
...
</main>
</body>
Javascript
CSS.registerProperty({
name: "--app-header-height",
syntax: "<length>",
inherits: false,
});
CSS.registerProperty({
name: "--app-header-offset-top",
syntax: "<length>",
inherits: false,
});
<body
class="..."
style="--app-header-height: 223px; --app-header-offset-top: 223px"
>
<header
class="..."
>
...
</header>
<main
class="..."
>
...
</main>
</body>
Javascript
CSS.registerProperty({
name: "--app-header-height",
syntax: "<length>",
inherits: false,
initialValue: "223px",
});
CSS.registerProperty({
name: "--app-header-offset-top",
syntax: "<length>",
inherits: false,
initialValue: "223px",
});
<body
class="..."
style="--app-header-height: 223px; --app-header-offset-top: 223px"
>
<header
class="..."
>
...
</header>
<main
class="..."
>
...
</main>
</body>
#3
“Is this even used?”
<body
class="..."
>
<header
class="..."
>
...
</header>
<main
class="..."
>
...
</main>
</body>
110 ms
Presentation delay
Set custom properties close to where you need them (if you need them)
And disable their inheritance if not required
🎯
All at once.
Too much JS
Landing page
Landing page
2 s
Don’t use requestAnimationFrame�for scheduling non-animation work
🎯
3
Of them are above-the-fold.
7
Carousels initialized.
const sliders = document.querySelectorAll(".my-slider");
const sliders = document.querySelectorAll(".my-slider");
const initializeSlider = async (slider) => {
};
const sliders = document.querySelectorAll(".my-slider");
const initializeSlider = async (slider) => {
};
const observer = new IntersectionObserver((entries, observer) => {
});
const sliders = document.querySelectorAll(".my-slider");
const initializeSlider = async (slider) => {
};
const observer = new IntersectionObserver((entries, observer) => {
});
sliders.forEach((slider) => {
observer.observe(slider);
});
const sliders = document.querySelectorAll(".my-slider");
const initializeSlider = async (slider) => {
if (slider && !slider.classList.contains("slider-initialized")) {
}
};
const observer = new IntersectionObserver((entries, observer) => {
});
sliders.forEach((slider) => {
observer.observe(slider);
});
const sliders = document.querySelectorAll(".my-slider");
const initializeSlider = async (slider) => {
if (slider && !slider.classList.contains("slider-initialized")) {
await yieldToMain();
}
};
const observer = new IntersectionObserver((entries, observer) => {
});
sliders.forEach((slider) => {
observer.observe(slider);
});
const sliders = document.querySelectorAll(".my-slider");
const initializeSlider = async (slider) => {
if (slider && !slider.classList.contains("slider-initialized")) {
await yieldToMain();
new Swiper(slider, { /* Options */ });
}
};
const observer = new IntersectionObserver((entries, observer) => {
});
sliders.forEach((slider) => {
observer.observe(slider);
});
const sliders = document.querySelectorAll(".my-slider");
const initializeSlider = async (slider) => {
if (slider && !slider.classList.contains("slider-initialized")) {
await yieldToMain();
new Swiper(slider, { /* Options */ });
slider.classList.add("slider-initialized");
}
};
const observer = new IntersectionObserver((entries, observer) => {
});
sliders.forEach((slider) => {
observer.observe(slider);
});
const sliders = document.querySelectorAll(".my-slider");
const initializeSlider = async (slider) => {
if (slider && !slider.classList.contains("slider-initialized")) {
await yieldToMain();
new Swiper(slider, { /* Options */ });
slider.classList.add("slider-initialized");
}
};
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
});
});
sliders.forEach((slider) => {
observer.observe(slider);
});
const sliders = document.querySelectorAll(".my-slider");
const initializeSlider = async (slider) => {
if (slider && !slider.classList.contains("slider-initialized")) {
await yieldToMain();
new Swiper(slider, { /* Options */ });
slider.classList.add("slider-initialized");
}
};
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
}
});
});
sliders.forEach((slider) => {
observer.observe(slider);
});
const sliders = document.querySelectorAll(".my-slider");
const initializeSlider = async (slider) => {
if (slider && !slider.classList.contains("slider-initialized")) {
await yieldToMain();
new Swiper(slider, { /* Options */ });
slider.classList.add("slider-initialized");
}
};
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
initializeSlider(entry.target)
}
});
});
sliders.forEach((slider) => {
observer.observe(slider);
});
const sliders = document.querySelectorAll(".my-slider");
const initializeSlider = async (slider) => {
if (slider && !slider.classList.contains("slider-initialized")) {
await yieldToMain();
new Swiper(slider, { /* Options */ });
slider.classList.add("slider-initialized");
}
};
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
initializeSlider(entry.target)
.then(() => { observer.unobserve(entry.target); })
}
});
});
sliders.forEach((slider) => {
observer.observe(slider);
});
const sliders = document.querySelectorAll(".my-slider");
const initializeSlider = async (slider) => {
if (slider && !slider.classList.contains("slider-initialized")) {
await yieldToMain();
new Swiper(slider, { /* Options */ });
slider.classList.add("slider-initialized");
}
};
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
initializeSlider(entry.target)
.then(() => { observer.unobserve(entry.target); })
.catch(console.error);
}
});
});
sliders.forEach((slider) => {
observer.observe(slider);
});
“Do we still need this code?”
W3C Editor’s Draft https://drafts.csswg.org/css-overflow-5/
Run only the Javascript code �that is currently needed
And delay everything else
🎯
Wrapping up
Takeaways
I’d love your feedback
1-minute �feedback
Thank you 👋