1 of 77

Improving Web Responsiveness

INP fixes from real-world cases.

Andrea Verlicchi @verlok andreaverlicchi.eu

performance.sync() 2025

2 of 77

😣

🤩

3 of 77

How to avoid the most common mistakes �that slow down UI responsiveness.

In 15 minutes…

4 of 77

Andrea�Web perf geek

Web → Frontend → Ecomm → Web Perf

@verlok

5 of 77

5

Behold,�people,

a PWA!

London, 2019

6 of 77

Make your website load instantly

Preloading and prerenderingwith AI click prediction

Dynamic caching and accelerationof personalized content

Zero infrastructure changesfor the existing website setup

Seamless plug & play integrationwithin days, instead of weeks

7 of 77

  • Three common errors I keep seeing
  • What causes each slowdown
  • How to avoid slowdowns on your website

What we'll cover

8 of 77

Have you improved INP?

Never really tried

Tried, but failed

Yes, successfully!

9 of 77

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

🏆

10 of 77

The most common mistake.

Sync Analytics

11 of 77

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

12 of 77

1100 ms

Problematic calls

13 of 77

function selectSizeHandler() {

selectSize();

pushToTagManager();

}

14 of 77

function selectSizeHandler() {

selectSize();

pushToTagManager();

}

15 of 77

async function selectSizeHandler() {

selectSize();

pushToTagManager();

}

16 of 77

async function selectSizeHandler() {

selectSize();

await yieldToMain();

pushToTagManager();

}

17 of 77

async function selectSizeHandler() {

selectSize();

await yieldToMain();

pushToTagManager();

}

function yieldToMain() {

}

18 of 77

async function selectSizeHandler() {

selectSize();

await yieldToMain();

pushToTagManager();

}

function yieldToMain() {

return scheduler.yield();

}

19 of 77

async function selectSizeHandler() {

selectSize();

await yieldToMain();

pushToTagManager();

}

function yieldToMain() {

if ("scheduler" in window && "yield" in scheduler) {

return scheduler.yield();

}

}

20 of 77

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);

});

}

21 of 77

Always yield to the main thread before calling tracking functions

(or any other long-running function)

🎯

🏆

22 of 77

“But Andrea, the synchronous tracking code is secretly hidden deep within

Google Tag Manager!”

23 of 77

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

Not enough?

Search yieldUnlessUrgent → Jacob Groß’s https://kurtextrem.de/posts/improve-inp (hacky)

Update.

Make sure you’re using the latest version of the tags.

24 of 77

Learning

Synchronous tracking functions frequently slow down user interactions.

Recommendation

Check how your website handles event tracking calls to analytics platforms.

🏆

25 of 77

Inducing style recalculation

26 of 77

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

27 of 77

250 ms

Presentation delay

28 of 77

<html>

<head>

...

</head>

<body

class="..."

>

...

</body>

</html>

29 of 77

<html>

<head>

...

</head>

<body

class="..."

style="--app-header-height: 223px; --app-header-offset-top: 223px"

>

...

</body>

</html>

30 of 77

“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

31 of 77

<html>

<head>

...

</head>

<body

class="..."

>

...

</body>

</html>

#0

Could we use classes instead?

32 of 77

<html>

<head>

...

</head>

<body

class="... app-header__visible"

>

...

</body>

</html>

33 of 77

<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

34 of 77

<body

class="..."

>

<header

class="..."

style="--app-header-height: 223px; --app-header-offset-top: 223px"

>

...

</header>

<main

class="..."

>

...

</main>

</body>

35 of 77

<body

class="..."

>

<header

class="..."

>

...

</header>

<main

class="..."

style="--app-header-height: 223px; --app-header-offset-top: 223px"

>

...

</main>

</body>

36 of 77

<body

class="..."

style="--app-header-height: 223px; --app-header-offset-top: 223px"

>

<header

class="..."

>

...

</header>

<main

class="..."

>

...

</main>

</body>

#2

Avoid CSS properties inheritance

37 of 77

<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",

});

38 of 77

<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",

});

39 of 77

<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>",

});

40 of 77

<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,

});

41 of 77

<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",

});

42 of 77

<body

class="..."

style="--app-header-height: 223px; --app-header-offset-top: 223px"

>

<header

class="..."

>

...

</header>

<main

class="..."

>

...

</main>

</body>

#3

“Is this even used?”

43 of 77

<body

class="..."

>

<header

class="..."

>

...

</header>

<main

class="..."

>

...

</main>

</body>

44 of 77

110 ms

Presentation delay

45 of 77

Set custom properties close to where you need them (if you need them)

And disable their inheritance if not required

🎯

46 of 77

All at once.

Too much JS

47 of 77

Landing page

Landing page

2 s

48 of 77

49 of 77

50 of 77

51 of 77

52 of 77

53 of 77

54 of 77

55 of 77

Don’t use requestAnimationFrame�for scheduling non-animation work

🎯

56 of 77

3

Of them are above-the-fold.

7

Carousels initialized.

57 of 77

const sliders = document.querySelectorAll(".my-slider");

58 of 77

const sliders = document.querySelectorAll(".my-slider");

const initializeSlider = async (slider) => {

};

59 of 77

const sliders = document.querySelectorAll(".my-slider");

const initializeSlider = async (slider) => {

};

const observer = new IntersectionObserver((entries, observer) => {

});

60 of 77

const sliders = document.querySelectorAll(".my-slider");

const initializeSlider = async (slider) => {

};

const observer = new IntersectionObserver((entries, observer) => {

});

sliders.forEach((slider) => {

observer.observe(slider);

});

61 of 77

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);

});

62 of 77

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);

});

63 of 77

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);

});

64 of 77

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);

});

65 of 77

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);

});

66 of 77

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);

});

67 of 77

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);

});

68 of 77

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);

});

69 of 77

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);

});

70 of 77

“Do we still need this code?”

71 of 77

72 of 77

73 of 77

74 of 77

Run only the Javascript code �that is currently needed

And delay everything else

🎯

75 of 77

Wrapping up

76 of 77

  • Yield to the main thread before calling tracking functions
  • Before calling tracking functions, yield to the main thread 😉
  • Set custom CSS properties close to where you need them
  • Check requestAnimationFrame() is only used for animations
  • Delay all the Javascript code that is not currently needed

Takeaways

77 of 77

I’d love your feedback

1-minute �feedback

Thank you 👋