+1 (415) 779-8456

Shipping Less JavaScript from Gatsby: Slices, Partial Hydration, and What Each One Actually Fixes

Most Gatsby sites we inherit ship far more JavaScript than the page needs. The HTML is already correct — Gatsby rendered it at build time — and then the browser downloads 300–600 KB of React, the page-data JSON, and a framework runtime whose only job on a marketing page is to re-create markup that is already on screen. You pay for that twice: in Interaction to Next Paint on cheap Android hardware, and in build time, because every page that embeds the same header, footer, and nav has to be regenerated whenever one of those components changes.

Gatsby 5 gives you two levers for this that a lot of teams never turned on: the Slices API, and partial hydration with React Server Components. This tutorial covers when each one is worth the trouble, how to wire them up, and how to measure whether it actually helped.

Step 1: Find out what you are shipping

Before changing anything, get numbers. Gatsby's webpack bundle analyzer works without a plugin:

npx gatsby build
npx webpack-bundle-analyzer public/webpack.stats.json

If webpack.stats.json is missing, set GATSBY_WEBPACK_STATS=true or add gatsby-plugin-webpack-bundle-analyser-v2. Then check what a single page actually costs over the wire:

npx serve public -l 9000 &
npx lighthouse http://localhost:9000/ --only-categories=performance \
  --throttling-method=simulate --output=json --output-path=./lh.json
node -e "const r=require('./lh.json');console.log(r.audits['total-byte-weight'].displayValue, r.audits['unused-javascript'].displayValue)"

Write those two numbers down. Almost always the biggest offenders are, in order: a date library imported whole, an icon set imported whole, a carousel or animation library used on one page, and the framework itself.

The first three are ordinary bundle hygiene and you should fix them first — loadable-components around the carousel, date-fns/format instead of moment, per-icon imports. Slices and partial hydration are for what remains after that.

Step 2: Slices — the build-time win

A Slice is a component Gatsby renders once and stitches into every page's HTML at the end of the build. Change the footer copyright year and Gatsby re-renders one slice and patches HTML, instead of rebuilding 4,000 pages. On large content sites this is the difference between a 25-minute deploy and a 90-second one.

Create the slice component at src/slices/footer.js:

// src/slices/footer.js
import * as React from "react";
import { Link } from "gatsby";

export default function Footer({ year, links }) {
  return (
    <footer>
      <nav>
        {links.map((l) => (
          <Link key={l.to} to={l.to}>{l.label}</Link>
        ))}
      </nav>
      <p>&copy; {year} Example Ltd.</p>
    </footer>
  );
}

Register it in gatsby-node.js:

// gatsby-node.js
exports.createPages = async ({ actions }) => {
  const { createSlice } = actions;
  createSlice({
    id: "footer",
    component: require.resolve("./src/slices/footer.js"),
    context: { year: new Date().getFullYear() },
  });
  // ...your existing createPage calls
};

Then use it in the layout:

import { Slice } from "gatsby";

export default function Layout({ children }) {
  return (
    <>
      <Slice alias="header" />
      <main>{children}</main>
      <Slice alias="footer" links={FOOTER_LINKS} />
    </>
  );
}

Two rules that trip people up:

  1. Props passed to <Slice /> must be serializable. No functions, no JSX children, no class instances. If you need per-page variation, pass strings or plain objects — or create multiple slices with createSlice({ id: "footer--legal" }) and pick the alias per page with the slices option on createPage.
  2. A Slice cannot contain another Slice. Flatten your layout before you start.

Verify the build actually used them:

npx gatsby build --verbose 2>&1 | grep -i "slice"
ls public/slice-data | head

If public/slice-data is empty, the <Slice /> components are not being reached — usually because the layout is wrapped by wrapPageElement in gatsby-ssr.js in a way that bypasses them.

Slices do not reduce the JavaScript sent to the browser. They reduce build time and make incremental deploys cheap. That is a real win, but it is a different win from the next section.

Step 3: Partial hydration — the runtime win

Partial hydration inverts Gatsby's default. Normally every component is a client component and the whole tree hydrates. With partial hydration enabled, components are server components by default, ship zero JavaScript, and you opt individual components into the client with "use client".

Enable it in gatsby-config.js:

module.exports = {
  flags: { PARTIAL_HYDRATION: true },
};

Then mark only the interactive leaves:

// src/components/newsletter-form.js
"use client";
import * as React from "react";

export default function NewsletterForm() {
  const [email, setEmail] = React.useState("");
  // ...
}

Everything else — the article body, the nav, the pricing table, the footer — stays a server component and disappears from the bundle.

Things that will break, and you should expect all of them on a real site:

  • useState, useEffect, useContext, refs, and event handlers in a component that is not marked "use client" cause a build error. The fix is usually to push the state down into a small leaf, not to mark the whole layout as client.
  • Theme and auth providers are context, so they are client components — and everything rendered inside them as children is fine, but props you pass across the boundary must be serializable.
  • Third-party UI libraries that do not ship "use client" directives (a lot of older ones) must be wrapped in your own client component.
  • Partial hydration is still a flag, not a stable API, and it is React 18 server components — not the React 19 implementation. Do not enable it on a site you are actively planning to migrate off Gatsby within the year; you will be rewriting the boundaries anyway.

Measure again with the same Lighthouse command. On the content-heavy sites where we have used this, the realistic outcome is a 30–50% cut in shipped JavaScript on article pages and a meaningful INP improvement on mobile; on an app-like dashboard where nearly everything is interactive, the gain is close to zero and the added complexity is not worth it.

Step 4: Decide honestly which lever you need

A quick triage we use on client audits:

SymptomReach for
20+ minute builds, thousands of pages, shared chromeSlices
Poor INP / high unused-JS on mostly-static content pagesPartial hydration (after bundle hygiene)
One heavy widget on three pagesloadable-components, not either of these
Slow builds from image processing or GraphQL queriesNeither — see build caching and query discipline
Planning to leave Gatsby in the next 6–12 monthsBundle hygiene only; skip the flags

The honest framing for stakeholders: Slices is a safe, stable, boring optimisation that pays off on build time. Partial hydration is an experimental flag that pays off on user-facing performance and costs you a week of untangling client boundaries. Both are worth doing on a Gatsby site you intend to keep. Neither is a reason to stay on Gatsby if the rest of the platform case says otherwise.

Rollout checklist

  • Record baseline: total byte weight, unused JS, build wall-clock, page count.
  • Fix obvious bundle bloat first; re-measure.
  • Convert header/footer/nav to Slices; confirm public/slice-data is populated and HTML output is unchanged (diff a few built pages against the previous build).
  • Enable PARTIAL_HYDRATION on a branch only. Expect the first build to fail; fix boundaries one component at a time.
  • Re-run Lighthouse and your Playwright smoke suite before merging — hydration changes break interactive tests loudly, which is exactly what you want.
  • Keep the measurements in the repo so the next person can see whether the complexity earned its keep.

If you are weighing these changes against a replatform, or your builds have crept past the point where anyone wants to deploy on a Friday, get in touch — a short audit usually tells you which of these two levers is actually your problem.