Lab — Profile with DevTools
Use Chrome DevTools to measure the rendering pipeline of a deliberately slow page, identify the bottlenecks, and apply targeted fixes.
- Use the Network tab to identify render-blocking resources and measure waterfall timing
- Read a Performance flame chart to locate long layout and paint tasks
- Apply defer, preload, lazy loading, and font-display to a slow page
- Compare before-and-after waterfalls to quantify the impact of each fix
Reading about the critical rendering path is one thing. Seeing the browser stall on a render-blocking stylesheet, then watching that stall disappear after a targeted fix, makes the concept permanent. This lab gives you that experience.
You will work with a deliberately underoptimised HTML page that exhibits four common problems, profile it in DevTools, apply fixes one at a time, and re-profile after each change.
This lab is hands-on in the browser. The exercises below are structured as investigation questions and self-review checklists. Open Chrome (or a Chromium-based browser) and follow along.
The slow page
Create a file called slow.html with the following content. Save it locally
and open it in Chrome via File → Open File or a local server
(python3 -m http.server).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Slow Page</title>
<!--
Problem 1: Render-blocking stylesheet with no preload.
The browser must download and parse this before painting anything.
-->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=block">
<!--
Problem 2: Synchronous script in <head> — blocks HTML parsing entirely.
-->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
</head>
<body>
<h1 style="font-family: Roboto, sans-serif;">Welcome</h1>
<p>This page has several rendering bottlenecks.</p>
<!--
Problem 3: Large above-the-fold image with no preload.
Discovered late in parsing — slows LCP significantly.
-->
<img src="https://picsum.photos/1200/600" alt="Hero image"
style="max-width: 100%; height: auto;">
<!--
Problem 4: Below-fold images loaded eagerly.
-->
<img src="https://picsum.photos/600/400?random=1" alt="Gallery image 1">
<img src="https://picsum.photos/600/400?random=2" alt="Gallery image 2">
<img src="https://picsum.photos/600/400?random=3" alt="Gallery image 3">
</body>
</html>Part 1 — Network tab: find what blocks FCP
Open DevTools (F12 or Cmd+Opt+I), go to the Network tab, and:
- Set throttling to Slow 3G using the dropdown (top toolbar of the Network tab, next to "No throttling").
- Check Disable cache so every run starts clean.
- Reload the page (
Cmd+Shift+R/Ctrl+Shift+Rto bypass the cache in the browser itself too). - Wait for the load to finish, then examine the waterfall.
Questions to answer:
- Which resources have a solid coloured bar that extends before the HTML finishes parsing? Those are render-blocking candidates.
- Find the thin vertical line labelled FCP in the waterfall. What is the timestamp? Which resources must finish before that line appears?
- Which resource request starts latest relative to the HTML response? Why does it start late?
In the Network waterfall, a blue vertical line marks DOMContentLoaded; a red line marks the load event. The coloured dots on the timeline axis mark FCP and LCP. Hover over any bar for request timing breakdown: Queueing, Stalled, DNS, Connect, SSL, TTFB, Download.
Do it yourself — document your baseline:
- Record the FCP timestamp (in ms).
- Record the LCP timestamp (in ms).
- List every resource that finishes after FCP. Those are not on the critical path but may be delaying LCP.
Part 2 — Performance tab: read the flame chart
Switch to the Performance tab. With Slow 3G still active and Disable cache still checked:
- Click the circular Record button (top-left).
- Reload the page.
- Wait for the load to complete, then click Stop.
DevTools renders a flame chart of the main thread activity.
What to look for:
- Long yellow tasks in the main thread lane — these are JavaScript execution events. Find the task that corresponds to the lodash script execution. How long does it take?
- Purple "Layout" bars — each one is a reflow. Are any unusually long?
- Green "Paint" bars — locate the first paint event. What precedes it on the timeline?
- Hover over the FCP and LCP markers in the Timings row. The gap between them is the time spent loading the hero image after first paint.
Do it yourself:
- Screenshot or note the durations of the three longest main-thread tasks.
- Identify the task that most directly delays FCP.
Part 3 — Apply fixes
Create a copy of the file called fast.html and apply the following changes:
Fix 1 — Defer the synchronous script
<!-- before -->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<!-- after -->
<script defer src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>defer downloads the script in parallel with HTML parsing and runs it after
the DOM is complete — the script no longer blocks the parser.
Fix 2 — Preload the hero image
Add this inside <head>, before the stylesheet:
<link rel="preload" href="https://picsum.photos/1200/600" as="image">The browser now fetches the hero image at high priority alongside the HTML,
rather than discovering it only when the parser reaches the <img> tag in
<body>.
Fix 3 — Preconnect to the font origin and add font-display
Replace the Google Fonts <link> with:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap">display=swap in the Google Fonts URL sets font-display: swap on the
@font-face rules, so text is shown immediately in a system font rather than
hidden. The preconnect hints eliminate the DNS + TLS overhead on first font
request.
Fix 4 — Lazy-load below-fold images
<!-- before -->
<img src="https://picsum.photos/600/400?random=1" alt="Gallery image 1">
<!-- after -->
<img src="https://picsum.photos/600/400?random=1" alt="Gallery image 1" loading="lazy">Apply loading="lazy" to all three gallery images. The browser will defer
their download until they are about to scroll into view, freeing bandwidth for
the critical resources during load.
The completed <head> section of fast.html should look like this:
<head>
<meta charset="UTF-8">
<title>Fast Page</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" href="https://picsum.photos/1200/600" as="image">
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap">
<script defer src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
</head>Part 4 — Re-profile and compare
Open fast.html in the same Chrome window (keep Slow 3G and Disable cache
active). Run the same Network and Performance recordings you ran in Parts 1
and 2.
Compare:
| Metric | Baseline (slow.html) | Fixed (fast.html) |
|---|---|---|
| FCP (ms) | ___ | ___ |
| LCP (ms) | ___ | ___ |
| Longest main-thread task (ms) | ___ | ___ |
| Number of requests before FCP | ___ | ___ |
If the fixes worked as expected:
- FCP should drop significantly — the synchronous script no longer blocks the parser before paint.
- LCP should improve — the hero image started downloading earlier thanks to the preload hint.
- The lodash task in the flame chart should now appear after DOMContentLoaded rather than before first paint.
- The three gallery images should not appear in the waterfall at all until the user scrolls (or should start much later if they are near the fold).
Results will vary with network conditions and CDN caching. Re-run each profile two or three times and use the median to avoid one-off outliers. In a controlled benchmark, run against a local server with traffic shaping to get stable numbers.
Self-review checklist
Before moving on, verify you can answer each of these:
- I can explain why the synchronous
<script>in<head>blocked FCP, and whatdeferchanges. - I can read the Network waterfall and identify which resource finishes last before FCP.
- I can find a long Layout task in the Performance flame chart and understand what caused it.
- I know the difference between
preload(current page, high priority) andprefetch(next page, low priority). - I know what
font-display: swapdoes and why it improves perceived performance even before the font loads. - I can explain the trade-off of
loading="lazy"— what it saves and when not to use it (never on above-the-fold images).
Never add loading="lazy" to your hero image or any image that is visible
without scrolling. Lazy-loading the LCP image is one of the most common
performance mistakes — it defers the most important image on the page.
When the tools are unavailable
If you need to diagnose a rendering bottleneck without access to DevTools (server-side metrics, CI pipelines, or a remote session):
# Lighthouse CLI — measures CWV and produces an HTML report
npx lighthouse https://example.com --output html --output-path ./report.html
# WebPageTest via API (free tier available)
curl "https://www.webpagetest.org/runtest.php?url=https://example.com&f=json&k=<API_KEY>"
# Chrome's built-in performance timing in the console
# (run in the browser after the page loads)
# performance.getEntriesByType('navigation')[0]
# performance.getEntriesByType('paint')Lighthouse integrates into Chrome DevTools under the Lighthouse tab and can be run headlessly in CI to enforce performance budgets.
Loading Strategies
The browser loads resources in the order it encounters them — but defer, async, preload, and prefetch let you change that order to serve users faster.
CSS Custom Properties
CSS custom properties (variables) inherit down the document tree and can be overridden at any scope — which makes them a native theming primitive, not just a convenience.