Debugging a Slow or Broken Page

Lesson 10 — Debugging a Slow or Broken Page | Firefox DevTools
Lesson 10 of 11 · Scenario

Debugging a Slow or Broken Page

Firefox Developer Tools · Inspector · Console · Network · Debugger · Performance
A page can be "broken" in many ways — wrong layout, crashed click handler, 10-second load, or a frozen scroll. Each failure type has a home in a specific DevTools panel. This lesson teaches a systematic triage order and shows how to apply the right tool at the right moment.

The Triage Order

Resist the urge to jump straight into the Debugger. Start broad and work inward. Most bugs are resolved by step 2 or 3.

1
Console
Is there an uncaught error? Does the stack trace point to your code?
~10 sec
2
Network
Did all resources load? Are any requests failing or slow?
~30 sec
3
Inspector
Is the DOM correct? Are the right CSS rules being applied?
~1 min
4
Debugger
Where exactly in the code does it go wrong? Step through it.
5–20 min
5
Performance
Why is it slow when there are no obvious errors?
10–30 min

Part 1 — Visual & Layout Bugs

When an element is missing, invisible, or in the wrong place, the Inspector is the first stop. Right-click the element → Inspect Element to jump directly to it in the DOM.

Reading the Rules panel

In the Inspector, the right-hand Rules panel lists every CSS rule that applies to the selected element, in order of specificity — the topmost rule wins. Overridden properties are shown with a strikethrough.

.modal-overlay { layout.css:84
display: none;
}
.modal-overlay.active { layout.css:92
display: flex; ← wins (more specific)
}

The strikethrough on display: none means it was overridden. If you see display: none without a winning override, the JavaScript that should add the .active class never ran — check the Console for errors.

🔑 Common visibility causes The four most common reasons an element is invisible: display: none, visibility: hidden, opacity: 0, or being clipped by overflow: hidden on a parent. All are visible in the Rules panel.

Responsive design bugs

Open Responsive Design Mode (Ctrl + Shift + M) to resize the viewport. As you drag the width, watch the Rules panel — @media rules grey out when their condition is not met. Click any @media label to jump to the breakpoint declaration in the Style Editor.

Part 2 — JavaScript Errors

Open the Console. JavaScript errors appear in red and always include a clickable stack trace — use it.

Console Network Debugger
🔴 Uncaught TypeError: Cannot read properties of undefined (reading 'email')
at UserProfile (app.js:142:18) ← click to open in Debugger
at renderDashboard (app.js:87:5)
at main (app.js:12:3)

This tells you: something at app.js:142 tried to read .email from undefined. Click the link — Firefox opens the exact line in the Debugger. The object you expected to exist (a user object from an API call) was never populated — likely because the API call failed or returned an unexpected shape.

Common error signatures

Console messageLikely cause
Cannot read properties of undefined (reading 'X')Object wasn't initialised — API returned an unexpected shape, or data hasn't loaded yet
X is not a functionYou called something that isn't a function — variable shadowing, or a typo
ReferenceError: X is not definedVariable used before declaration, or a typo in the name
Uncaught (in promise) Error: …Async function threw with no .catch() or try/catch
SyntaxError: Unexpected token '<'A fetch() expected JSON but got HTML — check the Network panel Response body for a 404 page
Failed to fetchfetch() failed — check the Network panel for the corresponding request status

Catching async errors in the Debugger

When the Console shows "Uncaught (in promise)", click the Exception breakpoint icon (⬡ hexagon) in the Debugger toolbar → select Pause on uncaught exceptions. Trigger the failing action — the Debugger pauses at the exact throwing line, with the full call stack visible on the right.

Part 3 — Slow Page Loads

Open the Network panel. Click the Reload button (🔄) in the toolbar to capture the full page load. Look at the DOMContentLoaded (blue line) and Load (red line) markers at the bottom of the request list.

Waterfall anatomy — a slow load

Network All XHR JS CSS Img ☑ Disable Cache
Status
URL
Size
Time
Waterfall →
200
example.com/index.html
12 KB
80ms
200
example.com/app.bundle.js ⚠ 2.8 MB
2.8 MB
3 400ms
200
example.com/styles.css
48 KB
120ms
200
api.example.com/users
8 KB
210ms
DCL 3.5s
Load 3.7s
⚠ app.bundle.js (2.8 MB, no defer) is blocking HTML parsing — DCL is delayed to 3.5s

Everything after app.bundle.js starts only after the script finishes — the yellow bar consumes 68% of the waterfall timeline. Adding defer to the script tag would allow the HTML to parse in parallel, moving DCL to under 500ms.

Diagnosing slow requests — TTFB vs payload

Sort the Network panel by Time descending. Click the slowest request → Timings tab. The question is: is the time dominated by Waiting (server is slow) or Receiving (payload is large)?

Slow server (long TTFB)
DNS Lookup
8ms
Connecting
14ms
Waiting (TTFB)
2 800ms
Receiving
12ms
Fix: optimise DB query, add caching, check for N+1 queries, or check server cold-start
Large payload (long receive)
DNS Lookup
8ms
Connecting
14ms
Waiting (TTFB)
80ms
Receiving
3 100ms
Fix: enable gzip/Brotli compression, paginate large API responses, or split the bundle

Part 4 — Runtime Performance (Jank & Freezes)

When the page loads fine but slows down during use — scroll jank, a frozen UI after a click, an animation that stutters — the Performance panel is the right tool. Open it with Ctrl + Shift + P.

A performance recording with a long task

Performance CPU 4× slowdown
FPS
Idle
Task — click handler
Idle
filterProducts()
Layout
sort()
Layout
Array.forEach ×2400
Paint
Summary
Bottom-Up
Call Tree
filterProducts · app.js:214
320ms self
recalculate styles · (browser)
190ms self
Array.prototype.sort · (native)
85ms self

The red section in the FPS bar corresponds to the long JS task (marked ▲). The Bottom-Up tab immediately names filterProducts at app.js:214 as the most expensive function — that's where to look first.

Layout thrashing — the pattern

The repeated Layout bars inside the JS task above are a sign of layout thrashing: JavaScript reads a layout property after changing the DOM, forcing the browser to recalculate layout synchronously on every iteration.

✖ Bad — layout thrashing
// Reads offsetWidth AFTER each write
elements.forEach(el => {
    // write, then immediate read
    el.style.width =
        container.offsetWidth + 'px';
});
✔ Good — read once, write in bulk
// Read once before the loop
const w = container.offsetWidth;

elements.forEach(el => {
    el.style.width = w + 'px';
});

The Bottom-Up shortcut

After recording any profile, open the Details panel → Bottom-Up tab → sort by Self Time descending. The top entry is the function doing the most work. Click it to jump to it in the flame chart. This is always faster than reading the flame chart left-to-right.

Cross-Panel Workflow — A Real Example

Scenario: "The dashboard loads but freezes every time I click 'Load More'."

Step 1
Console
Open DevTools. Click Load More. Any red errors? If yes — fix the error first. A TypeError or ReferenceError here often explains the whole problem. If no errors — continue.
Step 2
Network → XHR/Fetch filter
Click Load More. Does a request fire? No → the click handler isn't calling the API. Check the Debugger. Yes, but 500/422 → server error. Check the Response body tab. Yes, 200 but page still freezes → the JS processing the response is slow. Go to Performance.
Step 3
Performance
Record while clicking Load More. Find the click event in the flame chart. Open Bottom-Up → sort by Self Time. If the top entry is JSON.parse or a filter/sort function, the response payload is too large or the processing is unoptimised.
Step 4
Debugger (if still unclear)
Set a breakpoint at the start of the loadMore function. Step through with F10. Watch the Scopes panel — at which line does the data become unexpectedly large, or does an operation take multiple iterations over thousands of items?

Practical Tips

ℹ Event Listeners tab — fastest way to find a click handler In the Inspector, select any element → in the right panel, click Event Listeners. This lists every event handler registered on the element with the function name, file, and line number. Click any handler to jump directly to it in the Debugger. No searching, no guessing.
🔑 $0 in the Console After selecting an element in the Inspector, switch to the Console and type $0. This gives you a live JavaScript reference to that element. Call methods, read properties, check its current dimensions — all without writing setup code: $0.getBoundingClientRect()
🔑 Ctrl + Shift + R vs F5 F5 = normal reload (uses cache). Ctrl + Shift + R = hard reload (ignores cache). Always use Ctrl + Shift + R when investigating resource loading — otherwise you may be looking at a cached version of a file you already fixed.
ℹ Disable Cache checkbox In the Network panel toolbar, check Disable Cache. This forces all resources to re-download on every reload while DevTools is open — so you always see real network timings rather than cache hits. Uncheck when you're done.

Exercises

Right-click any button on a page → Inspect Element. In the Inspector's right panel, click the Event Listeners tab. What function is registered on click? What file and line number is it on? Click the link — does it open in the Debugger?
Open the Console and paste:
document.querySelectorAll('img').forEach((img, i) => {
  if (!img.complete || img.naturalHeight === 0)
    console.warn(`Image ${i} failed:`, img.src);
});
Are any images broken? Switch to the Network panel → filter by Images to confirm.
Open the Network panel and reload a page. Sort by Size descending. What is the largest resource? Is it compressed — does the Transferred column show a smaller number than Size?
Sort the Network panel by Time descending. Click the slowest request → Timings tab. Is the long time in Waiting (TTFB) or Receiving? What would you investigate differently depending on the answer?
Open the Performance panel (Ctrl + Shift + P). Click Record, scroll the page for 3 seconds, stop. Are there any red sections in the FPS bar? Click into one → open the Bottom-Up tab. What function has the highest Self Time?
In the Console, run:
performance.getEntriesByType('navigation')[0]
Read the domContentLoadedEventEnd, loadEventEnd, and transferSize values. What do they tell you about this page's load performance?
Open the Debugger panel. In the right sidebar, expand Event Listener BreakpointsMouse → check click. Click any interactive element on the page. The Debugger pauses at the first line of the click handler. Press Shift + F11 (Step Out) to jump back up the call stack. Uncheck the breakpoint when done.

Key Takeaways

  • Triage in order — Console → Network → Inspector → Debugger → Performance. Most bugs are solved before step 4.
  • Stack traces are clickable — click any file.js:line in a Console error to jump directly to the Debugger at that line.
  • SyntaxError: Unexpected token '<' after a fetch means the server returned HTML (an error page) instead of JSON — check the Network Response body.
  • Sort Network by Size to find large resources; sort by Time to find slow ones. Timings tells you whether the delay is TTFB (server) or Receiving (payload).
  • Render-blocking scripts without defer or async in <head> block parsing — visible as a gap in the waterfall where nothing else starts.
  • Layout thrashing (read → write → read → write in a loop) appears as repeated purple Layout bars inside a JS task. Fix: read layout properties once, outside the loop.
  • Bottom-Up → Self Time is the fastest route to the most expensive function in any performance recording.
  • Event Listeners tab in the Inspector shows every handler on a selected element with a direct link to the Debugger.
  • $0 in the Console gives you a live reference to the element currently selected in the Inspector.
  • Disable Cache in the Network toolbar ensures you always see real load times, not cached results.
Next: Lesson 11 — Reverse-Engineering an API. The final scenario: using the Network panel, Console, and Debugger to discover, document, and replay an undocumented internal API — the same technique professionals use for integration work and compatibility testing.