Every page shipped the blog to put a title in the tab by niteeshkanna-sh · Pull Request #153 · niteeshkanna-sh/create-react-app · GitHub
Skip to content

Every page shipped the blog to put a title in the tab - #153

Merged
niteeshkanna-sh merged 1 commit into
mainfrom
claude/stoic-rubin-fnglq7
Oct 13, 2026
Merged

niteeshkanna-sh merged 1 commit into
mainfrom
claude/stoic-rubin-fnglq7

Conversation

@niteeshkanna-sh

Copy link
Copy Markdown
Owner

useSeo() keeps the tab title, description and canonical in step after a client-side navigation, because navigation never fetches HTML again. To know what each route is called it imported towns.json, models.json, service-areas.json and posts.json and composed the list in the browser.

All four landed in the entry chunk. So every page of the site carried every blog post's body, every car's luggage capacity and the prose for twelve town pages — about 70KB of object literal, run at module top level before anything on screen could answer a tap — to end up with a path, a title and a sentence.

The list is composed at build time now, by scripts/seo-routes.mjs, from exactly the same four files and the same four helpers. prerender-seo.mjs reads what it writes instead of composing its own copy, so the titles written into each page's HTML and the titles useSeo puts in the tab are one list and cannot drift — which is what the old comment in useSeo was reaching for when it said a second list is a second list to forget.

Entry chunk: 374,166 → 314,667 bytes (−59.5 KB, −16%). Every page pays that, not just the home page.

What it did for blocking time

Phone at CPU 4× and 1.6 Mbps, median of eleven runs per arm, two builds measured the same way:

TBT longest task long tasks
before 278ms 152ms 6
after 256ms 144ms 5

Honest about the size of that: the runs scatter 208–388 before and 153–351 after, so a 22ms median is a real direction and not a precise figure. The 59.5 KB is the certain part.

Verification

Across all 44 prerendered pages the only lines that changed are the hashed asset filenames — not one title or description differs. A browser check covers the other half: clicking Tariff, Blog and Contact retitles the tab and moves the canonical, which is the whole job of the file this touches. All 43 routes crawl clean at desktop and phone; LCP 1720 and CLS 0.000, unchanged.

Two wrong turns, measured and reverted rather than shipped

Both came from misreading a profile, and both are worth recording:

  • content-visibility on the offscreen sections. 83% of the home page is below the first screen, so this looked obvious. It cut the longest layout pass from 73ms to 41ms and did nothing for blocking time, because the cost was never layout. Reverted.
  • The scene drawings. Stripping all 176 SVG nodes changed TBT by less than the run-to-run noise. The page is not slow because of what it draws.

The thing that found the real cause was a control experiment: with no script at all, TBT is 65ms. That put ~150ms squarely on JavaScript and stopped the search going further down the layout path.

Two profiler readings taken along the way were wrong and are corrected here for anyone reading back: a first run attributed idle gaps to whichever function followed them, inflating one react-router function to 152ms when it is 0.6ms; and a trace's EvaluateScript self-time read as 4ms because it excluded the nested v8.evaluateModule, which was in fact 176ms of the 219ms longest task.

🤖 Generated with Claude Code

https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf


Generated by Claude Code

useSeo() keeps the tab title, description and canonical in step after a
client-side navigation, because navigation never fetches HTML again. To
know what each route is called it imported towns.json, models.json,
service-areas.json and posts.json and composed the list in the browser.

All four landed in the entry chunk. So every page of the site carried
every blog post's body, every car's luggage capacity and the prose for
twelve town pages -- about seventy kilobytes of object literal, run at
module top level before anything on screen could answer a tap -- to end
up with a path, a title and a sentence.

The list is composed at build time now, by scripts/seo-routes.mjs, from
exactly the same four files and the same four helpers. prerender-seo.mjs
reads what it writes instead of composing its own copy, so the titles
written into each page's HTML and the titles useSeo puts in the tab are
one list and cannot drift -- which is what the old comment in useSeo was
reaching for when it said a second list is a second list to forget.

  entry chunk   374,166 -> 314,667 bytes   (-59.5 KB, -16%)

Every page pays that, not just the home page.

What it did for blocking time, phone at CPU 4x and 1.6 Mbps, median of
eleven runs per arm, two builds measured the same way:

                   TBT    longest task   long tasks
  before           278ms      152ms           6
  after            256ms      144ms           5

Honest about the size of that: the runs scatter 208-388 before and
153-351 after, so a 22ms median is a real direction and not a precise
figure. The 59.5 KB is the certain part.

Verified the output did not move: across all 44 prerendered pages the
only lines that changed are the hashed asset filenames. Not one title or
description differs. A browser check confirms the other half -- clicking
Tariff, Blog and Contact retitles the tab and moves the canonical, which
is the whole job of the file this touched.

Two wrong turns on the way, both measured and both reverted rather than
shipped. content-visibility on the offscreen sections, which cut the
longest layout pass from 73ms to 41ms and did nothing for blocking time,
because the cost was never layout. And the scene drawings: stripping all
176 SVG nodes changed TBT by less than the noise. The page is not slow
because of what it draws. It was slow because of what it carried.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
@niteeshkanna-sh
niteeshkanna-sh merged commit 08ba44e into main Oct 13, 2026
@niteeshkanna-sh
niteeshkanna-sh deleted the claude/stoic-rubin-fnglq7 branch October 13, 2026 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants