The Performance Panel

⚡ Lesson 7 — The Performance Panel

The Performance panel records every millisecond of browser work — JavaScript, layout, paint, compositing — and displays it as a flame chart. It answers the question "why is my page slow?" with precision. Open it with Ctrl + Shift + P, or press F12 and click the Performance tab.

🔄 The Browser Rendering Pipeline

Every frame the browser draws goes through five stages. At 60 FPS each frame has just 16.7 ms to complete all five. If any stage overruns, the frame is dropped and the user sees a stutter.

🟡
JavaScript
Event handlers, timers, fetch callbacks, framework renders
🟣
Style
Recalculate which CSS rules apply to which elements
🟣
Layout
Calculate size and position of every element (reflow)
🟢
Paint
Draw pixels — colours, text, images, borders
🔵
Composite
Combine layers and send the result to the screen
⚠ The Main Thread Is Single-Threaded
JavaScript, Style, and Layout all run on one thread. If your JS runs for 200ms without yielding, the browser cannot process clicks, update animations, or do anything else for that entire time. Any task exceeding 50ms is a Long Task — highlighted red in the Performance panel.

🗺️ The Performance Panel Interface

⏺ Record 🔄 Reload CPU: 4× slowdown ▾ Ctrl+E — start/stop
FPS
FCP
DCL
L
CPU
0ms500ms1000ms1500ms2000ms2500ms
Main
Parse HTML
app.init()
Recalc Styles
Layout
Paint
fetchUsers()
loadModules()
renderUI()
JSON.parse()
import()
eval()
buildDOM()
setStyle
processRows()
Bottom-Up
Call Tree
Summary
Event Log
JSON.parse
87ms
87ms
buildDOM
54ms
61ms
loadModules
38ms
96ms
Recalculate Style
31ms
31ms

The red triangle on the app.init() bar marks it as a long task (>50ms). The Bottom-Up panel shows JSON.parse with the highest self-time (87ms) — the most expensive individual function in the recording.

📊 Frame Rate and the 16.7ms Budget

Frames per second → visual feel
60 FPS
16.7ms/frame — silky smooth ✓
30 FPS
33ms/frame — acceptable for non-animated content
15 FPS
67ms/frame — visibly janky, users notice
<10 FPS
100ms+/frame — appears frozen
💡 Zoom Into Red FPS Sections Click and drag on the overview strip to zoom into any time range. Zoom into a red section of the FPS bar to see exactly which task caused the frame drop. The flame chart updates to show that window's detail.

🔴 Long Tasks (>50ms)

Any main-thread task exceeding 50ms gets a red triangle in the top-right corner of its flame chart bar. These are your highest-priority optimisation targets — they block input, animation, and all other work.

Large JS Bundle Parsing
Parsing and compiling a 2 MB bundle on load blocks the thread for hundreds of milliseconds before any user code runs.
Code-split the bundle; defer non-critical scripts with async/defer
Layout Thrashing
Reading layout properties (offsetWidth, getBoundingClientRect) immediately after writing styles forces synchronous reflow on every read.
Read layout values once before writing; batch DOM writes in requestAnimationFrame
Large JSON.parse()
Parsing a 5 MB API response synchronously on the main thread. JSON.parse() is blocking and cannot be interrupted.
Paginate API responses; consider streaming JSON parsing or a Web Worker
Third-Party Scripts
Analytics, chat widgets, and ad scripts run on your main thread and often contain unoptimised code outside your control.
Load third-party scripts with async; consider a facade pattern to defer until interaction

Layout Thrashing — the Pattern to Avoid

// Bad — forces layout recalculation on every loop iteration elements.forEach(el => { el.style.width = container.offsetWidth + 'px'; // read, then write }); // Good — read once outside the loop, then write in bulk const width = container.offsetWidth; // one layout read elements.forEach(el => { el.style.width = width + 'px'; // write only });

In the flame chart, thrashing appears as dozens of alternating purple Layout bars inside a yellow JS task — a very distinctive "striped" pattern.

🎨 CSS Property Cost and Animations

Not all CSS changes are equally expensive. Properties that only require compositing skip layout and paint entirely and run on the GPU — they can never cause a long task on the main thread.

CSS PropertyTriggersCostUse for animation?
transform, opacity Composite only ✅ Very cheap ✓ Always prefer these
color, background-color, border-color Paint + Composite 🟡 Medium OK for simple transitions
width, height, padding, margin Layout + Paint + Composite 🔴 Expensive ✗ Avoid — use transform instead
top, left, right, bottom Layout + Paint + Composite 🔴 Expensive ✗ Use translate() instead
font-size, line-height Layout + Paint + Composite 🔴 Expensive ✗ Avoid animating text metrics
ℹ️ The Golden Rule for Animations Use transform: translateX() instead of left. Use opacity instead of visibility or display. Add will-change: transform to elements that animate frequently — this tells the browser to promote them to their own compositor layer in advance. Check css-triggers.com for a full property reference.

📍 Key Timeline Markers

MarkerFull NameWhat to Optimise
FCP First Contentful Paint When the first text or image appears. Everything before this is a blank screen. Reduce render-blocking scripts and large CSS files.
DCL DOMContentLoaded When HTML parsing is done and the DOM is ready. Long JS tasks before this delay interactivity.
L Load event All resources have loaded. Late-loading images and fonts push this right. Lazy-load below-the-fold images.
LCP Largest Contentful Paint When the largest visible element (hero image, headline) renders. A Core Web Vital — target under 2.5s.

📌 Adding Your Own Performance Marks

Annotate the timeline with named markers using the Performance API — they appear as labelled regions in the flame chart:

performance.mark('data-fetch-start'); const data = await fetch('/api/users').then(r => r.json()); performance.mark('data-fetch-end'); performance.measure('data-fetch', 'data-fetch-start', 'data-fetch-end');

The measure entry appears in the Performance panel as a named bar spanning the measured duration. This is how you identify which part of your own code is responsible for a slow operation, even when the flame chart is crowded with library calls.

💡 Practical Tips

💡 Use Bottom-Up First Don't try to read the entire flame chart. Open the Details panel → Bottom-Up tab → sort by Self Time descending. The top entry is the most expensive function. Click it to jump directly to it in the flame chart.
💡 Always Profile with CPU Throttling Set CPU throttling to 4× slowdown when profiling for mobile users. Your dev machine is 5–10× faster than a mid-range Android phone. A function that costs 5ms on your machine may cost 25ms on a user's device — the difference between smooth and janky.
💡 Record in a Private Window Browser extensions run on your main thread and add noise to the profile. Open a Private Window (where extensions are usually disabled) for the cleanest, most representative recording.
ℹ️ Self Time vs Total Time Total time includes all nested function calls. Self time is only what the function itself did. A function with high total time but low self time is just the entry point — the real cost is in what it calls. A function with high self time is the bottleneck.

✏️ Exercises

  • Record a page load. Open the Performance panel (Ctrl + Shift + P). Click the 🔄 Reload button to capture a full load. Find the FCP marker — how many milliseconds did the first content take to appear?
  • Find a long task. Look in the flame chart for bars with a red triangle in the top-right corner. Click one. Read the function name and duration in the Details panel.
  • Use Bottom-Up. Click the Bottom-Up tab in the Details panel. Sort by Self Time. What is the most expensive function? Is it your code or a library?
  • Zoom into a drop. Drag across a red section of the FPS bar in the overview strip to zoom in. What task was running when the frame dropped?
  • Try CPU throttling. Set throttling to 4× slowdown. Record the same page load again. How much later does FCP appear compared to no throttling?
  • Add performance marks. Run this in the Console, then record a profile: performance.mark('loop-start'); for (let i = 0; i < 5000000; i++) {} performance.mark('loop-end'); performance.measure('my-loop', 'loop-start', 'loop-end'); Can you find the my-loop measure in the flame chart?
  • Identify an animation. Find a page with a CSS animation (a spinner, a slide-in, a hover transition). Record while triggering it. Is the animated bar yellow (JS), purple (layout), or green (paint/composite)? Purple means layout is being triggered — consider switching to transform.

📌 Key Takeaways

  • The render pipeline is JS → Style → Layout → Paint → Composite, and every frame must complete in 16.7ms for 60 FPS.
  • Long tasks (>50ms) block the main thread — they prevent clicks, animations, and all other work from running.
  • The flame chart shows what ran and for how long — width = time, the red triangle = long task.
  • Bottom-Up sorted by Self Time is the fastest way to find the most expensive function.
  • Layout thrashing (reading layout after writing styles in a loop) creates dozens of forced reflows — read once, write in bulk.
  • transform and opacity are GPU-accelerated and skip layout and paint — always use these for animations instead of width, top, or left.
  • CPU throttling (4×) is essential for realistic mobile profiling — your machine is far faster than users' devices.
  • Performance marks let you annotate the timeline to measure specific operations in your own code.
Up next
Lesson 8 — Scenario: Inspecting Authentication — applying DevTools to determine what auth mechanism a site uses, read its tokens, and trace the full login-to-API flow