Most "make it faster" advice assumes you can rebuild. On real small-business sites — a clinic on WordPress, a retailer on a 2018 theme — you usually can't. Here's how we cut Interaction to Next Paint (INP) on a legacy site from ~480ms to under 200ms without a rewrite.
1. Find the slow interactions, don't guess
INP measures the worst interaction, not the average. Pull real-user data first:
import { onINP } from 'web-vitals';
onINP((metric) => {
// ship to your analytics endpoint
navigator.sendBeacon('/vitals', JSON.stringify({
value: metric.value,
target: metric.attribution?.interactionTarget,
}));
});
The interactionTarget tells you which element is slow — usually a menu toggle, a filter, or an "add to cart".
2. Break up long tasks
The usual culprit is a single handler doing too much synchronously. Yield to the main thread:
async function handleClick() {
updateUIImmediately(); // cheap, visible feedback first
await scheduler.yield?.() ?? new Promise(r => setTimeout(r));
doExpensiveWork(); // the rest, after paint
}
3. Defer third-party scripts
Chat widgets, pixels and analytics are the silent INP killers. Load them after first interaction or on idle:
<script>
addEventListener('load', () => requestIdleCallback(() => {
const s = document.createElement('script');
s.src = 'https://widget.example.com/chat.js';
document.body.append(s);
}));
</script>
4. Measure again
After these three changes, the legacy site moved from a failing INP to passing — no framework migration, no redesign.
If you want this run against your own legacy site, we do exactly this for Singapore SMEs at SGBP — performance and AI work on the site you already have. Happy to take a look — drop us a line.
— Daniel
Top comments (0)