What I Check First in a Frontend Performance Audit

The exact order I work through when auditing a slow frontend — from TTFB to third-party scripts. No padding, just the sequence.

When a site lands on my desk with a bad Lighthouse score and a “make it faster” ask, I follow the same sequence every time.

Not because it’s the only way — because it’s the fastest path from “something is slow” to “I know exactly what to fix.”


Before Any Tool

The first question is not “what’s the Lighthouse score.” It’s: what are real users experiencing?

Open PageSpeed Insights. Enter the URL. Look at the field data (CrUX) before the lab data (Lighthouse). If field data shows green LCP and green INP, you have a passing score with real users and the problem is probably isolated — a specific page, a specific device segment, a specific geography.

If field data shows red, it means real users are hitting this in production. That’s the priority.

Field data also tells you which metric is failing. Go fix that metric, not the generic Lighthouse score.


Step 1: Network tab, throttled

Open Chrome DevTools. Network tab. Set throttling to “Slow 4G” (or “Fast 3G” if you want to stress-test). Hard reload.

What I’m looking at:

TTFB first. Click the HTML document request in the waterfall. “Waiting (TTFB)” should be under 600ms. If it’s over 1 second, nothing else I fix downstream will save the LCP. The problem is the server or CDN, not the frontend.

A CDN-cached static page should have TTFB under 100ms from most regions. A server-rendered page doing database work on the critical path can be 2–3 seconds. These are fundamentally different problems.

Render-blocking resources second. Look for resources with a long horizontal bar before the first paint. CSS in <head> is render-blocking by definition. JS in <head> without defer or async is render-blocking. Each one delays LCP by its full load time.

Request waterfall shape third. A healthy waterfall looks like a tree — lots of parallel requests after initial HTML. An unhealthy waterfall looks like a chain — each request waits for the previous. Chains happen when code fetches data sequentially instead of in parallel, or when resources are loaded in JS instead of HTML (they can’t be discovered until JS runs).


Step 2: Identify the LCP element

Performance tab. Record a page load. Look for the “LCP” marker in the timeline.

Click it. DevTools shows you the exact element that triggered LCP. This is the only element that matters for LCP score. Everything else is secondary.

Common LCP elements and their failure modes:

ElementCommon Problem
<img> heroNo fetchpriority="high", wrong format, no preload
CSS backgroundNot preload-eligible — move to <img>
<h1> textWeb font blocking render
Video posterPoster not preloaded, codec issues

If the LCP element is a CSS background-image, that’s the first thing to change. Background images are not discoverable by the preload scanner. The browser finds them only when it processes CSS, which is after HTML parsing and stylesheet downloading. Move it to an <img> element.


Step 3: Images

Run Squoosh or sharp on any image over 200KB. Convert to WebP. If you can tolerate the browser support matrix, AVIF saves another 20%.

Check every <img> for explicit width and height. Missing dimensions = browser can’t reserve space = layout shift as images load = CLS.

<!-- CLS source: browser doesn't know height until image loads -->
<img src="product.jpg" alt="Product" />

<!-- Fixed -->
<img src="product.webp" width="800" height="600" alt="Product" />

Responsive images: if the same image is served at every viewport size, you’re sending a 1200px image to a 375px screen. Use srcset:

<img
  srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w"
  sizes="(max-width: 600px) 100vw, 800px"
  src="/hero-1200.webp"
  width="1200"
  height="630"
  alt="Hero"
/>

Step 4: Fonts

Open the Network tab filtered to “Font” requests. If fonts are loading from fonts.googleapis.com or use.typekit.net, that’s an external DNS lookup on your critical path.

Self-hosting web fonts removes the third-party dependency. Use google-webfonts-helper to download Google Fonts as WOFF2 files.

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-400.woff2') format('woff2');
  font-weight: 400;
  font-display: optional;
}

Preload the fonts used above the fold:

<link rel="preload" as="font" href="/fonts/inter-400.woff2" type="font/woff2" crossorigin />

font-display: optional gives the browser a very short window to use the web font. If it doesn’t arrive in time, the system font renders and the web font is not swapped in later. No layout shift. The tradeoff is that some users see the fallback on cold loads.

font-display: swap causes a visible swap (FOUT) but at least content is readable immediately. Use it if brand consistency is more important than CLS stability.


Step 5: JavaScript

Coverage tab in DevTools (Cmd+Shift+P → “Coverage”). Record a page load. Shows you how much of each JS file actually executes on load.

A typical Next.js or Create React App site sends 300–500KB of JS. Of that, 40–60% often goes unused on the initial load — it’s code for routes the user hasn’t visited yet.

What to look for:

  • Large third-party bundles executing on load (analytics, marketing tools)
  • Framework chunks that could be code-split
  • Polyfills for browsers you don’t target

For code splitting in Vite/Rollup, dynamic imports are the mechanism:

// Before: button.ts is in the main bundle
import { initHeavyWidget } from './button';
initHeavyWidget();

// After: loaded only when needed
const { initHeavyWidget } = await import('./button');
initHeavyWidget();

Third-party scripts deserve special attention. Every marketing pixel, chat widget, and analytics tool runs on your main thread. They delay INP. Audit the list and ask: does this actually drive a business decision? If you can’t answer yes to that question, remove it.


Step 6: INP Check

INP (Interaction to Next Paint) is a field metric. You can’t reliably reproduce it in DevTools because it depends on what interactions real users are performing. But you can identify likely problems.

Add the web-vitals library:

import { onINP } from 'web-vitals';

onINP(({ value, attribution }) => {
  // Log to your analytics
  console.log('INP:', value, 'on element:', attribution.interactionTarget);
});

When you deploy this, the interactionTarget will tell you exactly which elements on the page are causing slow responses. Check that element first.

In DevTools, you can look for Long Tasks in the Performance timeline — any task over 50ms is a candidate. Event handlers that trigger large renders, localStorage reads, synchronous fetch, heavy JSON.parse — these are the common culprits.


Step 7: Third-Party Audit

WebPageTest’s “Connection View” or the “Third Party” filter in the Coverage tab shows third-party resource impact.

For each third-party resource, note:

  • Size
  • Time to load
  • Whether it’s render-blocking

The ones that show up on the critical path and are render-blocking are the ones to remove or defer. A chat widget that blocks render by 800ms while the user reads your homepage is costing you LCP.


The Sequence, Condensed

  1. PageSpeed Insights field data — identify failing metric
  2. Network tab (Slow 4G) — check TTFB
  3. Performance tab — identify LCP element
  4. Fix LCP element: preload, fetchpriority, format
  5. Fix all images: dimensions, WebP, responsive srcset
  6. Self-host fonts, preload, set font-display
  7. Coverage tab — find unused JS
  8. Defer / code-split heavy JS
  9. Add INP instrumentation, find slow interactions
  10. Audit third-party scripts — remove what you can’t justify

Work top to bottom. Each step affects what you find in subsequent steps. Starting with JS when TTFB is broken is wasted effort.


If you’d like a professional audit that works through this on your specific site and gives you a prioritised fix list, see Frontend Performance Optimization. Audits start at ₹10,000 with a 3–5 day turnaround.

☀ Try The Archive