Frontend Performance/Monitoring
Lesson 10 of 20 · Episode 10

Profiling with Chrome DevTools

Recording a performance trace, reading the flame chart, and hunting down long tasks that block interaction.

DevToolsFlame chartLong tasks
Watch on YouTube ↗

RUM finds the slow segment; WebPageTest shows the symptoms. To find the line of code at fault, you record a trace in the Chrome DevTools Performance panel. It looks intimidating, but it's really just a few linked views — and once you can read the flame graph, you can catch the classic killer: a forced reflow.

Orientation

The panel's linked views

Don't be scared by the wall of color. The Performance panel is a stack of time-aligned tracks — scroll/zoom one and they all move together:

The colour convention
Throughout the Performance panel, red means “problem here” — a long task, a dropped frame, a forced layout. Hunt the red.
The key skill

Reading the main-thread flame graph

The flame graph reads top-down: a task at the top calls functions below it, each bar a call in the stack. Wider = longer. A task over 50 ms gets flagged as a long task (the red corner). Click into the stack to find who's eating the time — and hover any frame to see whether it's scripting or rendering:

InteractiveA 93 ms long task, drilled into
46%
42%
🔴 Long task · 93ms
Layout (Forced) ⚠Rendering36 ms
Forced synchronous layout — the smoking gun. Reading offsetTop after a write forced this.
Scripting Rendering Painting System
Task → Animation Frame Fired → app.update() → Recalculate Style + Layout (Forced). The red-bordered 'Layout (Forced)' is the real culprit.
The smoking gun

The classic bug: forced reflow

That “Layout (Forced)” entry is the most common surprise in real traces — layout thrashing. It happens when you write a style and then immediately read a layout property (like offsetTop, offsetHeight, getBoundingClientRect()) in the same loop. The read can't be answered until layout is up to date, so the browser is forced to re-run layout synchronously — once per iteration. Toggle the fix:

InteractiveRead-after-write forces a layout every iteration
What the main thread does, per image:
writeread → 🔴 layoutwriteread → 🔴 layoutwriteread → 🔴 layoutwriteread → 🔴 layoutwriteread → 🔴 layoutwriteread → 🔴 layout
Forced reflows / frame
6
Frame health
Janky
❌ Forces a layout every iteration
for (const img of images) {
  img.style.top = top + "px";   // WRITE
  top = img.offsetTop + 3;       // READ → forced synchronous layout!
}
Reading offsetTop right after writing top forces the browser to recalculate layout immediately — for every image, every frame. That's the long task you saw in the flame graph.
Naïve: write → read → forced layout, ×N. Optimized: compute from a local variable, write only, and let the browser lay out once. Same logic, no jank.
The rule of thumb
Batch your reads and writes. Read all the layout values you need first, then do all your writes — never interleave them in a loop. Or, as in the demo, compute from a local variable so you never read the DOM back at all.

Don't take the diagram's word for it — benchmark it on the device you're holding. Both buttons below restyle 360 real DOM elements; one forces 360 synchronous layouts, the other forces exactly one. The numbers come from performance.now(), live:

InteractiveBenchmark it on your machine — 360 real elements

360 real DOM elements. Both buttons restyle every one of them — the only difference is when the layout reads happen.

Thrashing
Batched
Speedup
run both

These numbers are measured live on your device — run them a few times and on a phone if you can. Same writes, same final layout; the thrashing version just asks “how wide is it now?” after every single write, and each answer costs a full synchronous layout pass.

Same writes, same final layout. The thrashing version just asks 'how wide is it now?' after every write — and each answer costs a full layout pass. Expect a 10–100× gap.
In practice

How to record a trace

Two ways to record

Record (then interact, then stop) captures a specific interaction — perfect for debugging a janky scroll or click. Start profiling and reload captures the whole page load from the first byte, and adds a Timings track with DCL, FP, FCP, and LCP marked.

Reproduce your users' conditions

Before recording, set CPU throttling (4×/6× slower to mimic a cheap phone) and network throttling (Slow 3G). A bug that's invisible on your M-series laptop is glaring at 6× CPU — which is exactly what your real users feel. This is how you turn the RUM finding (“mobile users in India are slow”) into a reproducible trace.

Q1Multiple choice
Scanning a Performance recording, where do you start your investigation?
Q2Multiple choice
Inside a loop you set el.style.width then read el.offsetHeight, for 200 elements. Why does this run ~200 layouts instead of one?
Q3Multiple choice
A teammate 'fixes' a layout-thrashing loop by wrapping each read in requestAnimationFrame. It's still slow. Why?
Q4Multiple choice
RUM says the slow segment is mid-tier Android on Slow 3G, but the bug won't show on your laptop. Most faithful repro in DevTools?
Q5Sort each scenario
Does each operation force a synchronous layout (reflow)? Watch for reads of layout properties after a write.
Reading el.offsetWidth right after changing a style
Calling el.getBoundingClientRect() after toggling a class
Setting el.style.transform (write only, no read back)
Reading a width you cached in a local variable earlier
Reading el.clientHeight inside a loop that also writes styles
Key takeaways
  • The Performance panel is time-aligned tracks: Summary, CPU/Frames, Interactions, Main thread.
  • The flame graph shows tasks + call stacks; wider = longer; red = problem (long task).
  • Forced reflow = reading a layout prop right after a write — the classic jank bug.
  • Fix it by batching reads and writes (or caching in a local variable).
  • Record with CPU + network throttling to reproduce real users' conditions.
← Previous
9. Synthetic Monitoring — WebPageTest
Next →
11. Bundle Splitting