PLAINFINCH learn by building

Project: a quick bird club page

You will fix a bird club page using the CSS, font and script lessons so it paints quickly and answers taps fast.

What is wrong with the bird club page?

The page has one of each problem from the CSS, fonts and scripts lessons, which is what real pages look like after a few years of additions. Open the Waterfall first, because three of the four problems show up there before you read a line of code.

The first paint happens at about 3 seconds. Three rows carry blocking tags: style.css, old-pages.css and walks-map.js. The photo paints just after, at about 3.1 seconds, and the Vitals view shows INP of about 396 ms for the Show this week only button. The page downloads about 470 KB in total.

Make a list before you change anything. For each problem, write down the row or number that shows it and the lesson it belongs to. That habit keeps you from fixing one thing and breaking another, and it gives you a before-and-after record to check against.

Why does an `@import` hurt so much?

The first line of style.css is @import url(/css/old-pages.css);. The preload scanner cannot see inside a stylesheet, so the browser only learns about old-pages.css once style.css has downloaded. Then it starts a second request, and the first paint waits for both, one after the other.

The imported file is also 120 KB and styles a 2019 newsletter and an old gallery that no longer exist. Nothing on this page uses a single rule from it. Delete the line. If the styles were needed, the right fix would be to copy them into style.css or link the file with its own <link> in the HTML, where the browser finds it at once.

/* Before */
@import url(/css/old-pages.css);

/* After: the line is gone, and nothing on the page changed */

Why are the headings missing at first?

The headings use Lora, a web font, with font-display: block. That tells the browser to keep text invisible for up to 3 seconds while the font downloads. On this page the font cannot even be found until the stylesheet has arrived and the heading has been laid out, so the heading space stays blank for a long time.

Change it to swap. The heading shows at once in Georgia, the fallback named in the font-family list, and changes to Lora when the file arrives. Georgia and Lora are close enough in size that the switch barely moves the text. optional is another good choice for a decorative font: it shows the fallback and uses Lora only if it arrives within about 100 ms, so slow visits never see a switch at all.

@font-face {
  font-family: "Lora";
  src: url(/fonts/lora.woff2) format("woff2");
  font-display: swap;
}

Should you defer the map script or remove it?

walks-map.js is a plain <script src> in the head. The parser stops when it reaches it, waits for 210 KB to download, runs it for 150 ms, and only then reads the rest of the page. That alone accounts for most of the 3 second blank screen.

Adding defer would let the page show first. Here there is a better answer: the home page has no map. The script was copied from the walks page template. Removing it saves the download and the 150 ms of main-thread work. Always ask whether a script is needed on this page before you decide how to load it.

In the Break It page the opposite is true. The photo gallery script is needed later on the page, so you keep it and add defer, which runs it after the HTML is read without blocking anything.

Why does the button take 400 ms to answer?

The click handler in app.js filters 2,000 sightings, which work(380) stands for, and only then changes the button text. INP counts all 380 ms plus 16 ms to draw, so the tap scores about 396 ms, close to the poor line of 500 ms.

Use the pattern from the long tasks lesson. Make the handler async, change the button text first, then await scheduler.yield(). The browser paints the new text straight away. After that, filter in pieces of under 50 ms, yielding between them, so the page stays responsive while the list updates.

button.addEventListener('click', async function () {
  button.textContent = 'Showing this week';
  await scheduler.yield();
  for (let i = 0; i < 10; i++) {
    work(38);
    await scheduler.yield();
  }
});

What should the numbers look like when you finish?

With all four fixes, the first paint arrives at about 1 second, the photo at about 1.7 seconds, the tap answers in about 16 ms and the page weighs about 137 KB. The only blocking row left is style.css, which is small and needed to draw the page correctly.

The checks allow some room: first paint under 1.5 seconds, photo under 2 seconds, tap under 200 ms. If you chose defer for the map script instead of removing it, the first paint check still passes, but compare the totals. The deferred version still downloads 210 KB, which pushes the photo to about 2.1 seconds and fails the photo check, and still runs a 150 ms long task after the page shows.

Try each fix alone and watch which number it moves. Removing the @import and the map script moves the first paint. The font change moves when the headings show in the Filmstrip. The handler change moves INP and nothing else. Knowing which change moves which number is the main skill this project practises.

FigureFour bird club fixes linked to the numbers they move. Deleting the import and map script moves first paint from 3.04 to 0.97 s and the photo from 3.07 to 1.66 s; swap and yielding fix headings and INP.Four bird club fixes linked to the numbers they move. Deleting the import and map script moves first paint from 3.04 to 0.97 s and the photo from 3.07 to 1.66 s; swap and yielding fix headings and INP.
Each fix moves its own number. The two deletions share the first paint and the photo, while the font and the handler each move one thing.

How would you check a page like this in real tools?

In Chrome DevTools, the Network panel shows each request's initiator, which is where you spot that old-pages.css was started by style.css. Lighthouse flags render-blocking resources and lists each with its potential saving. The Performance panel shows the parser pausing at the map script and the long task when you click the button.

Real browsers start the font request a little earlier than the simulator in some cases, and their main thread also spends time on style and layout. The order of problems and the size of the wins are the same. PageSpeed Insights adds field data from the Chrome UX Report, which tells you whether real visitors on real phones feel the slow tap.

What would an AI assistant probably miss here?

Asked to speed this page up, an AI assistant is likely to add defer to every script and font-display: swap to the font. Both help. It is less likely to notice that the map script does not belong on this page at all, or that the @import loads styles for pages that were deleted years ago. Those findings need someone to look at what the page actually shows.

It may also suggest preloading old-pages.css to speed up the import chain. That makes the useless file arrive sooner instead of removing it. And for the slow button, AI fixes often wrap the filter in setTimeout, which makes the first paint after the tap happen but leaves a 380 ms long task blocking the next one. Check each suggestion against the Waterfall and the Vitals view before you keep it.

Field notes

  • Open the waterfall first; most problems show up there before you read code.
  • Replace an @import with a direct link so the browser finds the file early.
  • Change font-display: block to swap so headings show at once.
  • Show the button change first, then yield before the slow filtering.

Easy to get wrong

Preloading makes an unused file arrive sooner. It still costs 120 KB and still blocks the first paint. Remove what the page does not need.

defer stops the script blocking the first paint, but every home page visit still downloads and runs it for nothing.

block hides text for up to 3 seconds while the font downloads. swap shows it at once in the fallback font.

Questions people ask

In what order should I fix things on a slow page?

Start with whatever blocks the first paint, because nothing else helps while the screen is blank. Then the largest image or text block, then layout shift, then slow taps. Re-check the numbers after each change.

Why does removing scripts also make the photo faster?

Every download shares one connection. The 120 KB stylesheet and the 210 KB script took bandwidth the photo could have used, and the blocked first paint delayed when the photo could appear.

Is one blocking stylesheet acceptable?

Yes. A small stylesheet that the page needs to look right should block the first paint, otherwise visitors see unstyled content jump into shape. Keep it small and linked directly from the HTML.

Could I inline `style.css` instead?

For a small page, inlining the critical rules in a <style> block removes one more round trip. The critical CSS lesson covers when that is worth doing and when it hurts caching.

Keep going