Scenario Debugging

The Debugging Method: Measure, Isolate, Hypothesize, Verify

The method every answer in this series uses: which DevTools panel or server number answers which question, and how to explain your choices out loud.

Lesson 0012 min read#method#performance#interview

On this page

  1. The four moves
  2. Move 1: Measure (pin down the symptom)
  3. Move 2: Isolate (which layer owns the time?)
  4. The tool map
  5. Move 3: Hypothesize (one cause at a time)
  6. Move 4: Verify (change one thing, compare to the baseline)
  7. Talking it through in an interview
  8. Anti-patterns
  9. A worked mini-example
  10. Common mistakes
  11. Interview questions
  12. Key takeaways
  13. What's next

In one line: never fix what you have not measured. Every performance or bug scenario in an interview is answered with the same four moves. Measure the symptom. Isolate which layer owns it. Hypothesize one cause (make one clear guess you can test). Verify by changing one thing and measuring again.

Senior interviewers rarely want the "right" answer to "the page is slow." They want to hear how you would find the answer without guessing. This lesson gives you the framework that every later lesson in this series reuses. It also gives you a tool map that shows which panel answers which question.

The four moves

TEXT
1. MEASURE     Turn "it feels slow" into a number, a place and a baseline.2. ISOLATE     Find which layer owns the time: network, server, database, main thread, rendering.3. HYPOTHESIZE Form ONE specific, testable explanation for that layer.4. VERIFY      Change one thing, measure again against the baseline. Keep it only if the number moved.

A baseline is the starting number you compare against. The main thread is the single thread in the browser that runs your JavaScript and updates the page. A long task is a piece of work on the main thread that runs for more than 50 ms. While it runs, the page cannot react to clicks.

Some answers jump straight to "add useMemo" or "add Redis." useMemo is a React hook that memoizes (remembers) a value, so React does not calculate it again on every render. Redis is a fast database that keeps data in memory and is often used as a cache (a saved copy of a result). Those answers skipped moves 1 and 2. This is the most common way to sound junior.

Move 1: Measure (pin down the symptom)

Before you touch code, ask questions that turn a complaint into a precise problem statement:

QuestionWhy it matters
What exactly is slow? Load, a click, scrolling, one API, one page?"Slow" is not a metric. Load speed and interaction speed have different causes
How slow, compared to what? 3 s vs a 1 s goal? vs last week?It gives you a baseline and a finish line
For whom? All users, mobile, one region, one browser, logged-in only, large accounts?A problem for 1% of mobile users is not the same as a problem for everyone
Since when?Link it to a deploy, a jump in data size, a traffic spike, or a dependency update
Is it constant or does it come and go?Constant usually means a code or config problem. Intermittent (on and off) means contention, cache misses, cold starts, or noisy neighbours (other apps sharing the same machine)
What changed?Releases, feature flags, new third-party scripts, data growth, infrastructure changes
Where is the evidence? Field data (RUM), logs, traces, user reportsYour own laptop is the least typical device

Use percentiles, not averages

A percentile is a number that tells you the value below which a given share of your measurements fall. For example, p50 (the median) is the time that half of the requests beat, and p95 is the time that 95% of requests beat. Only the slowest 5% take longer.

Suppose 1% of requests take 10 seconds. The average still looks fine, but thousands of users suffer. So always look at p50, p95 and p99 for latency (the time a request takes). For Core Web Vitals, look at the 75th percentile. Core Web Vitals are three measures from Google about real user experience: LCP (Largest Contentful Paint, how fast the main content appears), INP (Interaction to Next Paint, how fast the page reacts to a click or key press) and CLS (Cumulative Layout Shift, how much the page jumps around while loading). See Browser Contract, Lesson 09.

Field data vs lab data

  • Field data is data measured on real users' devices while they use your site. Its sources are RUM (real user monitoring: a small script in your page that reports timings), analytics, and CrUX (the Chrome User Experience Report, a public data set of real Chrome users' timings from Google). It tells you whether there is a problem and for whom.
  • Lab data is data from tests you run yourself in a controlled setup. Examples are DevTools, Lighthouse (a Google tool that audits a page and gives scores) and a load test (many fake users sent to your server). It lets you reproduce and debug the problem. Slow down the CPU (4x or 6x) and the network. Your development machine hides problems that a mid-range phone shows.

Move 2: Isolate (which layer owns the time?)

A user action passes through many layers. Your job is to find the layer that holds most of the time before you optimise anything.

TEXT
User clicks ─▶ [Main thread: handler, render] ─▶ [Network] ─▶ [Edge/CDN] ─▶ [App server]                                                                              │                              ◀──────────── response ◀── [DB / cache / 3rd-party APIs]

The first fork in the road is the most important one:

Is the time spent waiting for the network or server, or working on the main thread in the browser?

Open the Network tab. Click the slow request, and read the Timing breakdown:

PhaseMeaningIf it is large…
Queueing / StalledWaiting for a free connection, or for a more important request to go firstToo many requests at once; HTTP/1.1 limits; blocked by scripts
DNS Lookup / Initial Connection / SSLSetting up the connectionLong distance, no keep-alive, no CDN, many origins (Browser Contract, Lesson 00)
Waiting for server response (TTFB)The server, and everything behind it, is working. TTFB means "time to first byte": the time until the first byte of the answer arrivesBackend problem: slow code, database, external calls, cold start (Lesson 02)
Content DownloadReceiving the bytesThe payload is too big, compression is missing, or the link is slow

Then open the Performance panel. It shows what happens after the response arrives: scripting, rendering and painting on the main thread.

The decision tree

TEXT
Is the page slow...├─ ...before anything appears?           → Network + server: TTFB, redirects, render-blocking CSS/JS, bundle size│                                           (Lessons 02 and 06)├─ ...after the response arrives,        → Main thread: parsing, re-rendering, long tasks, hydration│   but the API was fast?                   (Lessons 01, 03, 07)├─ ...only on interaction (click/type)?  → Handlers, re-renders, long tasks (INP)         (Lessons 01, 03, 08)├─ ...worse the longer the tab is open?  → Memory leak                                      (Lesson 04)├─ ...making the same request twice?     → Effects, double submits, races                   (Lesson 05)└─ ...jumping around as it loads?        → Layout shift (CLS)                               (Lesson 08)

The tool map

A few terms in the table: the Server-Timing header is an HTTP response header where the server can report its own timings (for example db;dur=53) so that DevTools can show them. A flame chart is a chart where each bar is a function call and its width is the time it took. A heap snapshot is a saved picture of everything in the browser's memory at one moment. APM (application performance monitoring) tools record how long each step of a request takes on the server. OpenTelemetry is an open standard for collecting this kind of trace data. EXPLAIN (ANALYZE, BUFFERS) is a PostgreSQL command that runs a query and reports how the database executed it and how long each step took. The event loop is the Node.js mechanism that takes one task at a time from a queue and runs it on a single thread.

QuestionToolWhat to look at
Which request is slow? Where does its time go?DevTools Network tabWaterfall, Timing breakdown, size (transferred vs resource)
What is the main thread doing?DevTools Performance panelLong tasks (red corners), Scripting vs Rendering vs Painting, flame chart
Why did this component re-render?React DevTools ProfilerCommit flame chart, "why did this render", highlight updates
What is in my bundle?@next/bundle-analyzer, source-map-explorer, Lighthouse "unused JavaScript"Largest modules, duplicates
Is memory growing?DevTools Memory tab, Performance monitorHeap snapshots, detached DOM nodes, listener count
How do real users experience it?RUM with the web-vitals library, CrUX, Search Consolep75 LCP, INP, CLS by device and route
What does the whole page look like on a cold load?Lighthouse, WebPageTestFilmstrip, waterfall, LCP element, blocking resources
Which part of the request is slow on the server?APM / tracing (OpenTelemetry, Datadog, X-Ray), Server-Timing headerSpan breakdown: handler, DB, external calls
Why is this query slow?EXPLAIN (ANALYZE, BUFFERS), pg_stat_statementsSeq scans, sorts, row estimates vs actual (Backend, Lesson 07)
What happened to this one request?Structured logs by request IDThe full story, errors, durations (Backend, Lesson 05)
Is the event loop blocked on the server?Event-loop-lag metric, CPU profilep99 lag, flame graph (Backend, Lesson 10)

Move 3: Hypothesize (one cause at a time)

Write your hypothesis so that it can be proved wrong:

  • Weak: "Maybe the database is slow."
  • Strong: "The /orders endpoint runs one query for each order line (this is called an N+1 problem: 1 query for the list, then N more queries, one for each item in it). So the delay grows with the number of lines. If this is true, a customer with 200 lines should show about 200 queries in the trace. Using include (an ORM option that loads the related rows in one extra query, instead of one query per row) should drop it to 2 queries."

A good hypothesis says what you will see if it is true. It also points to the cheapest experiment.

Ways to narrow it down

  • Bisect. This means cutting the search space in half again and again. If the page used to be fast, find the bad commit with git bisect (a Git command that tests commits in halves until it finds the first bad one). Or turn feature flags and bundles on and off.
  • Remove, don't add. Turn off third-party scripts, browser extensions (use an incognito window), or a suspect component. See if the symptom goes away.
  • Compare a fast case with a slow case. Take the same endpoint with a small account and a large account. What is different?
  • Look at the shape of the cost. If it grows in a straight line with list size, think N+1 or rendering every row. If it jumps at a deploy, think regression (a new change broke something). If it grows over time, think leak. If it spikes now and then, think cron jobs, GC (garbage collection, the memory clean-up) or cache expiry.
  • Reproduce at scale. A bug that needs 10,000 rows will not show up in your small test data. Create realistic amounts of data.

Move 4: Verify (change one thing, compare to the baseline)

  1. Record the baseline first. Use the same device and the same throttling. Do several runs and take the median (the middle value).
  2. Change one variable only.
  3. Measure again and compare. A real fix moves the number by a clear amount, not just by noise.
  4. Check for new problems elsewhere. For example, a cache can fix latency and break freshness.
  5. Add a guard so the problem cannot return quietly. A guard can be a performance budget in CI (an automatic check that fails the build if a number, such as bundle size, goes over a limit), an alert, a regression test (a test that stops an old bug from coming back), or a dashboard panel.
JavaScript
// A minimal, repeatable in-browser measurementperformance.mark("filter:start");applyFilter(rows, query);performance.mark("filter:end");performance.measure("filter", "filter:start", "filter:end");console.table(performance.getEntriesByName("filter"));

Talking it through in an interview

This structure works for any scenario question:

  1. Clarify. ("Is it slow on load or on interaction? For everyone? Since a deploy?")
  2. State the plan. ("First I'd split the time between network, server and main thread.")
  3. Name the tool for each step. ("The Network tab timing, then the Performance panel.")
  4. Give 2 or 3 ranked hypotheses. Say how you would confirm each one.
  5. Suggest fixes with trade-offs. Say what each fix costs and risks.
  6. End with prevention. (Budgets, monitoring, tests.)

Say your thinking out loud. The interviewer is judging your process, not whether you guess the exact bug.

Anti-patterns

An anti-pattern is a habit that looks helpful but usually causes harm.

Anti-patternWhy it fails
Optimising before measuringYou improve something that was never the bottleneck
Memoizing everything (useMemo, memo, useCallback). Memoizing means remembering the result of work so you do not repeat itIt adds extra work and complexity. It fixes nothing if re-renders are not the problem
Adding a cache to hide a slow queryIt hides the cause and adds invalidation bugs. The first uncached request is still slow
Testing only on a fast laptop and fast networkYou miss the users on slow devices and slow networks, who suffer most
Changing several things at onceYou cannot tell which change helped (or hurt)
Trusting averagesYou cannot see the users in the slow tail
"Works on my machine"In production, the data size, the number of users at once, and the caches are different
Fixing symptoms in the UI for a backend problem (or the reverse)The real cost stays

A worked mini-example

Report: "The orders page takes 4 seconds."

  1. Measure: in the Network tab, GET /orders has Waiting (TTFB) = 3.4 s. Content download is 40 ms. The Performance panel shows that the main thread is idle for most of that time.
  2. Isolate: the time is on the server, not in the browser.
  3. Hypothesize: the trace shows 120 almost identical SELECT … FROM order_items WHERE order_id = $1 queries. This is an N+1. The prediction: a customer with 5 orders is fast, and one with 200 orders is slow.
  4. Verify: switch to two queries in total (the orders, then all their items with IN or include). TTFB drops from 3.4 s to 180 ms. Check it again with the 200-order customer. Add a test that says "at most 3 queries" and an alert on p95 latency.

Two minutes, four moves, no guessing. That is the whole series in a small form.

Common mistakes

  1. Starting with a solution instead of a measurement.
  2. Looking only at the layer you know best. (Frontend engineers blame React. Backend engineers blame the network.)
  3. Using averages and a single run.
  4. Debugging in conditions that are unlike production (no throttling, tiny data).
  5. Changing several things, then giving credit to the wrong one.
  6. Never adding a guard, so the same regression comes back months later.

Interview questions

How do you approach "the app is slow"?

First I clarify the symptom: what is slow, for whom, since when, and how slow. Then I measure it. I split the time between network, server and main-thread work with the Network and Performance panels. I make one testable guess for the layer that owns the time. I change one thing and measure again against the baseline. Last, I add monitoring or a budget so it does not come back.

Lab vs field data?

Field data comes from real users (RUM, CrUX). It tells me whether there is a problem, and for which devices, regions and routes, using percentiles like p75. Lab data comes from controlled runs (Lighthouse, DevTools with throttling). It lets me reproduce the problem, profile it and check my fixes. I use field data to decide what matters most, and lab data to debug.

What do you measure before changing any code?

I measure a baseline for the exact symptom. I pick the metric (LCP, INP, TTFB, endpoint p95). I fix the conditions (device, CPU and network throttling, data size). And I repeat the run several times. Then I break the time down by layer, so I know where it goes. Without a baseline, I cannot prove that a change helped.

Why look at percentiles instead of averages?

Averages hide the slow tail. A few very slow requests barely change the mean, but they hurt real users. Percentiles (p95, p99) show what the slowest users experience. They are also the basis for SLOs (service level objectives, which are targets for reliability, such as "95% of requests finish in under 300 ms") and for Core Web Vitals (75th percentile).

How do you tell a frontend problem from a backend problem?

In the Network tab, check the request's Timing. A large "Waiting for server response" (TTFB) points to the backend. A fast response followed by a busy main thread in the Performance panel points to the frontend (parsing, rendering, long tasks). Server-Timing headers and traces confirm the backend breakdown.

What is the risk of fixing a slow endpoint by adding a cache first?

A cache hides the root cause. It adds the work of invalidation (deciding when saved data is too old) and the risk of stale data. Also, requests that are cold or missed are still slow. So I first find out why the query or code is slow (indexes, N+1, payload size) and fix that. I add caching only where the way the data is used justifies it.

How do you make sure a performance fix stays fixed?

I add a regression guard. This can be a performance budget in CI (bundle size, Lighthouse limits), a test that checks the query count, and dashboards and alerts on p95 latency or web vitals. I also use RUM to watch real users after the release.

Scenario: a user says only "checkout is slow." What do you do first?

I turn the sentence into a problem I can measure. Which step is slow (loading the page, clicking pay, the confirmation)? For whom (device, region, account size)? Since when? How slow compared with before? Then I look at field data and traces for that step. I split the time between network, server and main thread, and I check what changed recently. Only then do I make a hypothesis. I do not touch code until I have a baseline.

Scenario: a fix made things fast on your machine, but production p95 did not change. Why?

There are several likely causes. The test may have used small data or a warm cache. The real bottleneck may be in another layer, or in a group of slow devices. The change may not have reached the slow path. Or the metric may be driven by a different tail (cold starts, one particular tenant). I compare lab and field data for the same group, check the deployed version, and measure the breakdown again in production before I try another fix.

Key takeaways

  • Measure, isolate, hypothesize, verify. Do them in this order, and change one thing at a time.
  • The first fork: is the time before the response (network and server) or after it (main thread)? The Network timing tab answers this in seconds.
  • Use percentiles. Use field data to decide what matters and throttled lab data to debug.
  • Write hypotheses that predict what you will see. Use bisecting and comparisons.
  • Add a guard (budget, test, alert) so the fix lasts.

What's next

Lesson 01: "The API Is Fast but the UI Is Slow". This is the first scenario. The Network tab says 80 ms, but users wait two seconds. We follow the time onto the main thread.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related lessons

Part of the study guide

Related series