I cut a production Shopify Plus Hydrogen homepage from roughly 4 to 5 seconds to about 2 seconds by removing one bad assumption: that every tab on the page needed its product data on first load. It did not. Once I moved the first tab into the route loader and lazy-loaded the rest, both speed and SEO got cleaner.
Why the homepage was slow in the first place
The slowdown came from loading too much data too late. A single homepage section had 12 tabs with 8 products each, which meant 96 products per section before counting multiple sections. All of that was fetched client-side in useEffect, so the page hydrated first, then started the real work.
This was for a Shopify Plus jewelry brand I work with, a homepage with multiple horizontal product rails and filter tabs. From a React point of view, the code looked harmless. From a Hydrogen point of view, it was a footgun.
// Before, anti-pattern in Hydrogen
function HomepageSection() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetchAllTabsProducts().then(setProducts);
}, []);
return <Tabs products={products} />;
}
The practical result was obvious. The homepage took roughly 4 to 5 seconds to feel ready. The SEO problem was quieter. The initial HTML had no product listings at all, because product data only arrived after hydration.
What I changed, and why it worked
I changed the fetch strategy to match real user behavior. Only the first tab of each section is server-rendered with its 8 products. The rest of the tabs lazy-load on click. That cuts initial data weight, gets product HTML into the response, and stops the homepage from paying for tabs most users never open.
I moved the first-tab fetch into the route loader and removed the initial useEffect path entirely. The component now receives the first tab as props from the server response, and only requests other tabs after an actual interaction.
// After, Hydrogen-native
export async function loader({context}) {
const firstTabProducts = await context.storefront.query(FIRST_TAB_QUERY, {
cache: context.storefront.CacheCustom({
mode: 'public',
maxAge: 60,
staleWhileRevalidate: 300,
}),
});
return json({firstTabProducts});
}
function HomepageSection() {
const {firstTabProducts} = useLoaderData();
return <Tabs initialProducts={firstTabProducts} />;
}
I am not claiming those exact cache numbers above were the production values. The real cache policy was tuned to the product catalog's update frequency. The important point is the pattern: first tab in the loader, other tabs on demand, cache the collection query intentionally instead of hoping the default strategy happens to fit.
That one change cut the homepage from roughly 4 to 5 seconds to about 2 seconds in observed behavior. I did not benchmark it with Lighthouse or WebPageTest, so I am not pretending this is a lab-grade performance study. It is a production engineering note. The page felt dramatically lighter because it stopped trying to fetch everything “just in case.”
The SSR bug that almost made this change look finished when it wasn’t
The first deploy still had an SSR hole. The in-house team tested the homepage with JavaScript disabled and found that the first-tab products were still missing from the initial HTML. That meant the “SSR fix” was not actually fully server-side yet, even though the page looked correct in a normal browser session.
The root cause was leftover client-only logic. The first tab had been conceptually moved to the loader, but one residual branch still routed its fetch through the old client path. So the UI worked, but the HTML response was still incomplete.
The fix was simple once the test exposed it. I made sure the first-tab data was fetched in the route loader and passed straight into the component as props, with no fallback path that depended on hydration. After that, the JS-disabled test showed product listings in the HTML exactly where they should be.
This is why I keep telling teams that “it works in the browser” is not a real SSR test. If the route matters for SEO, disable JavaScript once before you call it done. It is the fastest sanity check you can run.
The small UI cleanup that broke context after the performance fix
Performance changes often create pressure to compress the UI, and that can quietly remove meaning. In this case I stripped category prefixes from tab labels so long titles fit better inside horizontal rails. The result looked cleaner, but one section lost enough context that the shortened label became confusing.
A good example was the chain section. Titles like “Diamond Chains” and “Gold Chains” were shortened to make the tabs visually lighter. After deploy, the team noticed that under the “Gold Chains” section, the label just read “Chains.” It was shorter, but it also felt detached from the parent context.
The fix was to add a conditional guard rail. Prefix stripping stayed in place for titles where the parent section already carried the right context, but not in cases where shortening the label made the tab feel generic. That was a small change, but it mattered. Faster UI is not automatically clearer UI.
I mention this because performance work often gets discussed as if it is separate from interface language. It is not. Every time you compress data, text, or layout, you risk dropping context that the user still needed.
What I would tell another Hydrogen developer before they copy this pattern
If your first instinct is to preload every tab on a tabbed homepage, stop and check whether real users justify that cost. Most do not. They look at the first tab, some click the second, and very few make their way through all 12 tabs across multiple sections. Optimize for the common path, not the paranoid one.
If you are fetching primary route data in useEffect on a Hydrogen page, you are usually fighting the framework. Hydrogen gives you route loaders for a reason. Use the server for the part of the page that must exist on first load, then lazy-load the rest behind real user intent.
If you need more background before making that decision, my Should I Use It? page explains where Hydrogen is justified and where it is not. My cost breakdown is the practical version of the same conversation, and the case studies page shows the kind of stores where these tradeoffs actually appear.
The larger lesson is that this was not really a Hydrogen problem. It was a React habit problem. Teams coming from SPA-heavy or Next.js-heavy workflows reach for useEffect because it feels familiar. In Hydrogen, that habit gets expensive fast. You lose SSR, you lose HTML visibility, and you slow down the exact page that should be easiest to trust.
FAQ
Why not preload all tabs for instant switching?
Because the common path does not justify the cost. On a homepage with many sections and many tabs, preloading everything means paying an upfront performance penalty for interactions most users never make.
What was the most useful test in this refactor?
Turning JavaScript off and checking the first HTML response. That immediately exposed that the first-tab products were still going through a client-only path after the first deploy.
Did this require changing the whole homepage architecture?
No. The core change was narrowing what counted as initial data, then moving that smaller set into the route loader. The rest of the page could stay lazy and interaction-driven.
Is CacheCustom always necessary for this?
No. But it helps when your collection query freshness needs do not fit the default presets. In this case, I wanted more deliberate control over homepage collection caching behavior.