Troubleshoot Website Performance Issues: A Developer's Guide

Vergate Team6 min read

Your Lighthouse score is 60. Your LCP is 4 seconds. Your users are bouncing. But you don't know where to start fixing it.

Website performance is a system problem — slow pages are rarely caused by one thing. This guide walks through the diagnostic process, from measuring the right metrics to fixing the actual bottlenecks.

#Step 1: Measure before you fix

Performance work without measurement is guessing. Before changing a single line of code, gather data.

#The metrics that matter

MetricWhat it measuresTarget
TTFBTime to first byte — server response speedLess than 200ms
LCPLargest Contentful Paint — main content visibleLess than 2.5s
INPInteraction to Next Paint — input responsivenessLess than 200ms
CLSCumulative Layout Shift — visual stabilityLess than 0.1
FCPFirst Contentful Paint — first content appearsLess than 1.8s
TBTTotal Blocking Time — main thread blockingLess than 200ms

Learn more: web.dev Core Web Vitals

#Tools to measure

  1. Lighthouse — built into Chrome DevTools (F12 → Lighthouse tab). Run in desktop mode for accurate results. Use --preset=desktop if running from CLI.

  2. Chrome DevTools Performance panel — record a page load, then look at the flame chart for long tasks (anything over 50ms blocks the main thread).

  3. WebPageTest — the gold standard for detailed waterfall analysis. Shows exactly what loaded, when, and how long each resource took.

  4. Vergate Performance Check — runs Lighthouse plus 11 custom HTTP resource checks and scores each metric with fix suggestions.

Learn more: web.dev measuring performance

#Step 2: Fix server response time (TTFB)

If TTFB is slow, everything downstream is delayed. A 500ms TTFB means the fastest possible LCP is 500ms plus render time.

#Common TTFB problems

Database queries:

sql
-- Find slow queries (PostgreSQL)
SELECT query, mean_time, calls
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;

Add indexes for frequently queried columns. Use EXPLAIN ANALYZE to understand query plans.

No caching:

nginx
# Nginx — cache static assets for 30 days
location ~* \.(jpg|jpeg|png|webp|css|js|woff2)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
}

Geographic distance:

If your server is in Virginia and your users are in Tokyo, TTFB will be 200-400ms just from network latency. Solutions:

  • CDN (Cloudflare, Vercel Edge, AWS CloudFront)
  • Edge functions for dynamic content
  • Database read replicas in distant regions

Learn more: MDN Cache-Control

#Step 3: Optimize images

Images are typically 50-70% of page weight. Optimizing them is the highest-ROI performance fix.

#The optimization checklist

  1. Use modern formats — WebP is 30-50% smaller than JPEG with identical quality. AVIF is even smaller but has less browser support (95%+ in 2026).
html
<picture>
  <source srcset="hero.avif" type="image/avif" />
  <source srcset="hero.webp" type="image/webp" />
  <img src="hero.jpg" alt="..." width="1200" height="675" />
</picture>
  1. Set explicit dimensions — prevents Cumulative Layout Shift:
html
<img src="photo.webp" alt="..." width="800" height="600" />
  1. Lazy load below-the-fold images:
html
<img src="photo.webp" alt="..." loading="lazy" width="800" height="600" />
  1. Responsive images — serve different sizes for different viewports:
html
<img srcset="photo-400.webp 400w, photo-800.webp 800w, photo-1200.webp 1200w"
     sizes="(max-width: 640px) 100vw, 50vw"
     src="photo-800.webp" alt="..." />
  1. Preload the LCP image — the hero image should not wait for CSS/JS discovery:
html
<link rel="preload" as="image" href="hero.webp" type="image/webp" />

Tools: Squoosh for manual compression, sharp for Node.js automation, ImageOptim for batch processing.

#Step 4: Fix render-blocking resources

CSS and JavaScript that block rendering delay the first paint.

#CSS

  • Inline critical CSS — the styles needed for above-the-fold content go in a <style> tag in the <head>:
html
<style>
  /* Only the styles for the hero section */
  .hero { padding: 4rem 2rem; }
  .hero h1 { font-size: 2.5rem; }
</style>
<link rel="stylesheet" href="styles.css" />
  • Defer non-critical CSS:
html
<link rel="stylesheet" href="full.css" media="print" onload="this.media='all'" />

#JavaScript

  • Use defer — downloads in parallel, executes after HTML parsing:
html
<script src="app.js" defer></script>
  • Use async — downloads in parallel, executes immediately (only for independent scripts like analytics):
html
<script src="analytics.js" async></script>
  • Module scripts are deferred by default:
html
<script type="module" src="app.js"></script>

#Third-party scripts

Third-party scripts (analytics, chat widgets, ads) are the most common performance killer and the hardest to fix because you don't control their code.

Strategies:

  • Lazy load — inject third-party scripts after user interaction:
javascript
// Load chat widget only after user scrolls or clicks
window.addEventListener('scroll', () => {
  const script = document.createElement('script');
  script.src = 'https://chat-widget.example.com/widget.js';
  document.body.appendChild(script);
}, { once: true });
  • Self-host — download the script and serve it from your domain (eliminates DNS lookup + connection time)
  • Use Partytown — runs third-party scripts in a Web Worker, freeing the main thread

Learn more: web.dev render-blocking resources

#Step 5: Reduce JavaScript bundle size

JavaScript is the most expensive resource — it must be downloaded, parsed, compiled, and executed. A 500KB JavaScript bundle can block the main thread for 200-500ms.

#Diagnostic steps

  1. Analyze the bundle:
bash
# Webpack
npx webpack-bundle-analyzer stats.json

# Vite
npx vite-bundle-visualizer
  1. Find the biggest offenders — sort by size, look for unexpected duplicates.

#Fix strategies

  • Tree shaking — import only what you use:
javascript
// Bad — imports entire library
import _ from 'lodash';
_.debounce(fn, 300);

// Good — imports only the function
import debounce from 'lodash/debounce';
debounce(fn, 300);
  • Code splitting — load code per route:
javascript
// Dynamic import — loaded only when the route is visited
const Dashboard = React.lazy(() => import('./Dashboard'));
  • Replace heavy libraries:

    • Moment.js (300KB) → date-fns (tree-shakeable, 0KB baseline)
    • Lodash (70KB) → lodash-es (tree-shakeable) or native methods
    • jQuery (87KB) → vanilla JavaScript (2026 browsers don't need jQuery)
  • Dynamic imports for below-the-fold features:

javascript
// Don't load the chart library until the user scrolls to the chart section
const Chart = React.lazy(() => import('./Chart'));

#Step 6: Audit third-party impact

Every third-party script adds:

  • DNS lookup (50-200ms)
  • TCP connection (50-100ms)
  • TLS handshake (50-100ms)
  • Download time (variable)
  • Parse and execute time (variable)

Third-party audit checklist:

  1. List every third-party script in your page source
  2. Categorize each as critical, useful, or unnecessary
  3. Remove unnecessary ones (you'd be surprised how many accumulate)
  4. Defer critical ones (load after first paint)
  5. Self-host where possible

Tools: Third Party Web — database of third-party scripts and their performance impact.

#Step 7: Set performance budgets

A performance budget prevents regression. Set limits and enforce them in CI:

json
{
  "budgets": [
    {
      "path": "/*",
      "timings": [
        { "metric": "LCP", "budget": 2500 },
        { "metric": "INP", "budget": 200 },
        { "metric": "CLS", "budget": 0.1 },
        { "metric": "TBT", "budget": 200 }
      ]
    },
    {
      "path": "/*",
      "sizes": [
        { "resourceType": "script", "budget": 300000 },
        { "resourceType": "style", "budget": 100000 },
        { "resourceType": "image", "budget": 500000 },
        { "resourceType": "total", "budget": 1000000 }
      ]
    }
  ]
}

Learn more: web.dev performance budgets

#Further reading

Frequently asked questions

Why is my website loading slowly?

The most common causes are unoptimized images (large file sizes), render-blocking JavaScript and CSS, slow server response time (TTFB), no browser caching, and too many third-party scripts. Run a Lighthouse audit to identify the specific bottlenecks.

What is a good TTFB (Time to First Byte)?

Under 200ms is excellent, 200-500ms is acceptable, above 500ms needs investigation. High TTFB usually indicates server-side issues: slow database queries, unoptimized code, or geographic distance from the user.

How do I reduce JavaScript bundle size?

Use code splitting to load only what each page needs, tree-shake unused exports, replace heavy libraries with lighter alternatives, and lazy-load below-the-fold components. Webpack Bundle Analyzer and source-map-explorer help identify bloat.

Does image optimization really matter?

Yes — images are typically 50-70% of page weight. Converting from PNG/JPEG to WebP saves 30-50% with no quality loss. Adding explicit width/height attributes also eliminates layout shift (CLS).

Keep reading