Shopify Hydrogen Journal

How I Cut a Hydrogen Homepage From 5s to 2s

By Emre Mutlu, creator of the world's first English Shopify Hydrogen course on Udemy.

Published April 21, 2026Last updated April 21, 20266 min read

Article summary

I cut a Shopify Plus Hydrogen homepage from roughly 4 to 5 seconds to about 2 seconds by stopping the initial useEffect fetch, server-rendering only the first tab of each section, and lazy-loading the rest on interaction. The bigger win was not speed alone. It was getting product HTML back into the first response.

Close-up jewelry detail arranged in a premium editorial composition.

Homepage performance

When a Hydrogen homepage feels slow, the problem is often the data path, not the UI.

This store got faster when I stopped fetching every tab on hydration and only rendered what the first view actually needed.

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.

Next Step

Let’s build the storefront your growth stage actually needs.

If this article sounds like your store’s situation, I can help you turn the insight into a clear Hydrogen scope and launch plan.

Hire Emre on Upwork

Direct access. No agency maze.