Troubleshoot Website Performance Issues: A Developer's Guide
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
| Metric | What it measures | Target |
|---|---|---|
| TTFB | Time to first byte — server response speed | Less than 200ms |
| LCP | Largest Contentful Paint — main content visible | Less than 2.5s |
| INP | Interaction to Next Paint — input responsiveness | Less than 200ms |
| CLS | Cumulative Layout Shift — visual stability | Less than 0.1 |
| FCP | First Contentful Paint — first content appears | Less than 1.8s |
| TBT | Total Blocking Time — main thread blocking | Less than 200ms |
Learn more: web.dev Core Web Vitals
#Tools to measure
-
Lighthouse — built into Chrome DevTools (F12 → Lighthouse tab). Run in desktop mode for accurate results. Use
--preset=desktopif running from CLI. -
Chrome DevTools Performance panel — record a page load, then look at the flame chart for long tasks (anything over 50ms blocks the main thread).
-
WebPageTest — the gold standard for detailed waterfall analysis. Shows exactly what loaded, when, and how long each resource took.
-
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:
-- 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 — 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
- 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).
<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>
- Set explicit dimensions — prevents Cumulative Layout Shift:
<img src="photo.webp" alt="..." width="800" height="600" />
- Lazy load below-the-fold images:
<img src="photo.webp" alt="..." loading="lazy" width="800" height="600" />
- Responsive images — serve different sizes for different viewports:
<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="..." />
- Preload the LCP image — the hero image should not wait for CSS/JS discovery:
<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>:
<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:
<link rel="stylesheet" href="full.css" media="print" onload="this.media='all'" />
#JavaScript
- Use
defer— downloads in parallel, executes after HTML parsing:
<script src="app.js" defer></script>
- Use
async— downloads in parallel, executes immediately (only for independent scripts like analytics):
<script src="analytics.js" async></script>
- Module scripts are deferred by default:
<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:
// 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
- Analyze the bundle:
# Webpack
npx webpack-bundle-analyzer stats.json
# Vite
npx vite-bundle-visualizer
- Find the biggest offenders — sort by size, look for unexpected duplicates.
#Fix strategies
- Tree shaking — import only what you use:
// 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:
// 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:
// 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:
- List every third-party script in your page source
- Categorize each as critical, useful, or unnecessary
- Remove unnecessary ones (you'd be surprised how many accumulate)
- Defer critical ones (load after first paint)
- 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:
{
"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
- High Performance Browser Networking — Ilya Grigorik's comprehensive guide to network optimization
- web.dev Performance — Google's performance guides and case studies
- Vergate Performance Matrix — automated Lighthouse + HTTP resource quality checks with AI fix prompts
- Core Web Vitals — Google's official CWV documentation
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).