Partial Hydration

TL;DR / Partial hydration only attaches JavaScript to interactive components, leaving static content as plain HTML that never loads or executes framework code.

How It Works

 Page HTML:                          JS Shipped:

 ┌────────────────────┐              ┌──────────────┐
 │   Static Header    │   no JS      │  search.js   │
 └────────────────────┘      ┌───────│    (3 KB)    │
                             │       └──────────────┘
                          hydrate
 ┌────────────────────┐      │
 │   Search Widget    │←─────┘
 └────────────────────┘


 ┌────────────────────┐              Only interactive
 │    Article Body    │   no JS      parts get JS
 └────────────────────┘


 ┌────────────────────┐
 │   Static Footer    │   no JS
 └────────────────────┘

Edit diagramiKag0ex0ID0BluZwsqCO1hA9kcLjQWHuPj8-VekRi8WSqXSWRyBSKgNK9FwCFc2DMtBI0XFEuiEDgEmi0KO2G1UCcUDYgK4wJA8CQEmdRS8uAgPJAjpEmiIbQgIntIF6IFo9AkEjsISSqwAHGIRknMehcetcBkkkcODweKydppkPsuYcMKcQKZ+ZihSLrsdPFKnjKwnKPorviq-uqgeVKtVkFD6EgEC1YRH0VH+nBvDYQmtlmYmCIzPkAFpITHheYAM3QSYM23ZZc53OO1drxwbVzFqExLcez0CsveCq+yt+aoBA8C2q6iA+o0IaRjVqa5qWmgT4gLa9qOt4nqBG6HplC2Pp+jAoZQEQ0QAHT4DAAA6SAABRHC0si5AAlNOGKBG46zrKs+T4ug8z6BkEhwG4MBEEgIxuLibjFme2gHDydw3qgAh3qKNwcM+0ovB2H6fEqPyqv8xSagBOpFCBYEYCaZoWlaNoIHa0CIchrpwO6noYb69BsDheEtCwtDNFUkbRgBsgQB0ywQDSHAiAISbLBkSBJPufB8BIRBibs56SVefJoEm8lNqg1YPCpb5qfKGk9j+Okai6IhQDe2AQI4SQlhy6UYNaNbnKg2V+f0fDrDYywjBAbgdJuRJMM6CBEriuDhNIABihgpaWEkVjyJyZbchg5Q+AjKW2qlvCV3bftp-Z6QwQ4QiO9XQiAADySBcN4LQORUUAiKQcCQPRs70LitL5CwywSLicDoB0TDOHYACuNgdLIua5EkS3NatGXtfyhjoNtNxJntr7BMVXZflpfZ-udoLghWtQ3fQ0jVWQLQ+ZObA-f5925G4RLoLk1VJHYIzOCwCDhN4sgiBk+JiCjaVo7yGNGFtlwKWgIz4+2h3E5pva-rpLqAYZBpGiZ2CQeZzZwVZCFOi6qFOZKmH0MsUBBs6LS5AgNDeGz-RQPiSZEvkDIkOgsT4LEHTzDQYhgOsRzRNIRqnqlK2XlWG1YzjRgeJKL4a52n7a+VZ0upTw40-UgTjizPsxrisQMhAMAdNNZgAPoZEwtLzLkPC0ugqxJyAbIp+WafSR1hgcFnm3qwdBelSdZN61qBtFXknkIGeWM55FU1oXMqvW8LsO3VldiUC88wYOJ8geAARcG2BeOIlGWARqKQBBxHBkbXyO2suOyWbf1QOtFuiwTYrh0SIMfZLghHJgx2rSdMuCOIFwuqAIBgPh53VmAtCQGByjOkB2ieXAsx8aRjFwAtGiTJMwA8AsPAmDwEiEMsPlYsaa3pLjwf5yEWC8YXoa7Vb4clpKpNl+X0qnehw0jEqS3XEcaHnFR1Lqon6ASFQmPyKwLwyJN3oedoCxUFI4D4PgiGWTnDvG33gYbJQA7MaFYSWmGxZ7CXEs2gkY7S5AMunJBR21iATgmDWtgAXSi8hUCoHPlr7+L1sRkAkB8Cr6ALXUoHnFhijAOB4FwbSSCgOfvAQIgHhmNAVFXA27AXuwgAgWtesyIaDgEARwgOWIBVWIA1cq0dPTRDEzQpAE5LG3snV3cvVAsQAKCoEPoIqLiElPSsnIFYplbCVaq1eqNZAtBphABSHC6HAAFnANNUaENMCN6GYoNY4TQoABXEgaKbWHhiADqEAuNGJJV2YgO2jgumQJxARlMT2utlQUgcIBcbk8aCwL0CwVGXxi8SSaUy2TyRVK5TBNTCuAQXmw1loJGREslyIgcC4yNhUmwOqg7igHEqxi1ICsxLFdD8-lwEF5ICd-l0RB6EH8DpAwxAtHovREUi2MiJyIyTFyqWRMDhhRgqQuyKIrKOnIM3LOGFS-LQABZhaKHudvW8PmE5T9Ff8VUD1aCqq66h4GtgmjD6EgEF0EZHsdHRiVFCI4MkEMSoB4RAIpiIIBASFtCvlYizNGyOfpTrznlYBTI6-dxahq02ZZ9IvLfkqAargRq-_QdXqIAGjQRomtgZoWlajYgHaDpOiwLr0O6nrlAEvr-hwYZQEQyJdNStCdA0UYxmEsT4DIyRgI4Hj-I4Ai7KkHD5PgsSOKkUBBkW7LHKWPLnFcbo3IKd5io8lZSs2spvu2fzKoCaoghUf7arq5TAaBTzROB5qWrCT4wQg9rQPBiFhMh7helK6H0DAWE4QAdPgMAADpIAAFFIXQANK5AAlNOOJhEgqzVgAirgEgwAAtrsRBiGI6CJB4YaJbkXBcWeXJ8YKAnXmgsQiQ2qBaRJL6ttJCqyV-3aKZq9D-FAeXYBAbiKKePEXhKNqCQKBXEaMigsKFAi6Ik-HOI40RGvMxJwsYYD5BlHVlrylxVk83iFQ-MjPu8UnfJVn5dgpv7gmEA5DmWzQtbCID5EgxgsF0cDXRxpBwJAAWzvQ1IZBwabJBANACFFIhbM48hwvIqQsNW-S9EtJadTl61mGYW2PLEu0tuEFUfp28k-r2Z0VJCw7Qjd9AaA1ZBdIRk4cF9JEgLsDC5Lk-jOFsuACIx8g0FMHBiF5hTeCUXmI-eK3mKj6N3KJaACa8ZW4wd-Nyd-PZKSTAFqYaxoYKaOlQYKtqGXBzquuZQOoT6fr0IUUDBi6XS5AgNAsEzoyjRoJDyCUhQAGLUrELDJLgTBSGAZhwL0EsnsWUvZeg4l5U86AY-YfilXtr5qx2Gs1ad-Zk1do5BROU79fQTC7PkqxTAwDKpNWuDOGwSBcB4ZhgMijgZJLWXlleQlmKkmdPM8yu5-V-dVcdRPa66uv6vrYEgBBunQbBxmW0hK4od61lhIGwZdIHCAIHUUBe-Q1ZcEggcesybAyMizhwPIiRiEU1ZwN4g9eLlhTqjaIE8zCSmnjjNsh0Caa1qspUmg4oTXVaBXBmkYAC6u1yCoCoN9We74C7VROtgJA-goqumSNKKAXQzDlDAHM-AuBTIkBJBAPwCA4o21QFMfwxhbJ2EwXYIAA)

Full-page hydration treats every component equally: the entire JavaScript bundle downloads, parses, and executes before any part of the page becomes interactive. Partial hydration breaks this assumption by distinguishing between components that need interactivity and those that do not.

The core insight is that most web pages are predominantly static. A blog post might have thousands of words of text, a navigation bar, a footer, breadcrumbs, and metadata — none of which require JavaScript to function. The only interactive element might be a comment form or a search widget. Full hydration forces the browser to download and execute component code for every element, including the ones that will never respond to a click or keystroke.

How Partial Hydration Works

During the server render, components are annotated or categorized by whether they require client-side interactivity. Static components render to HTML and stop there — no JavaScript reference is emitted for them. Interactive components get a hydration marker in the HTML and a corresponding entry in the JavaScript bundle.

When the page loads in the browser, the framework only hydrates the marked interactive subtrees. The static HTML remains as-is: no virtual DOM is created for it, no event listeners are attached, no component instances are allocated. The framework effectively ignores the static portions of the page.

Implementation Approaches

Different frameworks implement partial hydration differently. Astro uses client:* directives — you explicitly opt components into hydration with client:load, client:idle, client:visible, or client:media. Components without a client directive ship zero JavaScript. This inverts the default: components are static unless you say otherwise.

Eleventy with is-land takes a similar approach, wrapping interactive regions in <is-land> custom elements that handle their own hydration lifecycle. The rest of the page is treated as static HTML.

In React-based frameworks, partial hydration is harder because React traditionally assumes it owns the entire DOM tree. React Server Components address this at a different level — server components never ship JavaScript, but client components still hydrate as a complete subtree. True partial hydration in the React ecosystem typically requires islands-based architectures.

The Bundle Size Impact

The effect on bundle size is dramatic. Consider a documentation site with 50 components per page, of which three are interactive (search, theme toggle, code copy button). Full hydration ships the JavaScript for all 50 components. Partial hydration ships JavaScript for only three. This can reduce the initial JavaScript payload by 90% or more.

Beyond raw bytes, the execution cost also drops. The browser does not need to create virtual DOM nodes, diff trees, or allocate component instances for static content. This directly improves Time to Interactive (TTI) and reduces main thread blocking.

When Partial Hydration Falls Short

Partial hydration works best on content-heavy sites with isolated interactive elements. It becomes complex when interactivity is deeply nested within static content, or when interactive components need to communicate with each other through shared state. In those cases, you may end up hydrating large subtrees anyway, negating the benefit.

The boundary between static and interactive must be explicit. Developers need to make conscious decisions about which components need JavaScript. This adds cognitive overhead but pays back in performance.

Gotchas

  • State cannot cross the static/interactive boundary. A static parent component cannot pass reactive state to a hydrated child. Data flows must be planned around hydration boundaries.
  • Hydration directives vary wildly between frameworks. Astro uses client:*, Eleventy uses <is-land>, and others have their own conventions. There is no standard.
  • Lazy hydration is not partial hydration. Lazy hydration defers when JavaScript loads for a component; partial hydration determines whether JavaScript loads at all. They are complementary but distinct.
  • CSS-in-JS libraries can break partial hydration because they often assume all components are hydrated to inject styles dynamically. Static components may need separate CSS strategies.
  • Event delegation assumptions break. Frameworks like React use event delegation on the root. If the root is not fully hydrated, delegated events on static subtrees will not fire.