Web Performance
Page Speed: comprehensive website loading optimisation

Page speed is not a single metric — it is a system of measurements and optimisations that determine how quickly a user gets a working page. We cover every key layer: server, assets, scripts, cache — from diagnosis to concrete techniques.
Page Speed is one of the most impactful investments in both SEO and conversion at the same time. Amazon calculated that every 100 ms of additional latency costs 1% of sales. Google has used speed as a ranking factor for desktop since 2010, for mobile since 2018, and in 2021 introduced Core Web Vitals as a direct Page Experience signal.
What is Page Speed and how is it measured
Page Speed is not a single number — it is a set of metrics, each measuring a different aspect of the loading experience. Google PageSpeed Insights combines lab data (Lighthouse) and real-user field data (Chrome User Experience Report). The final score from 0 to 100 is a weighted sum of several metrics and should not be used as the sole optimisation target.
For SEO, field data (CrUX) is what matters: Google uses the 75th percentile of real Chrome users when evaluating Core Web Vitals. Lighthouse helps diagnose problems, but it is not a direct input to the ranking algorithm.
Key speed metrics
Each metric addresses a specific aspect of the user experience. Understanding the differences is the foundation of correct diagnosis.
| Metric | What it measures | Good value | Affects ranking |
|---|---|---|---|
| TTFB (Time to First Byte) | Time until the first byte of HTML from the server | < 800 ms | Indirectly (via LCP) |
| FCP (First Contentful Paint) | First pixel of content on screen | < 1.8 s | Lighthouse, not a CrUX factor |
| LCP (Largest Contentful Paint) | Main content visible to the user | < 2.5 s | Yes (Core Web Vitals) |
| INP (Interaction to Next Paint) | Responsiveness to user interaction | < 200 ms | Yes (Core Web Vitals, replaced FID) |
| CLS (Cumulative Layout Shift) | Layout stability — how much elements jump | < 0.1 | Yes (Core Web Vitals) |
| TBT (Total Blocking Time) | Total main-thread blocking time from JS | < 200 ms | Lab proxy for INP |
Leave within 3 s
Mobile users who abandon a page loading longer than 3 seconds
Conversion per second
Each additional second of latency reduces conversion by 1–3% on average
CrUX percentile
Google assesses Core Web Vitals at the 75th percentile of real users
CrUX window
Rolling period over which data is collected for site assessment
Diagnostics: where to start optimisation
Blind optimisation is wasted effort. The correct sequence: measure first, identify the bottleneck, fix it, measure again.
- Open PageSpeed Insights and test 5–10 key pages: homepage, categories, product cards, landing pages. Look at Field Data, not just the Lighthouse score.
- In Google Search Console go to Core Web Vitals — it shows which URLs have 'Poor' or 'Needs improvement' ratings for LCP, INP, CLS with real user data.
- Run WebPageTest for deep analysis: load waterfall, filmstrip, TTFB, CDN impact. Choose the test region closest to your main audience.
- Use Chrome DevTools → Performance → record a page load. The flame chart shows which script occupies the main thread and blocks rendering.
- Check Coverage (DevTools → Coverage): unused CSS/JS is a direct candidate for removal or deferred loading.
PageSpeed Insights
Google's free tool. Shows both Lab (Lighthouse) and Field (CrUX) data. Provides a list of specific recommendations with estimated time savings. Best starting point for an initial audit.
WebPageTest
Detailed waterfall, filmstrip, multi-location testing, version comparison. Shows the impact of CDN, DNS, SSL handshake. Indispensable for deep TTFB diagnosis.
Google Search Console
Real CrUX data by URL groups. Shows metric trends over time — the best tool for monitoring improvements after deploying optimisations.
Chrome DevTools
Performance Profiler and Coverage tabs — for finding the specific script or CSS blocking rendering. The flame chart reveals Long Tasks (> 50 ms) on the main thread.
Server and TTFB optimisation
TTFB (Time to First Byte) is the foundation of all speed. If the server responds slowly, every subsequent optimisation works against an artificial ceiling. Target: TTFB < 200 ms for CDN environments, < 400 ms for dynamic servers.
Hosting and infrastructure
- Hosting choice
- Shared hosting with resources split among thousands of sites physically limits TTFB. A VPS or dedicated server with Nginx/Caddy and proper caching delivers TTFB of 20–80 ms for static content.
- CDN (Content Delivery Network)
- Cloudflare, BunnyCDN, Fastly — the nearest edge node responds instead of your origin server. For static or cached content, TTFB drops to 10–50 ms. For dynamic content, it depends on edge cache configuration.
- HTTP/2 and HTTP/3 (QUIC)
- HTTP/2 eliminates head-of-line blocking and supports multiplexing. HTTP/3 over QUIC further accelerates connection establishment (0-RTT) and is resilient to packet loss on mobile networks.
- Server geolocation
- Physical distance between the user and the server adds ~1 ms per 100 km (round trip). For a primarily European audience, a server in Frankfurt or Amsterdam is significantly faster than US-East.
Compression and encoding
- Enable Brotli (br) — saves 15–25% compared to gzip with comparable decompression speed. Supported by all modern browsers.
- Keep gzip as a fallback for older clients. Minimum: compression level 6.
- Configure compression for HTML, CSS, JS, JSON, XML, SVG. Images and binary files are already compressed — applying gzip to them achieves nothing.
- Use the Vary: Accept-Encoding header so the CDN caches separate versions for different clients.
Server-side caching
Dynamic pages (PHP, Node.js, Python) hit the database on every request. A cache allows serving ready HTML without recalculation.
- Full-page cache: Nginx FastCGI Cache, Varnish, Redis — store ready HTML and serve it without touching the application.
- Object cache: Redis or Memcached for caching database query results, sessions, and computed data.
- Invalidation rules: the cache must be purged when content changes (CMS webhook, TTL-based expiration).
- Stale-while-revalidate: serve the stale cache immediately and regenerate in the background — users don't wait for regeneration.
Asset optimisation: images, fonts, CSS
Images
Images are typically the heaviest category of page payload. Correct optimisation delivers 30–70% traffic savings and directly impacts LCP.
| Technique | Savings | Priority |
|---|---|---|
| WebP instead of JPEG/PNG | 25–35% | High |
| AVIF instead of JPEG/PNG | 40–55% | High (90%+ browser support) |
| Lossless compression (PNG → oxipng) | 5–20% | Medium |
| Correct sizing (srcset + sizes) | 50–80% on mobile | High |
| Lazy loading for off-screen images | Saves requests on load | High |
| fetchpriority="high" for LCP image | 0.5–1.5 s LCP | Critical |
Use the <picture> element to serve the right format: AVIF for supporting browsers, WebP as a fallback, JPEG/PNG for legacy browsers. The Next.js Image component handles this automatically.
Fonts
- Self-host fonts: serve .woff2 from your own domain — eliminates the DNS lookup, TCP handshake, and dependency on the Google Fonts CDN.
- Preload critical fonts: <link rel="preload" as="font" type="font/woff2" crossorigin> in <head> — the browser starts loading before CSS is parsed.
- font-display: swap — the browser shows the system font while the custom one loads. Eliminates FOIT (invisible text).
- Font subsetting: load only the needed glyphs (Latin + Cyrillic). Tools: glyphhanger or fonttools.
- Use variable fonts — one file instead of multiple weights (regular, bold, italic). Saves 30–50% of total font size.
CSS
- Critical CSS inline
- Above-the-fold styles are embedded directly in <head> inside a <style> tag — the browser renders the first screen without waiting for an external CSS file. The rest of the CSS loads asynchronously.
- Minification
- Removing whitespace, comments, shortening properties. Cssnano or LightningCSS. Typical savings: 10–30% of the original size.
- Unused CSS removal
- PurgeCSS, UnCSS, or the framework's built-in mechanism (Tailwind purge). Especially important when using Bootstrap or Material UI — they pull in thousands of unused rules.
- Avoid @import in CSS
- CSS @import creates sequential requests: the browser loads the file, discovers the @import, and only then starts loading the next file. Use <link> tags for parallel loading instead.
JavaScript: render-blocking and the main thread
JavaScript is the primary adversary of Page Speed on modern sites. It blocks rendering, occupies the main thread, and delays interactivity. Goal: minimal JS in the critical path, maximum use of defer/async/dynamic import.
Loading strategies
| Attribute | Behaviour | When to use |
|---|---|---|
| None | Blocks HTML parsing, executes immediately | Only critical inline code |
| async | Loads in parallel, executes when ready (may interrupt parsing) | Independent scripts (analytics) |
| defer | Loads in parallel, executes after HTML is parsed | Most scripts on the page |
| type="module" | Deferred by default + ESM support | Modern ESM scripts |
Bundle and code splitting
- Code splitting: split JS into chunks — load only what is needed for the current page. In Next.js this happens automatically per page.
- Dynamic import: import('module') for components not needed on initial load (modals, tabs, filters).
- Tree shaking: bundle only the code that is actually used. Webpack and Rollup do this for ESM packages. Check: are there CommonJS dependencies breaking tree shaking?
- Bundle analysis: bundlephobia.com to check npm package sizes, @next/bundle-analyzer for Next.js — shows exactly what occupies space.
- Avoid large monolithic libraries: moment.js (67 KB) → day.js (2 KB), lodash → individual utility, jQuery → native JS.
SPA and server rendering
Client-side SPAs (React without SSR, Angular, Vue with CSR) return empty HTML and generate content in the browser. This leads to high LCP and poor Time to Interactive.
- SSR (Server-Side Rendering)
- Full HTML is generated on the server. The browser receives ready content immediately. LCP, FCP, and Lighthouse Score all improve dramatically. Requires client-side hydration — account for TTI (Time to Interactive).
- SSG (Static Site Generation)
- HTML is generated at build time. Maximum TTFB and LCP speed. Suitable for content that changes infrequently: blog, documentation, landing pages.
- PPR (Partial Prerendering)
- New in Next.js 15: a static page shell with streamed dynamic parts. The static shell is served instantly from the CDN, while dynamic data streams in as it becomes ready.
Caching: browser and CDN
Correctly configured caching is the cheapest optimisation with the highest impact for returning visitors. Goal: all static assets cached for 1 year; HTML — minimal caching or none.
| Resource type | Cache-Control | Invalidation strategy |
|---|---|---|
| JS/CSS with hash (bundle.abc123.js) | max-age=31536000, immutable | Hash changes when the file changes |
| Images | max-age=2592000 (30 days) | URL with version or Content Hash |
| HTML pages | no-cache or max-age=0, must-revalidate | Always revalidated with server |
| Fonts | max-age=31536000, immutable | Hash in URL or versioning |
| API responses | Cache-Control: private, max-age=60 | Depends on data change frequency |
- Content-based hashing (fingerprinting): Webpack, Vite, and Next.js automatically append a content hash to filenames. When code changes — new hash, old cache is not used.
- ETag and Last-Modified: if the client already has the resource, the server responds with 304 Not Modified and no body. Less traffic, but there is still a network round trip.
- Service Worker: a programmable cache in the browser. Enables complex strategies (Cache-First, Network-First, Stale-While-Revalidate) for PWAs and offline support.
- CDN Cache-Control: use s-maxage to control CDN caching independently of the browser max-age. CDN caches longer, browser caches shorter.
Third-party scripts: the hidden main culprit
Third-party scripts (analytics, chat bots, pixels, A/B testing, widgets) are often the primary cause of poor TBT and INP. The average commercial page loads 15–30 third-party scripts.
Third-party script audit
WebPageTest → 'Third-party Summary' tab shows which script blocks the main thread for how many milliseconds. Typical offenders: Intercom, Hotjar, older versions of Google Analytics (gtag without async).
Deferred loading
Load non-critical scripts with a delay: setTimeout(loadScript, 3000) after the load event. The user gets to interact with the page before analytics loads.
DNS prefetch and preconnect
<link rel="preconnect"> for domains you will definitely contact. <link rel="dns-prefetch"> for likely ones. Savings: 100–300 ms per DNS lookup + TCP handshake for each third-party domain.
Replace or remove
The best third-party script optimisation is removal. Why a paid chat bot on every page? Why 5 social pixels? Auditing scripts actually in use often reveals 30–50% that are unnecessary.