This portfolio is full of animation, sound and effects, exactly the kind of site that usually does poorly on performance. So before having opinions, I measured. This post is the log of that optimization: which tools I used, what each one found, what I changed and how much it improved.
At the end there's a checklist you can apply to any project. I'm going to use it on my other apps too.
Before and after
PageSpeed Insights measurements of the home page, on mobile. "Before" is the first deploy (Oct 15, 2026), "after" is the custom domain the next day, with the fixes in this post.
| Metric | Before | After |
|---|---|---|
| Performance | 82 | 84 |
| Accessibility | 92 | 96 → 100* |
| Best practices | 96 | 100 |
| SEO | 100 | 100 |
| Total Blocking Time | 80 ms | 50 ms |
| Speed Index | 7.8 s | 6.4 s |
| Largest Contentful Paint | 3.6 s | 3.5 s |
* The 96 had a contrast issue that I fixed after that measurement; on local Lighthouse, with the fix, accessibility reaches 100.
The tools
| Tool | What it measures | Login required? |
|---|---|---|
| Lighthouse | Loading, accessibility, best practices and SEO, running on your machine | No |
| PageSpeed Insights | The same Lighthouse, running on Google's servers, plus real-user data once the site has traffic | No |
| Chrome DevTools | Smoothness while in use: FPS, slow frames, long tasks | No |
| Schema Markup Validator | Whether the structured data (JSON-LD) is correct | No |
| Rich Results Test | Whether Google can build rich results from that data | Yes, a Google account |
| Google Search Console | Indexing, the searches that bring people in, crawl errors | Yes, plus domain verification |
A rule I learned: run Lighthouse more than once. On desktop, three runs in a row gave 95, 83 and 85. Use the median, not the best number.
What each tool found
1. The preloader was holding the page back
The preloader (the 0 to 100 counter) lasted 1.6 s. It covered the main heading, the largest element on screen, and LCP went over 3 seconds on mobile. I cut it to 0.4 s.
The LCP gain was small (3.6 s to 3.5 s), and that's a lesson too: the heading comes in with a reveal animation, and the browser only counts LCP once it actually shows up. The next step is to have the heading render visible from the start and animate only the details.
2. An error that only existed in production
Best practices sat at 96 because of a console error: React hydration error (#418). Post dates were formatted in the server's time zone (UTC) and the browser's (UTC−3), so a midnight date became "Aug 12" on one side and "Aug 11" on the other.
On my machine, server and browser were in the same time zone, so the error never showed up. The fix was to always format dates in UTC. Result: 100 on best practices.
3. Accessibility: what the eye doesn't see
Lighthouse flagged four issues, none of them visible:
- Prohibited
aria-label: the library that splits text for animation (SplitText) addedaria-labelto paragraphs, which is only allowed on headings and a few other elements. - Accessible name different from the visible text: the logo link said "Home" to screen readers but showed "Iago Gois".
- Contrast on a shortcut inside the debugger.
- Button contrast: the Editorial theme's orange with white text came out at 4.0:1, and the minimum is 4.5:1. I computed the contrast for every theme: three combinations were below. I darkened two tones slightly and switched the text color on the third, without changing the look of any theme.
4. The page "jumped" on desktop
On desktop, PageSpeed flagged a CLS of 0.105 (the "good" threshold is 0.1). The culprit seemed to be the screen grain effect, but it wasn't: during the preloader, scrolling is locked with no scrollbar; when it unlocks, the scrollbar appears and the page gets 15 px narrower, and everything shifts. The grain only got the blame for being the largest element.
The fix is one line of CSS:
html {
scrollbar-gutter: stable;
}It reserves the scrollbar's space from the start. The same issue failed one check in Lighthouse's new Agentic Browsing category, which measures whether a site is navigable by AI agents.
5. Smoothness: what Lighthouse doesn't measure
Lighthouse only looks at loading. On an animated site, what also matters is whether scrolling is smooth. I wrote a test that opens the site in Chrome with a phone-sized screen and a 4x slower CPU, scrolls to the end and measures every frame.
The first finding was uncomfortable: lite mode wasn't lightening anything. In the Aurora theme it was even slower than having everything on.
To understand why, I recorded performance traces in Chrome DevTools and turned effects off one at a time:
- JavaScript wasn't the problem. It was only ~8% of CPU time while scrolling, counting GSAP, React and the site's own code.
- The cost came from effects that never stop: marquees (even off-screen), animated grain, line-by-line text reveal and, in Aurora, the live-blurred background and the glass blur on cards. While anything moves, the browser redoes style and layers for the whole page on every frame.
- Lite mode was aiming at the wrong target. It turned off the cursor, magnetism and tilt, which don't run on phones anyway (they need a mouse), and swapped the shader for a blurred background that wasn't any cheaper.
What changed:
- Marquees and infinite loops pause when they leave the screen, in every mode.
- In lite mode: still grain, the animated background becomes a fixed gradient, no glass blur and text reveals per block instead of per line.
Result on the simulated phone (average FPS, median of 4 rounds):
| Theme | Scenario | Before | After |
|---|---|---|---|
| Editorial | Everything on | 59 | 68 |
| Editorial | Lite mode | 55 | 75 |
| Aurora | Everything on | 45 | 48 |
| Aurora | Lite mode | 35 | 63 |
| Both | No animation (baseline) | 113 | 113 |
In Aurora, slow frames (below 30 fps) in lite mode dropped from 23.5% to 2.2%. And lite mode is now actually lighter than everything on.
A lesson about method: interleave your runs. Running one scenario after another, the same test gave 28 fps in one batch and 57 in the next, purely from machine noise. Alternating scenarios every round kept the numbers stable, and the "no animation" row, whose code didn't change, works as a control.
Technical SEO
The basics every site should have:
- Per-page metadata: its own title and description on every page, project and post.
- Canonical and
hreflang: the site is bilingual, and each page states which is the Portuguese version and which is the English one, so Google shows the right one. - Sitemap and robots.txt, generated automatically.
- Share image (OG) per language, so links look good on LinkedIn.
A domain gotcha: www.iagogois.com.br redirects to iagogois.com.br, but for a while the sitemap pointed to www. Google prefers the final URL, with no redirect, so I pinned the main domain in code.
Structured data
These are invisible JSON-LD blocks that tell machines what a page is: a person (Person), a website (WebSite), a list of projects and each post (BlogPosting).
The Schema Markup Validator found the blocks with 0 errors and 0 warnings. The Rich Results Test runs a similar check from Google's point of view, but it requires signing in with a Google account.
SEO for AI
More and more people ask ChatGPT, Claude or Perplexity instead of searching on Google. To show up in those answers, a site needs to be easy for an AI to read:
- Server-rendered content: the HTML arrives with all the text, without depending on JavaScript.
- llms.txt: a Markdown summary at
/llms.txt, with who I am, projects and links, in the format language models read. There's also a full version at/llms-full.txt. - robots.txt allowing AI crawlers, such as GPTBot, ClaudeBot and PerplexityBot.
- Structured data: the same data that helps Google helps AIs understand who is who.
In Lighthouse's Agentic Browsing category, mobile scored 3/3.
Search Console
Google Search Console is where Google tells you what it sees of your site: which pages it indexed, which searches brought visits and what failed. To set it up:
- Go to Search Console and add a domain property (
iagogois.com.br). It coverswww,httpand every page. - Google shows a TXT record. Create that record in the domain's DNS (in my case, at Registro.br).
- Come back and click Verify. It can take a few minutes.
- Submit the sitemap:
https://iagogois.com.br/sitemap.xml.
Data starts showing up within a few days. I'll come back with the numbers once I have them.
Checklist for any site
- Run Lighthouse 3 times and use the median
- Run PageSpeed Insights on mobile and desktop
- Check the browser console in production, not just locally
- Test with the server and browser in different time zones
- Check text contrast on the accent color (minimum 4.5:1)
- Use
scrollbar-gutter: stableif the page ever locks scrolling - Measure scroll smoothness, not just loading
- Pause infinite animations when they're off-screen
- Title, description and canonical on every page
-
hreflangif the site has more than one language - Sitemap and robots.txt
- Structured data validated with the Schema Markup Validator
-
llms.txtand robots.txt allowing AI crawlers - Search Console with a domain property and the sitemap submitted
What's still missing
- Mobile LCP (3.5 s): make the main heading show up without waiting for the animation.
- Initial JavaScript on mobile: it's the biggest blocking-time cost.
- Real visitor data: the numbers above are lab data. With traffic, PageSpeed starts showing the experience of real visitors.