Performance Testing Basics
Users abandon slow websites. Every second matters. Learn to measure page load times, identify bottlenecks, and spot performance bugs using browser DevTools.
The Hook — The Two-Second Rule
A Wellington SaaS company launched a new checkout flow. The page was feature-complete, validation worked, and data was correct. But checkout took 4.2 seconds to load. Within a week, their cart abandonment rate jumped from 8% to 22%. Users weren't seeing bugs. They were leaving because the page felt slow.
Performance is not optional. A slow page is a failing page. Users don't read error messages; they hit the back button. Performance testing is not "we'll optimise later." It is QA. It is finding problems before they reach production.
The Rule
Performance testing measures how fast a website or app responds to user actions. It covers:
- Page load time: How long until the page is interactive? (Target: < 3 seconds)
- Core Web Vitals: Largest Contentful Paint (LCP), First Input Delay (FID), Cumulative Layout Shift (CLS)
- Interaction response: How long to respond when a user clicks a button? (Target: < 100ms)
- Memory usage: Does the app leak memory or grow unbounded?
- Network bottlenecks: Which assets are largest or slowest?
Key Metrics (No Tools Needed, Just Browser)
Largest Contentful Paint (LCP): When the biggest element on the page becomes visible. If it takes 5+ seconds, users see a blank screen and assume the page is broken.
First Input Delay (FID): Time between the user clicking a button and the app responding. If it's > 100ms, the interaction feels sluggish.
Cumulative Layout Shift (CLS): How much does the page jump around while loading? If elements move unexpectedly, users might click the wrong thing.
Senior engineer insight
The thing that changed how I think about performance testing is percentiles. For years I reported average response times and called it done — until a client's rates portal (used by thousands of NZ households every July for annual rates payments) showed an average of 800ms but a p99 of 22 seconds. Average looked fine. Real users were timing out. Now I never show averages without p95 and p99 alongside them; the average is almost always a lie.
The most common mistake juniors make: testing performance only once on their own machine with a warm cache, fast Wifi, and no concurrent users — conditions that will never exist in production.
From the field
A NZ district council was rolling out a new online portal for rates payments, dog registrations, and building consents. The team ran functional tests, signed off UAT, and deployed in May — well ahead of the July rates billing peak. When July hit, the rates-payment page collapsed under ~600 concurrent users; response times climbed past 30 seconds and most requests timed out. The team had never load-tested at all, assuming "it worked in testing" was enough. Post-incident analysis found three compounding issues: an unindexed ratepayer lookup query, a 4MB hero image served without compression, and a session-store that serialised to the database on every page view. The fix took three weeks and the council issued a public apology. The lesson that generalises: annual billing peaks are predictable — they are the worst possible time to discover you never load-tested.
Load vs Stress vs Soak Testing
Performance testing comes in three flavours, each revealing different problems:
- Load Testing: Run your normal expected load. Example: an e-commerce checkout page expecting 100 concurrent users during business hours. Measure response time and error rate at normal volume. Goal: confirm the app stays fast under expected conditions.
- Stress Testing: Gradually increase load until the system breaks. Example: ramp from 100 to 500 to 1,000 concurrent users and see where things fail. Goal: find the breaking point so you know your capacity ceiling.
- Soak Testing: Run a realistic load for a long time (8-24 hours). Example: simulate 200 concurrent users for 12 hours. Goal: detect memory leaks, connection pool exhaustion, and gradual degradation that wouldn't show up in a 5-minute test.
Real Example: E-commerce Checkout Under Load
A NZ online retailer runs a load test during the Boxing Day sale. Expected: 200 concurrent users checking out. They run a 10-minute test simulating 200 users clicking "Pay Now." Results:
- Average response time: 1.2 seconds
- 95th percentile (p95): 3.1 seconds
- 99th percentile (p99): 8.7 seconds
- Error rate: 0.1% (1 out of 1000 requests fails)
- Conclusion: acceptable for normal load, but p99 is concerning. If a user is in the slowest 1%, they wait 8.7 seconds.
Reading a Performance Report: Throughput, Response Time, Error Rate
Throughput: How many requests per second does the server handle? A healthy server processing 500 concurrent users should show throughput of ~100 requests per second (all of them completing in under 5 seconds). If throughput drops while load increases, the server is getting saturated.
Response Time: Never look at average alone. Always look at percentiles (p50, p95, p99). The average might be 500ms, but if p99 is 10 seconds, you have users experiencing slow interactions.
Error Rate: What percentage of requests fail? 0.1% is acceptable; 1% is concerning; 5%+ means the system is overwhelmed. Errors include timeouts, 500 errors, and connection resets.
Understanding Response Time Percentiles
Percentiles are critical for understanding user experience:
- p50 (median): The time 50% of users experience. If p50 = 500ms, half your users get the page in 500ms or faster.
- p95: The time 95% of users experience. If p95 = 1.5s, 95% of users load the page in 1.5s or faster. But 5% wait longer.
- p99: The time 99% of users experience. If p99 = 8s, that last 1% (1 out of 100 users) waits 8 seconds. On a busy site, that's significant.
Example: A SaaS app reports p50=300ms, p95=1200ms, p99=5000ms. The app feels fast for most users (p50), acceptable for most (p95), but painful for the slowest 1% (p99). If you want a fast experience, focus on lowering p99.
Example: Bad LCP
User opens a NZ news website. For 6 seconds, the page is blank. Then the headline appears. LCP = 6 seconds. The user is frustrated and leaves.
Why? The hero image is 8MB and the CDN is slow. Solution: compress the image, or load placeholder text while the image downloads.
Measuring Performance with DevTools
Step 1: Open DevTools (F12)
Step 2: Go to Performance tab
Step 3: Click the record circle, then reload the page. Click the circle again to stop.
You will see:
- Blue line (DOMContentLoaded): When the HTML and CSS are parsed. Usually < 2s.
- Red line (Load): When all resources (images, scripts) are downloaded. Often 3–5s.
- LCP marker: The green vertical line showing when the largest element became visible.
Real Performance Test Scenario: E-commerce Product Page
Setup: A team wants to test if their product page stays fast when it gets 1,000 concurrent visitors during a flash sale.
Test plan:
- Simulate 100 users, ramp up to 1,000 over 10 minutes (load test)
- Each user loads the product page 5 times with a 30-second think time between loads
- Measure: page load time, image load time, "Add to Cart" button response time, error rate
- Run for 30 minutes total
Results:
- Average load time: 2.1s (target: < 3s, PASS)
- p95 load time: 4.2s (concerning—5% of users experience slow loads)
- Image load time: 1.8s average, p95 = 3.4s (images are the bottleneck)
- "Add to Cart" response: 150ms average (acceptable)
- Error rate: 0.2% (1 in 500 requests timeout)
Actions: Images need optimization. Compress them, add lazy loading, or move them to a faster CDN. The "Add to Cart" endpoint is fine; focus on static asset delivery.
Simple k6 Load Test Example
k6 is a lightweight load testing tool that lets you simulate users in code. Here's a minimal example for testing an e-commerce checkout:
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
stages: [
{ duration: '2m', target: 100 }, // ramp up to 100 users
{ duration: '5m', target: 100 }, // stay at 100
{ duration: '2m', target: 0 }, // ramp down
],
};
export default function () {
// Step 1: Load the product page
let res = http.get('https://shop.example.com/product/123');
check(res, {
'product page loads': (r) => r.status === 200,
'page loads in < 3s': (r) => r.timings.duration < 3000,
});
// Step 2: Wait 5 seconds (user thinking time)
sleep(5);
// Step 3: Click "Add to Cart"
res = http.post('https://shop.example.com/cart', {
product_id: 123,
quantity: 1,
});
check(res, {
'add to cart succeeds': (r) => r.status === 200,
'add to cart responds in < 200ms': (r) => r.timings.duration < 200,
});
}
Tools Overview: k6, JMeter, Gatling
- k6: Code-first load testing. Write tests in JavaScript. Lightweight, fast, good for CI/CD. Start here if you're new to load testing.
- Apache JMeter: GUI-based load testing. No coding required. Good for teams without developers. Steeper learning curve but very flexible.
- Gatling: High-performance load testing. Code-based (Scala). Best for large-scale stress testing (10,000+ concurrent users). More complex to set up.
Common Performance Pitfalls
Pitfall 1: Uncompressed assets. A 5MB hero image loads on a 4G connection in 30+ seconds. Compress to < 200KB with proper image formats.
Pitfall 2: Blocking JavaScript. A large JavaScript file (500KB) prevents the page from rendering until it downloads. Use code splitting and lazy loading.
Pitfall 3: N+1 database queries. Showing a list of 10 products triggers 11 queries (one for the list, one per product). Use batch queries or joins instead.
Pitfall 4: No caching. The same API endpoint is called 100 times per second. Use HTTP caching (ETag, Last-Modified) or a cache layer (Redis) to avoid redundant computation.
Pitfall 5: Memory leaks in long-running services. A soak test reveals that memory grows from 500MB to 2GB over 24 hours. Investigate event listeners, timers, and global variables that are never cleaned up.
What to Watch For
- Long task (yellow block): JavaScript running for > 50ms blocks user input. Look for slow scripts.
- Render (purple block): Recalculating layout and painting to screen. Long renders mean heavy CSS or DOM manipulation.
- Layout shift (orange): Elements moving unexpectedly. Causes misclicks.
Network tab (to spot large assets):
- Open DevTools → Network tab
- Reload the page
- Look at the "Size" column — images should be < 200KB each
- Look at the "Time" column — assets should download in < 1 second on 4G
- If one image is 5MB and takes 10 seconds, that's a problem
Common Mistakes
❌ Testing Only on a Fast Connection
Your office has gigabit Wifi. A rural NZ customer on 4G loads the same page in 15 seconds. Always throttle your network: DevTools → Network → choose "Fast 3G" or "Slow 3G" before measuring.
❌ Testing Only on a High-End Device
Your MacBook with 16GB RAM loads instantly. A budget Android phone with 2GB RAM struggles. DevTools can throttle CPU (6x slowdown) to simulate a slower device.
❌ Not Clearing Cache
First load is slow; second load is fast (cached). Always disable cache in DevTools (checkbox in Network tab) or open in incognito mode to test fresh load performance.
❌ Ignoring Interaction Performance
Page loads in 2 seconds but clicking a button takes 5 seconds to respond. That's a performance bug. Test not just page load, but button clicks, form submissions, and scrolling.
Why teams fail here
- Testing only at "happy hour" load — the number of concurrent users you had last Tuesday — rather than the spike you get during end-of-financial-year billing runs, Boxing Day sales, or a government portal campaign launch.
- Reporting averages instead of percentiles, which hides the long tail of slow users; a p99 ten times higher than p50 is a sign the system is quietly failing a real slice of your audience.
- Running load tests against a dev or staging environment with a fraction of the production data set — query plans change completely when a table has 1 million rows instead of 500 test records.
- Stopping the test after five minutes and declaring victory, missing the slow memory and connection-pool degradation that only appears after several hours of sustained load (soak testing).
Key takeaway
Performance testing is not a final checklist item — it is the only way to know whether your system can survive the one day of the year when everyone shows up at once.
Interview Questions
What NZ hiring managers ask about performance testing fundamentals.
Q1. What is the difference between load testing, stress testing, and soak testing?
Strong answer: Load testing validates that the system meets performance SLAs under the expected number of concurrent users — it tests the normal case. Stress testing pushes beyond the expected load to find the breaking point — at what point does the system fail or degrade? This reveals capacity limits. Soak testing (endurance testing) runs at normal load for an extended period — hours or days — to find problems that only emerge over time: memory leaks, database connection pool exhaustion, log files filling disk. For NZ e-government services, all three matter: load testing validates normal usage, stress testing plans for high-traffic events (benefit claim periods, tax deadlines), soak testing catches the slow degradation that hits late on a busy Monday morning.
Q2. What is an SLA and how does it relate to performance testing?
Strong answer: An SLA (Service Level Agreement) is a formal commitment about system performance — for example, "95th percentile response time under 2 seconds for up to 500 concurrent users." Performance testing validates that the system meets the SLA. Without an SLA, "is the system fast enough?" has no answer. When writing performance tests, the SLA defines the pass/fail criteria: if the test shows 95th percentile at 3.2 seconds, the test fails. In NZ public sector projects, AoG (All-of-Government) digital service standards include performance guidance; for consumer-facing applications, the benchmark is typically 2-3 seconds for initial load on a mobile connection.
Q3. What would you include in a basic performance test report for a stakeholder who is not technical?
Strong answer: Translate metrics into business impact. Instead of "p95 latency was 2,847ms," write: "95% of users got a response within 3 seconds — within the agreed target." Show: the number of simulated concurrent users, the key transaction types tested (login, search, checkout), whether SLAs were met or missed for each, and the peak load where performance degraded. Include a recommendation: if the system can handle 300 concurrent users before response times degrade, and the expected peak is 200, say "the system has adequate headroom for the launch but should be monitored at peak trading periods." Stakeholders need to know whether to worry, not the raw numbers.
Self-Check
- What does LCP stand for and why does it matter?
Largest Contentful Paint. It's when the biggest element on the page becomes visible. If it takes > 4 seconds, users think the page is broken. - You measure page load at 2 seconds on your office Wifi. A user on 4G reports it takes 20 seconds. Why?
You tested on a fast connection. Always use DevTools Network Throttling (Fast 3G or Slow 3G) to simulate real-world conditions. - The page loads in 3 seconds but feels slow when scrolling. What might the problem be?
Layout shift or janky rendering. Look at the Performance tab for long render blocks (purple) or layout shifts (orange). JavaScript may be blocking the main thread. - An image on the page is 12MB and takes 15 seconds to load. What should you report?
Performance bug. Images should be < 200KB and load in < 1 second on 4G. The image needs compression or the CDN needs optimisation. - You reload the page and it feels fast. You reload again and it feels slower. Why?
Cache. First load is fresh (no cache). Second load is cached (fast). Always disable cache in DevTools when testing. - You run a load test with 100 concurrent users. Average response time is 800ms, but p99 is 6 seconds. What does this mean?
99% of users experience acceptable load times, but the slowest 1% wait 6 seconds. On a busy site with 10,000 users, that's 100 users frustrated. Focus on reducing p99, not just the average. - A soak test runs for 8 hours and response times start fine but degrade to 10+ seconds by hour 7. What's the likely cause?
Memory leak. The app is not cleaning up resources (event listeners, DOM elements, timers). Memory fills up, garbage collection takes longer, and performance degrades. Check for listeners that are never removed and objects that grow over time. - You're testing an API endpoint. Throughput is 50 requests per second with 200 concurrent users. But when you increase to 500 concurrent users, throughput drops to 30 requests per second. Why?
The server is becoming saturated. It can't handle the increased load, so more requests queue up and timeouts occur. This is your system's breaking point.