Web Performance Optimization

Cut LCP, INP, and bundle size on the site you already shipped.

Engineering-led Core Web Vitals work for production sites. We profile, fix, and ship measurable improvements — usually within two weeks.

Most slow sites are not slow for one reason. They are slow because three or four small things compound — an unoptimized hero image, a render-blocking font, a 300KB analytics bundle, and a layout shift no one tracked. We find those, fix them, and prove the win with before/after traces.

Real Web Vitals work

LCP, CLS, and INP fixes grounded in Lighthouse, WebPageTest, and field RUM data — not generic checklists.

Before / after report

Every engagement ships with a trace-backed delta: load times, bundle sizes, and Lighthouse scores on the exact pages we touched.

Production-safe changes

We work in PRs against your repo, behind feature flags where needed, with rollback notes. No surprise refactors.

Core Web Vitals

The six Core Web Vitals — and exactly what we tune for each

Core Web Vitals are the metrics Google publishes, Search Console penalizes you for, and real users feel. They are not abstract scores — each one maps to a specific moment in the page lifecycle and a specific class of fix. We instrument all six in lab (Lighthouse, WebPageTest) and field (CrUX, web-vitals.js), then attack them with named techniques. Below is the breakdown — what each metric measures, the thresholds Google uses, and the exact moves we make to push you into the green.

(Loading Time)
LCP Largest Contentful Paint

The time from navigation start to when the largest visible element (usually a hero image, headline, or video poster) finishes rendering.

Good Needs Improvement Poor
2.5s 4.0s
What we do (6)
  • Preload the LCP image with rel=preload and fetchpriority=high
  • Serve the hero in AVIF/WebP with a responsive srcset and explicit sizes
  • Inline critical CSS so the first paint is not blocked on a stylesheet round-trip
  • Self-host display fonts with font-display: swap and preload the WOFF2
  • Eliminate render-blocking scripts in <head>; defer or async everything else
  • Tune TTFB via edge caching, SSR streaming, and stale-while-revalidate
(Visual Stability)
CLS Cumulative Layout Shift

A unitless score measuring how much visible content shifts position during the page's life.

Good Needs Improvement Poor
0.1 0.25
What we do (5)
  • Set explicit width and height attributes on every image, video, and iframe
  • Reserve space with CSS aspect-ratio for ads, embeds, and cookie banners
  • Use font-display: optional or size-adjust to eliminate FOUT shift
  • Render skeletons at the final element dimensions, not smaller placeholders
  • Pin sticky headers and banners with transform instead of inserting into flow
(Interactivity)
INP Interaction to Next Paint

The latency between a user input (tap, click, key press) and the next visual update, sampled across the entire session.

Good Needs Improvement Poor
200ms 500ms
What we do (6)
  • Break long tasks with scheduler.yield() and isInputPending()
  • Defer non-critical hydration with React.lazy + Suspense or islands architecture
  • Move parsing, sorting, and crypto off the main thread into Web Workers
  • Budget third-party JS execution time and lazy-init on first interaction
  • Debounce expensive React re-renders with useDeferredValue and useTransition
  • Replace large event-handler closures with delegated, idempotent handlers
(Loading Time)
FCP First Contentful Paint

The time from navigation to when any text, image, or non-white pixel first renders.

Good Needs Improvement Poor
1.8s 3.0s
What we do (5)
  • Inline above-the-fold CSS and defer the rest with media=print swap
  • Preconnect to required origins (fonts, CDN, analytics) in the document head
  • Serve compressed HTML with Brotli and HTTP/3 from the edge
  • Eliminate redirect chains and consolidate to a single canonical origin
  • Stream HTML using SSR with React Suspense or Next.js app router
(Loading Time)
TTFB Time to First Byte

The duration from the user's request to the first byte of response arriving in the browser.

Good Needs Improvement Poor
800ms 1800ms
What we do (5)
  • Cache HTML at the edge with Cloudflare, Vercel, or Fastly with SWR
  • Move dynamic origins closer to users with regional functions or edge runtimes
  • Add Cache-Control and Surrogate-Control headers tuned per route
  • Eliminate origin redirects; collapse www, trailing-slash, and protocol hops
  • Pool database connections and add Redis or KV for hot lookups
(Interactivity)
TBT Total Blocking Time

The sum of time, between FCP and TTI, when the main thread was blocked by tasks longer than 50ms.

Good Needs Improvement Poor
200ms 600ms
What we do (6)
  • Code-split routes and components with dynamic import() and React.lazy
  • Tree-shake unused exports; audit duplicates with bundle-analyzer
  • Defer or async non-critical scripts (analytics, chat, A/B, tag managers)
  • Replace heavy moment.js / lodash with date-fns, dayjs, or native APIs
  • Pre-compute at build time anything that does not need runtime evaluation
  • Use Partytown or a worker to move third-party tags off the main thread
Case studies

Two case studies — what we changed and what it moved

Ace Hardware

Ace Hardware

10 weeks
Enterprise Retail · Home Improvement
LCP -57%
4.2s 1.8s
CLS -89%
0.18 0.02
JS bundle -62%
580 KB 220 KB
Lighthouse Performance +47 pts
42 89
What we changed (6)
  • Hero image pipeline rebuild
    AVIF + WebP, srcset/sizes, preload, fetchpriority=high
  • Critical CSS extraction
    Critters / postcss-critical-split, async stylesheet swap
  • Render-blocking script removal
    async/defer, requestIdleCallback, Partytown for gtag
  • Layout shift fixes
    aspect-ratio CSS, dimension attributes, skeleton reservation
  • Code-split + lazy hydration
    Dynamic import(), React.lazy, Suspense boundaries
  • Dependency cleanup
    webpack-bundle-analyzer, tree-shaking, dedupe
OpticsPlanet

OpticsPlanet

Ongoing (multi-year)
Enterprise Retail · Outdoor & Tactical
LCP -58%
3.8s 1.6s
INP -55%
320ms 145ms
JS bundle -61%
720 KB 280 KB
Lighthouse Performance +51 pts
38 89
What we changed (6)
  • Category-page hero refactor
    Selective hydration, AVIF/WebP, fetchpriority, DPR-aware srcset
  • Product gallery srcset + lazy
    loading=lazy, decoding=async, srcset/sizes
  • Third-party tag rationalization
    Partytown, lazy-init on intent, GTM consolidation
  • Redux + selector refactor for INP
    Reselect, useDeferredValue, useTransition, scheduler.yield()
  • Font subsetting + self-hosting
    glyphhanger subsetting, self-host WOFF2, preload, size-adjust
  • Server-rendered above-the-fold
    SSR streaming, selective hydration, islands pattern

Numbers shown are representative of typical engagement deltas. Real lab and field traces shared during kickoff.

Tools we use

The toolchain we actually open every week

Performance is mostly interpretation. The tools below are industry-standard — what matters is knowing which trace answers which question, and chaining them so a Lighthouse warning leads to a flame graph that leads to a one-line PR.

Lab measurement

  • Lighthouse Lab Audit
  • WebPageTest Lab Tester
  • PageSpeed Insights Field Audit
  • Chrome DevTools Performance Browser Profiler

Real User Monitoring

  • web-vitals.js RUM Library
  • SpeedCurve RUM Dashboard
  • Sentry Performance RUM + APM
  • Datadog RUM RUM + APM
  • DebugBear Continuous Monitor

Build & bundle

  • webpack-bundle-analyzer Bundle Analyzer
  • rollup-plugin-visualizer Bundle Analyzer
  • Vite Page Bundler
  • esbuild JS Transformer
  • Million.js / Million Lint React Linter

Image & font pipeline

  • Sharp Image Processor
  • Squoosh CLI Image Compressor
  • Cloudinary / Imgix Image CDN
  • next/image & astro:assets Image Optimizer
  • Glyphhanger / subfont Font Subsetter

Edge & CDN

  • Cloudflare Edge & CDN
  • Vercel Hosting + Edge
  • Fastly Enterprise CDN
  • Akamai Enterprise CDN

Diagnostics & adjacent

  • Partytown Script Offloader
  • Calibre Perf Budget
  • GTmetrix Lab Audit
  • Sentry Bundle Analyzer Bundle Tracker
Techniques

The techniques that actually move the numbers

Performance work converges on a finite set of named techniques. We organize them by where they live in the stack — every engagement is some subset of the list below, picked by what the traces say.

Image pipeline
Images are the largest single byte category on most retail and content sites, and the LCP element on 70%+ of pages. We rebuild the pipeline so format, dimensions, and priority are all correct from the build step through the CDN.
  • AVIF + WebP with JPEG fallback. Sharp emits all three at build time; the CDN negotiates based on Accept headers and ships AVIF where supported.
  • Responsive srcset + sizes. Per-breakpoint widths so a mobile request gets a 480px image, not a desktop one. Typical 60-80% byte cut.
  • Preload + fetchpriority=high on the LCP image. A rel=preload hint with fetchpriority lifts the LCP element above other resources in the browser's queue.
  • loading=lazy + decoding=async below the fold. Defers off-screen images and lets the browser decode them off the main thread.
  • CDN-side transforms (Cloudinary / Imgix / next/image). Runtime resizing, format negotiation, and smart crop without bloating the repo.
  • Width/height attributes on every image. Prevents the layout shift that CLS punishes — the single highest-leverage one-liner.
JavaScript
The biggest performance lever on a modern site is shipping less JavaScript and running it later. We use the standard set of techniques aggressively, and instrument to prove the wins.
  • Route-level code splitting. Dynamic import() at the route boundary so /checkout doesn't pay for /admin's bundle.
  • React.lazy + Suspense. Component-level splitting for modals, dialogs, and below-the-fold UI; Suspense boundaries give clean fallbacks.
  • Tree-shaking + dedupe. Named imports from lodash and date-fns; bundle-analyzer audits to remove duplicate copies of common libraries.
  • Lazy / partial hydration. Islands architecture (Astro) or RSC (Next.js app router) so static content never ships a hydration cost.
  • scheduler.yield() for long tasks. Breaks 200ms+ tasks into yield-friendly chunks so user input lands inside the INP budget.
  • Web Workers + Partytown. Move parsing, sorting, and third-party tags off the main thread entirely.
CSS
CSS is render-blocking by default. The win is delivering only what the first viewport needs, and getting the rest out of the critical path.
  • Critical CSS inlining. Critters or postcss-critical-split inlines the above-the-fold rules so first paint doesn't wait on a round-trip.
  • Async stylesheet loading. media=print swap pattern delivers the full stylesheet without blocking initial render.
  • Unused CSS pruning. PurgeCSS, Tailwind's JIT, or framework-native extractors strip selectors the page never uses.
  • CSS aspect-ratio reservations. Reserves space for ads, embeds, and async content — direct CLS-to-zero technique.
  • container queries over media queries. Reduces layout recalculation cost on resize and reflow-heavy templates.
Fonts
Custom fonts are a hidden tax on LCP and CLS. Treated correctly they cost almost nothing; treated wrong they cost 500ms and a layout shift.
  • Self-host WOFF2. Drops the third-party origin handshake and lets you set your own caching headers.
  • Subsetting with glyphhanger / subfont. Ships only the glyphs the site renders — typical Latin cut of 50-80%.
  • font-display: swap | optional. Eliminates the invisible-text period that delays LCP on slow connections.
  • size-adjust + fallback metrics. Matches the fallback font's metrics to the custom one so the swap doesn't shift layout.
  • Preload + crossorigin. Hints the browser to fetch the WOFF2 in parallel with the CSS that requests it.
Caching & edge
TTFB sits under every other metric. Cutting it cuts everything. We push as much of the response to the edge as the application allows.
  • HTML edge caching with SWR. Cache-Control + stale-while-revalidate so users get an instant byte while the cache refills.
  • ISR / on-demand revalidation. Next.js and Astro pre-render at build, revalidate on a tag or webhook — fresh content without origin pressure.
  • Brotli + HTTP/3 at the edge. Smaller payloads, faster connection setup, and multiplexed streams over UDP.
  • Edge runtimes (Vercel / Cloudflare Workers). Run dynamic logic 50ms from the user instead of in a single origin region.
  • Early Hints (103). Lets the browser start fetching CSS and the LCP image while the origin is still building the response.
Third-party scripts
Third-party tags are the most common cause of bad INP, TBT, and bundle bloat. They are also the easiest to fix because the application code doesn't have to change.
  • Tag audit + consolidation. Most sites have 30-50% redundant or unused tags. Half the work is cutting the list.
  • Partytown for analytics. Moves gtag, Hotjar, and segment to a Web Worker — frees 100-400ms of INP on real devices.
  • Lazy-init on interaction. Chat widgets and review widgets only load on click or scroll-into-view, not on first paint.
  • GTM consolidation. Single container, server-side endpoint where possible, with native deduplication of conversion events.
  • Self-hosted snippets. Self-host the small snippets (Plausible, Fathom) to drop a DNS lookup and gain caching control.
Server response (TTFB)
When the origin itself is the bottleneck, no amount of frontend work will rescue the page. We tune the origin and the routes that feed the LCP element.
  • SSR streaming. Flush the head and above-the-fold HTML while the rest of the page is still rendering on the server.
  • Database connection pooling. Reuse connections across requests so a cold function doesn't pay the handshake.
  • Redis / KV for hot reads. Catalog, price, and config lookups served from memory, not the primary store.
  • Redirect chain elimination. Collapse www, trailing-slash, and protocol hops into a single redirect or zero.
  • Origin-shield CDN. Cloudflare Argo, Fastly Origin Shield, or Akamai SureRoute to cut origin round-trip distance.
FAQ

Common questions

How long does a typical performance engagement take?
Quick-win audits ship in 2-5 days and usually move LCP and CLS visibly on the first PR. Standard engagements run 1-3 weeks. Deep refactors — code-splitting, hydration strategy, framework upgrades — run 4-10 weeks depending on the codebase. Every tier ships with a before/after report on the templates we touched.
Will rankings change after we ship performance fixes?
Core Web Vitals have been a confirmed Google ranking signal since 2021, weighted into the page experience update. Sites that move from poor to good on LCP and INP typically see improved rankings on competitive queries within 4-8 weeks, once CrUX updates. We never promise specific positions — we promise specific CWV deltas, which is what the algorithm reads.
What's the difference between Lighthouse and CrUX (lab vs field)?
Lighthouse is a synthetic lab test on simulated hardware — useful for diagnosing causes and gating CI. CrUX (and your RUM) is real-user field data from actual visitors with real devices, connections, and ad-blockers. Search Console reports field data. Lab numbers can disagree with field by 30-50%; we tune for field and use lab to find the root causes.
What if we're on Shopify, WordPress, or a no-code platform?
Performance work translates. On Shopify we tune Liquid templates, app loading, image transforms, and the Online Store 2.0 sections. On WordPress we tune theme assets, plugins, caching plugins (WP Rocket, LiteSpeed), and image pipelines. We avoid stack-level rewrites unless the numbers absolutely require them — usually they don't.
Do you deliver a written report?
Yes. Every engagement ships with a written report covering: baseline lab and field traces, the changes made (in PR-by-PR detail), the after traces on the same templates, bundle size diffs, and a list of regressions to watch for. The report is shareable with stakeholders who don't read Lighthouse traces for a living.
Can you keep monitoring after launch?
Yes. We set up RUM with web-vitals.js, CrUX dashboards, Calibre or SpeedCurve budgets, or your existing APM (Sentry, Datadog). Alerts fire to Slack on regression. Available as a one-time setup or as part of a monthly retainer that includes follow-up tuning when the inevitable third-party tag adds 100KB.
Do you work directly in our repo, or hand off a patch?
Default is PRs against your repo on a branch we own, with clear commits, rollback notes, and a description of the trace each change addresses. If your team prefers a diff or a patchset for internal review and merge, we deliver that instead. We work alongside your engineers, not over them.
Ready when you are

Get a free performance audit for your site

Send us a URL. We'll run lab and field traces, identify the top three wins, and send back a short written report — no pitch, no obligation.