Web performance covers the conditions that let a page arrive fast, display without moving and respond without delay, on each visitor's real device and network. This guide goes through the checks we run during our Core Web Vitals work, from server to browser, with the fix for each, as applied on this site, or not yet.
Every section separates what Google documents, with the Search Central or web.dev page we consulted, from what we recommend, which is ours alone.
Not every performance problem carries the same weight
A tool report lists thirty flat audits. We rank every problem on three levels, and each section states the typical level of what it covers.
- Blocking: the page is slow for most real visitors. Examples: a multi-second TTFB on every page because nothing is cached; a multi-megabyte hero image, lazy-loaded.
- Important: one field metric is out of threshold on a busy template. Examples: fonts that make the text jump on display; a third-party script blocking the main thread on every interaction.
- Optimisation: thresholds are met, comfort can improve. Examples: a whole stylesheet loaded for a page that uses a tenth of it; a lighter image format available.
One blocking issue comes before twenty optimisations, and a field metric out of threshold comes before any lab audit.
1. Measuring: lab and field impact: important
What Google documents. Core Web Vitals are "the subset of Web Vitals that apply to all web pages, should be measured by all site owners". Three metrics: LCP "within 2.5 seconds", INP "of 200 milliseconds or less", CLS "of 0.1 or less". The target is "the 75th percentile of page loads, segmented across mobile and desktop devices". Field tools, Chrome User Experience Report, PageSpeed Insights, Search Console, measure all three on real visitors; "tools like Lighthouse that load pages in a simulated environment without a user cannot measure INP" (Web Vitals).
What we recommend. Start with the field: Search Console's Core Web Vitals report says which templates are out of threshold for your real visitors. The lab then finds the cause, on the page and device that fail. What LCP, CLS and INP measure, and how to read them, is in our Core Web Vitals guide; here, one line each: LCP is the display of the largest visible element, CLS the stability of the layout, INP the delay before a response to an interaction.
How to check
Our speed test runs a Lighthouse analysis of one page, mobile or desktop, and reports the score, the lab metrics and the priority audits. It does not report field data: that needs enough Chrome visitors, and is read in Search Console or in PageSpeed Insights when the site is eligible. A good test result therefore does not guarantee a green Search Console report.
How to fix
The fix is a procedure: read the field first, then reproduce in the lab on the right template and device, then measure before and after every change.
# 1. field: Search Console > Core Web Vitals > "poor" and "needs improvement" URLs, by template
# 2. lab, same page, simulated mobile
npx lighthouse https://your-site.com/slow-page --preset=perf --form-factor=mobile --output=json --output-path=./before.json
# 3. after the fix, same command to ./after.json, compare LCP, CLS, TBT
What we did on seoforge.fr
The free speed test wraps the PageSpeed Insights API: it reports lab metrics and says so on the results page, where INP is flagged as absent from a lab run. Our own field metrics are read in Search Console, not in the tool.
2. Server and TTFB impact: blocking
What Google documents. TTFB "is the sum of the following request phases: redirect time, service worker startup time, DNS lookup, connection and TLS negotiation, request, up until the point at which the first byte of the response has arrived". "Good TTFB values are 0.8 seconds or less, and poor values are greater than 1.8 seconds." TTFB "isn't a Core Web Vitals metric", but since it precedes FCP and LCP, "it's recommended that your server responds to navigation requests quickly" (Time to First Byte).
What we recommend. A high TTFB is almost always fixed before the code: at most one redirect before the final page, a page cache for anonymous visitors, hosting or a CDN close to the visitors, and a database queried once per page rather than once per block. TTFB is the floor of LCP: nothing below can compensate for it.
How to check
On the command line, curl -o /dev/null -s -w "%{time_starttransfer}\n" https://your-site.com/ gives the TTFB of one request; repeat on three pages and from another network. Our speed test reports server response time among its audits, and the GEO test measures it on the home page.
How to fix
A page cache in front of the application, and a direct redirect.
# anonymous page: cacheable by a shared cache or CDN for five minutes
Cache-Control: public, s-maxage=300, stale-while-revalidate=60
# one redirect, straight to the final form
http://your-site.com/ -> https://your-site.com/en (301)
What we did on seoforge.fr
Public pages are served by Symfony's application HTTP cache, five minutes with revalidation; the sitemap and the llms.txt file are cached for an hour. The site root makes a single redirect to the default language, and an automated test forbids chains.
3. HTTP cache impact: important
What Google documents. For resources with a versioned URL, such as style.x234dff.css, Cache-Control: max-age=31536000, because a content change changes the URL and invalidates the old version by itself; immutable "as a further optimization". For unversioned URLs, no-cache requires revalidation before every use, whereas no-store forbids any storage; ETag and Last-Modified "both serve the same purpose: determining whether the browser needs to re-download a cached file", with a 304 response when it is unchanged (the HTTP cache).
What we recommend. Two regimes and no more: one year and immutable for every file whose name carries a content fingerprint, a short duration with revalidation for HTML. The worst setting is the default of many hosts, no header at all, which lets every browser guess.
How to check
curl -sI https://your-site.com/assets/css/site-abc123.css | grep -i cache-control, then the same command on an HTML page. In the browser's developer tools, the "Size" column must show "memory cache" or "disk cache" for static files on the second visit.
How to fix
Headers are set at the web server or application level, per URL family.
# nginx: versioned files, one year, immutable
location ~* ^/assets/.+\.(css|js|woff2|webp|avif|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# HTML: short, revalidated
location / {
add_header Cache-Control "public, max-age=300, must-revalidate";
}
What we did on seoforge.fr
Stylesheets, scripts, fonts and images are served under a name carrying their content fingerprint, with public, max-age=31536000, immutable; HTML gets public, max-age=300, must-revalidate. A response subscriber sets those headers per URL family, and API responses are excluded from any shared cache.
4. Images impact: blocking
What Google documents. LCP breaks down into four parts: TTFB, resource load delay, resource load duration, and element render delay. For the hero image, "never lazy-load your LCP image, as that will always lead to unnecessary resource load delay", state its priority with fetchpriority="high", preload it with link rel="preload" when it comes from CSS, "serve the optimal image size" and "a more optimal format (such as AVIF or WebP)" (optimize LCP).
What we recommend. One image per use, not an original resized by the browser: three widths in srcset, a modern format, width and height attributes everywhere, lazy loading on everything below the fold and never on the hero image. A CMS that does not generate those variants on upload is a CMS to equip before anything else.
How to check
The speed test lists the "Properly size images" and "Serve images in next-gen formats" audits with the estimated gain. In the developer tools, the Performance panel names the LCP element and the resource behind it; if that resource has loading="lazy", that is the first fix.
How to fix
A complete tag for the hero image, a deferred tag for the others.
<!-- hero image: high priority, never deferred, dimensions declared -->
<img src="/img/hero-1200.webp"
srcset="/img/hero-480.webp 480w, /img/hero-800.webp 800w, /img/hero-1200.webp 1200w"
sizes="(max-width: 700px) 100vw, 560px"
width="1200" height="630" alt="…" fetchpriority="high">
<!-- below the fold: deferred -->
<img src="/img/photo-800.webp" width="800" height="600" alt="…" loading="lazy">
What we did on seoforge.fr
Article images are served as WebP in three widths with srcset and sizes, dimensions declared, fetchpriority="high" on the hero image and lazy loading on the others. The AVIF recommendation is not yet applied on this site: our images stay in WebP.
5. Fonts impact: important
What Google documents. "Use only WOFF2 and forget about everything else": it compresses about 30% better than WOFF. "Removing unused glyphs can significantly reduce the filesize of a font." Preloading "should also be used carefully as it bypasses some of the browser's built-in content negotiation strategies", but it is effective when the font is declared in an external stylesheet the browser discovers late. font-display: optional avoids layout shifts, swap shows the text at once at the cost of a visible switch (font best practices).
What we recommend. Two files at most, variable, in WOFF2, hosted on your domain, preloaded from the HTML, limited to Latin glyphs when the site is in French or English. With swap, the fallback font must be calibrated on the final one, otherwise the text jumps on the switch; that calibration is what makes swap acceptable.
How to check
In the Network panel, filter on "font": the number of files, their format and their weight read in one line. The speed test flags "Ensure text remains visible during webfont load". A field CLS with no image to blame is often a font.
How to fix
A declaration with a subset and a calibrated fallback font.
<link rel="preload" href="/fonts/sora-var.woff2" as="font" type="font/woff2" crossorigin>
@font-face {
font-family: 'Sora'; font-weight: 600 700; font-display: swap;
src: url('/fonts/sora-var.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0152-0153, U+2000-206F, U+20AC;
}
@font-face {
font-family: 'Sora Fallback'; src: local('Arial');
size-adjust: 112%; ascent-override: 92%; descent-override: 24%; line-gap-override: 0%;
}
h1 { font-family: 'Sora', 'Sora Fallback', system-ui, sans-serif; }
What we did on seoforge.fr
Two variable WOFF2 fonts, hosted on the site, preloaded from the HTML head, limited to Latin glyphs by unicode-range, in font-display: swap with a fallback font calibrated through size-adjust and the vertical metrics, so that the switch does not move the text.
6. JavaScript and third-party scripts impact: important
What Google documents. For INP, "websites should strive to have an Interaction to Next Paint of 200 milliseconds or less". The rule is to do minimal work in event callbacks, to break long logic into separate tasks so the main thread is not blocked, and to yield: "setTimeout is one way to break up tasks, because the callback passed to it runs in a new task". Only render-critical logic should run before the next frame, the rest is deferred (optimize INP).
What we recommend. The first lever is not optimising a script, it is removing one. Every third-party script, analytics, chat, video, map, justifies itself or goes; those that stay are loaded with defer or after a visitor action, and measurement tools wait for consent. A brochure site does not need a client-side framework to display text.
How to check
The speed test lists "Reduce the impact of third-party code" and the total blocking time. In the Performance panel, tasks longer than 50 milliseconds are marked in red; their call stack names the responsible script, and it is almost always a third party.
How to fix
Deferred and conditional loading.
<!-- site script: deferred, run after HTML parsing -->
<script src="/assets/js/site-abc123.js" defer></script>
<!-- analytics: only after consent -->
<script>
window.addEventListener('sf:consent-granted', function () {
var s = document.createElement('script'); s.async = true;
s.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXX';
document.head.appendChild(s);
});
</script>
What we did on seoforge.fr
A single site script, loaded with defer, served by AssetMapper without a build step: modules are declared in an import map and served under a versioned name. The only third-party script is Google Tag Manager, injected only after explicit consent; without consent, no third party is loaded.
7. CSS and rendering impact: optimisation
What Google documents. For CLS, "sites should strive to have a CLS of 0.1 or less for at least 75% of page visits". "Always include width and height size attributes on your images and video elements", or reserve the space with aspect-ratio; reserve room for late-loading content with min-height; "avoid inserting new content without a user interaction"; "composited animations using translate can't impact other elements, and so don't count toward CLS" (optimize CLS).
What we recommend. A single stylesheet, served with a fingerprint and a year of cache, beats poorly maintained inline critical CSS; critical CSS is only justified when the sheet exceeds a few tens of compressed kilobytes and LCP is really blocked by it. Animations go through transform and opacity, never through properties that trigger layout.
How to check
In the Performance panel, enable the "Layout shifts" track: every shift is timed and the moved element is named. The speed test reports the lab CLS and the "Avoid large layout shifts" audit.
How to fix
Space reserved before the content arrives, and composited animations.
/* space reserved for a late-loading block */
.reviews { min-height: 320px; }
.embed { aspect-ratio: 16 / 9; }
/* composited animation: does not move the rest of the page */
.card { transition: transform .2s ease, opacity .2s ease; }
.card:hover { transform: translateY(-2px); }
What we did on seoforge.fr
A single stylesheet of about 100 kilobytes before compression, served under a versioned name with a year of cache; no inline critical CSS, by choice. Blocks revealed on scroll animate transform and opacity only, and images declare their dimensions.
8. CMS specifics: WordPress and Shopify impact: important
What Google documents. "Synchronous scripts delay DOM construction and rendering": "always load third-party scripts asynchronously unless the script has to run before the page can be rendered", async when it must run early, defer for the rest. Lazy-loading embeds "is a good way to improve page speed and paint metrics", a preconnect to a critical third-party origin "can save 100 to 500 ms", and the first question remains: "remove it if it doesn't add clear value to your site" (efficiently load third-party JavaScript).
What we recommend. On WordPress, slowness rarely comes from the core: it comes from the hosting, the theme and the number of plugins, each adding its scripts and stylesheets to every page. A page cache, a light theme, counted plugins and images generated on upload settle most of it; the detail, plugin by plugin, is in our article on WordPress site speed. On Shopify, hosting and cache are imposed: the lever is the theme and the installed apps, each injecting its script.
How to check
Count the CSS and JavaScript files loaded on the home page, in the Network panel or in the speed test's "Reduce unused JavaScript" audit. Beyond about twenty, the theme or the plugins load files the page does not use.
How to fix
Remove before optimising, then load a script only where it is used.
// WordPress, functions.php: a plugin's script only on the page that uses it
add_action('wp_enqueue_scripts', function () {
if (!is_page('contact')) {
wp_dequeue_script('contact-form-widget');
wp_dequeue_style('contact-form-widget');
}
}, 100);
What we did on seoforge.fr
This site is not a CMS: it is a server-rendered Symfony application, with no plugin or theme. The recommendations in this section come from our work on clients' WordPress sites, documented in the article linked above.
What a Core Web Vitals optimisation delivers with us
- The diagnosis per template: LCP, CLS, INP with the cause of each degradation.
- Field and lab measurement, before any change.
- The list of fixes ranked by expected gain and effort, from server to browser.
- The fixes implemented in your code, or specified for your team.
- The measurements after, and the rules to keep the metrics green.
On quote, depending on the number of templates and your technology; first diagnosis free within 24 working hours. The work does not stop at the report: the fixes are implemented, not only listed.
Where to start
Run the speed test on your most visited page, on mobile. To go further, the Core Web Vitals optimisation covers the eight sections of this guide across your templates, alone or within the Growth Audit. When slowness comes from crawling, redirects or rendering, the technical SEO guide takes over.