Static HTML first: why server-rendered pages still win
Run this against your own site:
curl -s https://your-site.example/ | grep -o "<h1" | wc -l
If the answer is zero, the page's main heading is built by JavaScript after load. Everything the page says is missing from the first response that crawlers, link previews, accessibility tools and slow connections receive. Look at what a server actually sends, not at what a browser eventually shows.
Four ways to produce a page
The difference between these approaches is where and when the HTML is made.
| Approach | HTML is built | First response contains content? | Typical use |
|---|---|---|---|
| Static (SSG) | At build time, once | Yes, complete, from a CDN | Content sites, docs, marketing, portfolios |
| Server-rendered (SSR) | On the server, per request | Yes, complete, but computed per request | Personalised or frequently changing pages |
| Client-rendered (SPA) | In the browser, by JavaScript | Usually an empty shell and a script tag | Logged-in apps and dashboards |
| Islands | Static HTML, plus small scripts for specific components | Yes, complete | Mostly static pages with a few interactive parts |
"Islands" is a community name for the last pattern, not a standard, and web.dev's guide does not use it. The guide does describe the spectrum around it and the costs of each end. Static rendering happens at build time, gives a consistently fast time to first byte because nothing is generated per request, and can sit on CDNs. Server rendering can produce a fast first paint, but generating pages takes time on the server, which can raise TTFB. Client rendering moves the work onto the visitor's device, and web.dev notes that the JavaScript required tends to grow as an application grows, which can hurt INP.
What each visitor receives
Different readers get different things from the same URL:
- A person on a fast laptop receives the HTML and the scripts and sees the finished page in about the same time either way.
- A person on a mid-range phone and a weak connection with client rendering waits for the script to download, parse and run before seeing anything useful. With static HTML they read text while scripts are still loading.
- A search crawler gets the initial HTML first. Google's JavaScript SEO documentation describes three phases for JavaScript pages: crawling, rendering and indexing. It says Googlebot queues every page with a 200 status for rendering, whether or not it uses JavaScript, that a page may wait on that queue for a few seconds or longer, and that rendering might be skipped for non-200 responses. Rendering can also fail for various reasons, so a page whose content is in the HTML does not depend on it.
- A link preview, a messaging app or another bot may read only the initial HTML to find the title, description and image. Google's own documentation says that not all bots can run JavaScript, so assume a given fetcher does not.
- A screen reader or text-mode browser gets the structure the page ships with: headings, landmarks and links that are in the HTML are available at once.
None of this says client rendering never works. The initial HTML is simply the one thing every reader receives, so it should contain the content.
What Google's own guidance says now
Older explanations describe a "two waves" model. Google's current JavaScript SEO page frames it differently: a queue for crawling and a queue for rendering, a headless Chromium that renders when resources allow, and a recommendation that server-side or pre-rendering is "still a great idea" because it makes sites faster for users and crawlers. Its page on dynamic rendering, the practice of serving bots a different version, calls it a workaround and not a long-term solution, and recommends server-side rendering, static rendering or hydration instead.
Three more details from the same documentation shape how you build:
- Do not rely on a script to undo noindex. If a page carries a
noindexrobots meta tag, Google may skip rendering and JavaScript execution, so JavaScript that later removes the tag may not work as expected. - Fingerprint your assets. Google says its rendering service caches aggressively and may ignore cache headers, which can leave it using outdated JavaScript or CSS. Putting a content hash in the filename avoids that, and it also lets browsers cache the file for a long time.
- Do not expect interaction. Google's guidance on lazy loading notes that Google Search does not scroll or click, so content that appears only after those events may never be seen.
Time to content, and what hydration costs
Two of the Core Web Vitals reward getting content on screen quickly and staying responsive. Largest Contentful Paint measures when the biggest element appears; a good value is 2.5 seconds or less. Interaction to Next Paint measures responsiveness; a good value is 200 milliseconds or less. Core Web Vitals in plain language covers the thresholds in full.
With static HTML, the largest element is in the first response, so LCP depends on how fast the HTML and its critical image or font arrive. With client rendering, LCP cannot start until the script has downloaded, parsed and executed, and fetched whatever data it needs.
Hydration adds a second cost. web.dev defines it as running client-side scripts to attach state and interactivity to server-rendered HTML, and describes the main drawback of server rendering with rehydration as a negative effect on Total Blocking Time and INP: the page can look loaded and interactive but not respond to input until the component scripts have run and attached their handlers. Most server-rendering setups also serialise the data behind the page into the document, so the same content travels twice, once as markup and once as data. web.dev describes it as shipping the app twice, and says real-site measurements suggest rehydration is rarely the best option. Progressive and partial rehydration are named as possible improvements, with partial rehydration described as difficult to implement. The practical reading is to hydrate less, not to avoid frameworks.
When an island is enough
Most pages are text, images and links, with one or two interactive parts. An island treats it that way:
<main id="content">
<h1>Spring timetable</h1>
<p>Departures from the north pier run every hour from 8:00 to 20:00.</p>
<!-- the one interactive part -->
<div data-island="timetable-search">
<form action="/timetable/" method="get">
<label>From <input name="from"></label>
<button>Search</button>
</form>
</div>
</main>
<script type="module" src="/assets/island.js"></script>
Three rules make this work:
- The page is complete without the script. All text, headings, images and links are in the HTML.
- The control works without the script too, as a real form or link. The script improves it: suggestions, instant results, no reload.
- The script is small and does not block rendering. MDN says module scripts defer by default, and that a script without
type="module",asyncordeferblocks parsing.
This is the idea MDN calls progressive enhancement: a baseline of essential content and function for as many users as possible, with a richer experience for browsers that can run more. web.dev gives a test for whether a site really does this: disable JavaScript and load the page. For a statically rendered page most interactive features still exist, while a prerendered single-page app is mostly inert. This site is built that way: every route is a complete HTML file, and one script adds the canvases, transitions and menu on top. The page-transition layer is described in how a page transition works without a framework, and the cost of the heavy part in a performance budget for a site with WebGL.
Verify it yourself
Use the command line before trusting a tool.
- Fetch the page as a bot would.
curl -s https://your-site.example/page/ | head -c 3000. The title, meta description and canonical link live in the head, so they should appear near the top of the response, followed by the start of the content. - Count the key elements.
curl -s URL | grep -o "<h1" | wc -lshould print 1 (counting matches, not lines, so minified one-line HTML is counted correctly). Check for<main,<article, and the paragraph text you care about withgrep -o "a phrase from the page". - Look at the response.
curl -sI URLshows the status code (200 for real pages, 404 for missing ones, not 200 with a "not found" message),content-type, and caching headers. - Compare with the browser. Open View Source and compare it with the Elements panel. Text in the Elements panel but missing in View Source is added by JavaScript.
- Turn JavaScript off. In Chrome DevTools open the Command Menu (Command or Control with Shift and P), run "Disable JavaScript", then reload. What you see is the page without its enhancements. Navigation, text and images should still be there.
- Ask Google. The URL Inspection tool in Search Console, and the Rich Results Test, can show the rendered HTML and loaded resources. Compare it with your own fetch.
- Check metadata. Title, meta description, canonical and robots meta should be in the HTML. Google says you can set a canonical with JavaScript but the best way is HTML, and that JavaScript must not change a value already in the HTML. See titles, descriptions and snippets.
What a bad result looks like (fictional)
A fictional single-page app answers step 1 with a document that is mostly this:
<head><title>Studio</title></head>
<body><div id="root"></div><script src="/assets/app.4f9c1b.js"></script></body>
Step 2 prints 0, and View Source has no paragraph text. The Elements panel shows a full page. Step 5 shows a blank screen. Together those say that every word of the page is created by app.4f9c1b.js, so a preview bot, or a crawler whose rendering fails, sees an empty document. The fix is not to abandon the app but to move the entry pages into static HTML and let the script take over after.
Where client rendering goes wrong for search
Watch for these patterns, all covered in Google's JavaScript SEO, link and lazy-loading documentation:
- Links that are not links. A
<span onclick>or a button that changes the route is not a crawlable link. Google says it can generally only crawl<a>elements with anhref. Use<a href="/real/path/">. - Routes in the fragment. URLs like
/#/pageare not reliably resolved. Google recommends the History API to give each view a real path. - Status codes that lie. A single-page app often returns 200 for every path. Google says this can lead to error pages being indexed. A missing page should return 404 from the server, or the app should redirect to a URL that does, or add a
noindexrobots meta tag to error views. - Content behind interaction. Text that appears only after a click or scroll event may never be rendered, since Google Search does not interact with your page.
- Metadata injected late, especially canonical URLs and robots directives, which Google recommends setting in the HTML.
The operational side
A static site has little to go wrong at runtime. There is no server process to restart and no per-request computation, and files sit on a CDN close to the visitor. A bad deploy is undone by pointing at the previous build, because every build is a complete set of files. Fingerprinted assets can be cached for a long time while the HTML is revalidated, so a change goes live without stale pages.
When to choose something else
Static HTML is the default, not a religion. Choose server rendering when the page depends on the request (personalised content, rapidly changing data). web.dev notes that static rendering must produce a file for every URL, which can be infeasible for a very large number of unique pages. Choose client rendering for the application part of a product: dashboards, editors, tools behind a login. The public pages in front of the app (home, pricing, documentation) are still usually better as static HTML, because people search for and share them.
Checklist
curlyour page: the main heading, body text, title, description and canonical are in the first response.- Every internal link is a real
<a href>to a real path, and missing pages return 404. - Interactive parts are islands over working HTML, with small module scripts.
- With JavaScript disabled, a visitor can still read and navigate.
- Re-run the checks after every framework or build change.
Questions and answers
Can Google index a client-rendered site?
Google documents that it renders pages with an evergreen version of Chromium, so it can index content produced by JavaScript. Rendering is a separate step that happens after crawling, can wait in a queue, and can fail. Google also says not all bots can run JavaScript, so content in the HTML avoids depending on any of that.
Is static HTML only for blogs?
No. Any page whose content is the same for every visitor can be built ahead of time, including marketing sites, documentation, portfolios and product pages. Interactive parts can be added as small scripts on top.
What is hydration?
web.dev defines hydration as running client-side scripts to add application state and interactivity to server-rendered HTML. It needs the component code in the browser as well, which is where its cost comes from.
When is a single-page app the right choice?
When the product is an application behind a login, is highly interactive, and does not depend on being found or previewed from its inner pages. Even then, the entry pages are often better as static HTML.
Sources
- Google Search Central: JavaScript SEO basics developers.google.com
- web.dev: Rendering on the Web web.dev
- web.dev: Interaction to Next Paint (INP) web.dev
- Google Search Central: Link best practices developers.google.com
- MDN: Progressive enhancement developer.mozilla.org