Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 042 / 100

Hybrid rendering with routeRules: lessons from production

Different rendering per route.

Expona Admin8 min read

Introduction: choosing the right rendering strategy per route

When you're running a production Nuxt application serving millions of requests, using the same rendering strategy for every route becomes a bottleneck. Your marketing homepage doesn't need to be regenerated on every request, but your user dashboard absolutely cannot be prerendered. Hybrid rendering with routeRules: lessons from production teaches us that we can—and should—combine multiple rendering strategies in a single application to optimize cost, performance, and user experience.

In this article, we'll walk through how to configure different rendering modes per route, drawing from real scenarios we've encountered in e-commerce platforms, travel booking systems, and SaaS dashboards. By the end, you'll understand when and how to apply prerendering, stale-while-revalidate caching, and client-side rendering within the same codebase.

Why hybrid rendering matters

Early in a Nuxt project, it's tempting to pick one rendering mode and stick with it. Full prerendering works great until you have thousands of product pages. Full server-side rendering gives you dynamic content but costs money on every request. Client-side rendering is fast for the browser but SEO suffers. The real power emerges when you stop thinking in absolutes and instead ask: what does this specific route need?

We've seen teams reduce hosting costs by 40–60% and improve Core Web Vitals by applying the right strategy to the right route. A prerendered marketing page combined with ISR product pages and SPA dashboards lets each part of your application do exactly what it does best.

Prerender: Marketing pages built at deploy

Prerendering means generating static HTML during the build step, before any user ever requests the page. This is ideal for content that changes infrequently: your homepage, about page, pricing page, legal pages, and blog posts.

The benefit: Static files served from a CDN have near-zero latency and cost almost nothing. Your server never sees these requests. SEO is perfect because search engines crawl fully rendered HTML.

The catch: You must know all the routes ahead of time, and regenerating them requires a new build and deploy. If you have 10,000 product SKUs, prerendering isn't practical without ISR.

In a travel booking platform, we prerendered:

  • Landing pages (/, /search, /about)
  • Static guides (/guides/paris-in-3-days)
  • Legal pages (/terms, /privacy)

Configure prerender in your nuxt.config:

typescript
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },
    '/about': { prerender: true },
    '/guides/**': { prerender: true },
    '/terms': { prerender: true },
    '/privacy': { prerender: true },
  },
});

During build, Nuxt crawls these routes and outputs static .html files. Your hosting (Vercel, Netlify, or static bucket) serves them with cache headers forever (or until your next deploy). No server compute needed.

SWR / ISR: Cache and revalidate product pages

Stale-while-revalidate (SWR) and incremental static regeneration (ISR) solve the prerender scalability problem. Instead of generating all pages upfront, you generate them on first request, cache the result, and quietly regenerate it in the background.

How it works: A user requests /products/leather-sofa. The server renders the page, caches the HTML for (say) 60 seconds, and serves it to subsequent users instantly. After 60 seconds, the next request triggers a background re-render; old users still get the cached version while the new one generates.

Where it shines: Product catalogs, blog archives, real estate listings, and other semi-dynamic content that doesn't change by the second but should reflect updates within minutes or hours.

We implemented ISR for an e-commerce site with 50,000 products:

typescript
export default defineNuxtConfig({
  routeRules: {
    '/products/**': { swr: 3600 }, // 1 hour cache, then revalidate
    '/blog/**': { swr: 7200 },     // 2 hours for blog posts
    '/categories/**': { swr: 1800 }, // 30 minutes for category pages
  },
});

The first visitor to /products/leather-sofa waits for server render (~200–500ms). The next 1000 visitors get instant cached HTML. After one hour, the page is stale and the next request triggers a background re-render. If inventory changes or the description updates, it's live within your revalidation window.

You can pair SWR with on-demand revalidation: when your CMS publishes a product update, send a webhook to your app to bust the cache immediately. This gives you CDN speed with fresh data:

typescript
// In your API route or webhook handler
export default defineEventHandler(async (event) => {
  if (event.node.req.headers['x-webhook-secret'] !== process.env.WEBHOOK_SECRET) {
    throw createError({ statusCode: 401 });
  }
  
  const productId = getQuery(event).productId;
  // Trigger revalidation by calling the page, then cache will refresh
  await $fetch(`/products/${productId}`, { headers: { 'cache-control': 'no-cache' } });
  
  return { ok: true };
});

SPA: Client-only for dashboards

Some routes make no sense to prerender or cache on the server. User dashboards, account settings, admin panels, and any page behind authentication should render client-side only.

Why: These pages are deeply personal. Prerendering a user's dashboard is pointless (and a security risk). Server-side rendering adds latency when the browser could fetch data in parallel. Client-side rendering with a skeleton loader gives a responsive, app-like feel.

The tradeoff: Initial HTML is bare. Search engines see an empty shell. But for authenticated routes, that's not a concern—bots can't log in anyway.

In a SaaS analytics platform, we set dashboard routes to SPA mode:

typescript
export default defineNuxtConfig({
  routeRules: {
    '/dashboard/**': { ssr: false },
    '/settings/**': { ssr: false },
    '/admin/**': { ssr: false },
    '/api/**': { cache: false },
  },
});

When a user navigates to /dashboard, Nuxt serves minimal HTML with your app shell. JavaScript hydrates and fetches the user's data from an API. The browser renders the dashboard without waiting for server I/O.

Pair SPA with a middleware guard to redirect unauthenticated users:

typescript
// middleware/auth.ts
export default defineRouteMiddleware((to, from) => {
  const user = useAuthStore().user;
  if (!user && to.path.startsWith('/dashboard')) {
    return navigateTo('/login');
  }
});

Configure: routeRules in nuxt.config

Now let's tie it together with a realistic, comprehensive example. Here's what a production nuxt.config might look like across an e-commerce + SaaS hybrid:

typescript
export default defineNuxtConfig({
  routeRules: {
    // Marketing & static pages: prerender
    '/': { prerender: true },
    '/about': { prerender: true },
    '/pricing': { prerender: true },
    '/blog': { prerender: true },
    '/blog/**': { prerender: true },
    '/contact': { prerender: true },
    '/terms': { prerender: true },
    '/privacy': { prerender: true },

    // Product pages: SWR cache with 1-hour revalidation
    '/products/**': { swr: 3600 },
    '/collections/**': { swr: 3600 },
    '/search': { swr: 1800 }, // Search results change more often

    // Authenticated user routes: client-side only
    '/dashboard': { ssr: false },
    '/dashboard/**': { ssr: false },
    '/account/**': { ssr: false },
    '/orders/**': { ssr: false },

    // Admin panel: SPA + auth guard
    '/admin/**': { ssr: false },

    // API routes: never cache
    '/api/**': { cache: false },

    // Catch-all 404: don't cache errors
    '/**': { cache: false },
  },

  nitro: {
    prerender: {
      crawlLinks: true,
      routes: ['/sitemap.xml'],
      ignore: ['/admin', '/dashboard', '/api'],
    },
  },
});

A few important settings:

  • prerender: true — Generates static HTML at build time.
  • swr: 3600 — Cache for 3600 seconds, then revalidate on next request.
  • ssr: false — Disable server-side rendering; deliver the shell and hydrate client-side.
  • cache: false — Never cache this route; always render fresh.
  • crawlLinks: true — Automatically discover and prerender links found in prerendered pages.

Deploy this config and observe the behavior:

  • Your build outputs static HTML for /, /about, /pricing, and all blog posts.
  • First visit to /products/shoes renders on the server and caches for an hour.
  • User navigates to /dashboard and gets an empty shell; JavaScript runs.
  • API calls never get HTTP caching; data is always fresh.

Common mistakes

Mistake 1: Prerendering routes you can't enumerate. If you try to prerender /products/** with 100,000 SKUs, your build will hang. Use SWR instead.

Mistake 2: Forgetting auth guards in SPA routes. Set ssr: false for dashboards, but don't forget middleware to redirect unauthenticated users before the browser loads.

Mistake 3: Not disabling cache for API endpoints. If you set a catch-all route rule with SWR, API responses get cached too. Explicitly set cache: false for /api/**.

Mistake 4: Using SWR for user-specific content. Don't cache pages like /user/me/profile with SWR. That user's profile becomes visible to the next browser that visits the same URL. Use ssr: false or cache: false for personalized routes.

Mistake 5: Not testing cache headers in production. Set up routeRules, deploy, and then verify with curl -I or your browser DevTools. Check the Cache-Control header to confirm what you intended actually happened.

Key takeaways

  • Use prerender: true for static content (marketing pages, legal docs) to get CDN speed and zero server cost.
  • Use swr for semi-dynamic content (product pages, blog archives) to balance freshness and performance.
  • Use ssr: false for authenticated or personalized routes (dashboards, account pages) to reduce server load and enable client-side data fetching.
  • Always set cache: false on /api/** and sensitive routes to prevent inadvertent caching of user-specific or sensitive responses.
  • Monitor cache headers in production and set up on-demand revalidation (webhooks) for critical content.
  • Hybrid rendering strategies can reduce hosting costs by 40–60% while improving performance across different parts of your app.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles