Our services
If you can think it, we can make it brainsoft.
If you can think it, we can make it brainsoft.
A client forwards you a Lighthouse report with three red circles and asks which one matters. Or you inherit a site that scores 40 on mobile and you have a sprint to improve it. The temptation is to open a bundle analyser and start deleting dependencies, which is usually the wrong first move.
Fix in this order: cumulative layout shift first because it is the cheapest to remove, then the largest contentful paint element because it is the most visible to users, then interaction to next paint because it needs the most careful measurement. Work down that list and stop when the numbers are acceptable.
Layout shift is almost always a missing dimension. An image without width and height, a web font that swaps late, an ad slot that injects itself above the fold. The fixes are small, they rarely break anything, and the payoff shows up immediately in field data. If you only have an afternoon, spend it here.
Start by finding what actually moves. Open DevTools, go to the Performance panel, and record a page load with the Layout Shift regions overlay enabled. The shifts flash on screen as you scroll. In the console you can also watch them arrive:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log(entry.value, entry.sources.map(s => s.node));
}
}
}).observe({ type: 'layout-shift', buffered: true });
Each logged source points at the node that moved. Nine times out of ten it is an <img> with no intrinsic size, or a banner that renders after a fetch resolves. Reserve the space with an aspect ratio or a fixed min-height on the container, and the shift disappears. Fonts are the second cause: add font-display: swap and preload the one weight you actually use above the fold instead of shipping four.
LCP is one image or one block of text. Find out which. In the Performance panel, hover the LCP marker on the timeline and it names the node. That single fact decides everything that follows.
fetchpriority="high", and make sure it is not lazy-loaded.<img> or preload it.The most common mistake I see is loading="lazy" on the hero image. It was added for a below-the-fold gallery and got copy-pasted everywhere. Lazy loading the LCP element delays it by a full network round trip. Remove the attribute and re-measure before touching anything else.
Once the element is identified and correctly prioritised, look at the server. Time to first byte sets the floor for LCP, and no amount of front-end tuning beats a slow origin. If TTFB is over 800ms, the fix belongs in caching or infrastructure, and that is work we do regularly through our services.
Interaction to next paint measures the worst interaction latency across the whole visit. You cannot judge it from a lab run on a fast laptop. You need field data, either from the Chrome UX Report or from your own RUM, and you need to know which interaction is slow. A dropdown that takes 400ms on a mid-range Android will look instant on your dev machine.
When you do find the culprit, it is usually one of three things: a long task on the main thread from a third-party script, an event handler doing synchronous work it could defer, or a large re-render triggered by a state change that touches too much of the tree. Break the long task with scheduler.yield() or a setTimeout boundary, and move non-urgent work off the critical path:
button.addEventListener('click', async () => {
renderImmediateFeedback();
await scheduler.yield();
await runExpensiveUpdate();
});
Do not start here. INP work is the most invasive and the hardest to verify, and if you begin with it you will spend two weeks refactoring render logic while the layout still jumps on every page load.
Almost always, yes. Reserving space for images, dropping unused fonts, removing lazy loading from the hero, and deferring third-party scripts are all implementation changes. A redesign is rarely required to pass the thresholds.
Use Lighthouse to find specific problems, and field data to decide whether they matter. A lab score of 95 with poor field INP means real users on slower devices are struggling. Google ranks on field data, so that is the number to move.
The Core Web Vitals report uses a 28-day rolling window, so improvements appear gradually rather than overnight. Expect a few weeks before the URL groups flip from poor to good, even if your own RUM shows the change the same day.