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, 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
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
🔴 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.
async/deferJSON.parse() is blocking and cannot be interrupted.async; consider a facade pattern to defer until interactionLayout 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 Property | Triggers | Cost | Use 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 |
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
| Marker | Full Name | What 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
✏️ 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 themy-loopmeasure 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.
transformandopacityare GPU-accelerated and skip layout and paint — always use these for animations instead ofwidth,top, orleft.- 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.