Notes /

The slowest thing on my homepage was my name

The page is small, the images are lazy and the fonts are preloaded. A fifth of visits still waited too long for the main content, because I had told the browser to wait.

01

What the numbers said

This site has no framework and no build step: a few HTML pages, one stylesheet and a small script. I expected the performance numbers to be boring. Then Cloudflare Web Analytics, which collects timings from real visits, showed this for the first week:

Largest Contentful Paint, real visits over 7 days
Good under 2.5 s79%
Poor over 4 s21%
Median584 ms
75th percentile2,412 ms

Largest Contentful Paint (LCP) is the moment the biggest piece of text or image in the first screen gets painted. A median of 584 ms is fine. A 75th percentile just under the 2.5 s line, with a fifth of visits past 4 s, is not.

The sample was small, about sixty visits, so a couple of slow phones move it a lot. But the pattern was worth a look.

02

Finding the element

Lighthouse, on its simulated slow phone, put LCP at 2.3 s and named the element:

<span style="--i:0">

That's the “H” in my name. It also splits LCP into phases, and the split was lopsided:

Where the 2.3 seconds went
Time to first byte641 ms
Resource load delay0 ms
Resource load time0 ms
Render delay everything after the page arrived1,638 ms

Nearly three quarters of the time came after the page had already arrived. The browser had everything it needed and still didn't paint the letter.

03

Why the letter waited

My name on the homepage is set like a type specimen. Each letter sits below its line, clipped by overflow: hidden, and rises into place one after another:

.name-line { overflow: hidden; }
.name-line > span {
  transform: translateY(115%);
  animation: rise .9s forwards;
  animation-delay: calc(350ms + var(--i) * 55ms);
}

A letter pushed fully below a clipping box isn't painted at all, so as far as LCP is concerned it doesn't exist until it starts to rise. The first letter waited 350 ms after the first render before it moved, and the last finished almost two seconds in. Below the name, the status line and the “Shipped” list started at opacity: 0 and only faded in after 1.15 s.

The usual suspects were innocent. In the network waterfall, the HTML, stylesheet, script and all three fonts finished downloading within about 0.3 s. The screenshots below the fold are lazy-loaded and never competed. The delay was a number I had typed into the stylesheet myself.

04

Keep the animation, make it cheaper

Removing the animation would have fixed the number, but the animation is most of the page's character. So I made two smaller changes.

Start sooner, finish sooner

animation: rise .8s forwards;
animation-delay: calc(60ms + var(--i) * 40ms);

The first letter now starts moving almost immediately, and the text below fades in at 0.5 s instead of 1.15 s.

Play it once per visit

Coming back to the homepage from a case study replayed the whole entrance every time, which nobody needs twice. A class set from sessionStorage turns it off after the first view:

try {
  if (sessionStorage.getItem('intro')) d.classList.add('seen');
  else sessionStorage.setItem('intro', '1');
} catch (e) {}
.seen .name-line > span { animation: none; transform: none; }

This has to run before the first paint. If it ran in the deferred script, the letters would already be hidden and rising by the time the class arrived. So it went into the small inline script in the <head> that already applies the saved colour theme.

05

The catch: a hashed script

The site's Content Security Policy doesn't allow inline scripts in general. It allows exactly one, identified by the SHA-256 hash of its text:

script-src 'self' 'sha256-…'

Change one character of that script, a space included, and the browser blocks it. The theme toggle and the new intro switch would both quietly stop working. So the change meant three things: edit the script identically on all seven pages, compute the new hash, and swap it into the headers file in the same deploy.

The same rule caught me again an hour later, when I put the site into git. On Windows, git converts line endings to CRLF on checkout by default. That changes the bytes of the inline script, which changes its hash, and that breaks the page. One line in .gitattributes pins the files to LF:

* text=auto eol=lf

06

What changed

In the lab, LCP went from 2.3 s to between 2.0 and 2.26 s across two runs. That's honest but not dramatic. The rest is first paint itself, about 1.6 s on Lighthouse's simulated slow connection: the server round trip plus the stylesheet that has to arrive before anything is drawn. Return visits in the same session skip the animation entirely. The real-visit numbers need another week of data before they mean anything.

What I'm keeping from this is simple. LCP measures when something is painted, not when it is downloaded, so an entrance animation is a render delay you chose. That's sometimes worth paying for, but it should be a decision, and the delay should be as short as the design allows.

Get in touch

I'm looking for an internship for the winter semester 2027/28. Email is the quickest way to reach me.

hsynrsdd@gmail.com