Unraveling Performance Bottlenecks in SSR and SSG with Nextjs and Nuxtjs
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Introduction
In the rapidly evolving landscape of web development, delivering exceptional user experiences is paramount. Two prominent paradigms, Server-Side Rendering (SSR) and Static Site Generation (SSG), championed by frameworks like Next.js and Nuxt.js, have emerged as powerful tools to achieve this by improving initial page load times and search engine optimization. However, despite their undeniable advantages, developers often encounter performance bottlenecks that hinder optimal application delivery. Understanding these bottlenecks, their root causes, and effective mitigation strategies is crucial for building high-performing web applications. This article delves into the intricacies of performance challenges within SSR and SSG contexts using Next.js and Nuxt.js, providing a technical deep dive into their underlying mechanisms and practical solutions.
Core Concepts Explained
Before dissecting the performance bottlenecks, let's briefly define the core concepts central to our discussion:
- Server-Side Rendering (SSR): In SSR, HTML is generated on the server for each request. When a user requests a page, the server fetches data, renders the component into a full HTML string, and sends it to the client. The client then "hydrates" this static HTML, attaching JavaScript event listeners and making it interactive.
- Static Site Generation (SSG): SSG involves pre-rendering all pages at build time. For each page, the necessary data is fetched, and the component is rendered into static HTML, CSS, and JavaScript files. These files are then deployed to a CDN and served directly to the user.
- Hydration: This is the process where a client-side JavaScript application takes over pre-rendered HTML (from SSR or SSG) and makes it interactive. It involves attaching event listeners, making the DOM dynamic, and managing state.
- Time to First Byte (TTFB): The time it takes for a user's browser to receive the first byte of the page content from the server. A low TTFB indicates a responsive server.
- First Contentful Paint (FCP): The time from when the page starts loading to when any part of the page's content is rendered on the screen.
- Largest Contentful Paint (LCP): The time from when the page starts loading to when the largest image or text block within the viewport is rendered.
- Cumulative Layout Shift (CLS): A measure of the unexpected shifts in webpage content as it loads.
- Total Blocking Time (TBT): The total amount of time between FCP and Time to Interactive (TTI) where the main thread is blocked for long enough to prevent input responsiveness.
SSR/SSG Performance Bottlenecks and Solutions
Both SSR and SSG offer distinct performance characteristics, leading to different sets of challenges.
Server-Side Rendering (SSR) Bottlenecks
SSR's primary bottleneck typically lies on the server side and during the hydration phase.
1. Data Fetching Overhead
Problem: For each request, the server needs to fetch data before rendering the page. If data fetching is slow (e.g., waiting for multiple API calls, database queries), it directly impacts the TTFB. This can be exacerbated if multiple components on a page independently fetch data, leading to waterfall effects.
Example Next.js:
// pages/posts/[id].js export async function getServerSideProps(context) { const { id } = context.params; const postRes = await fetch(`https://api.example.com/posts/${id}`); const post = await postRes.json(); const authorRes = await fetch(`https://api.example.com/authors/${post.authorId}`); const author = await authorRes.json(); return { props: { post, author } }; }
In this example, getServerSideProps
waits for two sequential API calls.
Solutions:
- Parallel Data Fetching: Fetch multiple data sources concurrently using
Promise.all
.export async function getServerSideProps(context) { const { id } = context.params; const [postRes, authorRes] = await Promise.all([ fetch(`https://api.example.com/posts/${id}`), fetch(`https://api.example.com/authors/${id}`) // Assuming author ID can be derived ]); const post = await postRes.json(); const author = await authorRes.json(); return { props: { post, author } }; }
- Caching: Implement server-side caching for frequently accessed data or API responses. Use tools like Redis or in-memory caches.
- GraphQL Batching/Persisted Queries: If using GraphQL, batch multiple queries into a single request or use persisted queries to reduce network overhead.
- Database Query Optimization: Ensure database queries are efficient with proper indexing and optimized schemas.
2. Server Resource Consumption
Problem: Each SSR request requires CPU and memory on the server to render the React/Vue components into HTML. For applications with high traffic or complex pages, this can lead to server overload, increased latency, and even server crashes.
Solutions:
- Compute Provisioning: Scale up server resources (CPU, RAM) or utilize serverless functions (e.g., Vercel's serverless functions for Next.js, Netlify Functions for Nuxt.js) that auto-scale based on demand.
- Edge Caching (CDN): Cache the rendered HTML at the edge using a CDN. This significantly reduces the load on the origin server for subsequent requests to the same page.
- Partial Hydration / Islands Architecture: Instead of hydrating the entire page, only hydrate interactive components. This reduces the amount of JavaScript processed on the client and server. While not natively built-in for Next.js/Nuxt.js, experimental approaches exist.
- Code Splitting: While primarily client-side, effective code splitting can indirectly reduce server work by simplifying the SSR bundle.
3. Hydration Overheads
Problem: After the server sends the HTML, the client-side JavaScript takes over to 'hydrate' the page. This involves rebuilding the virtual DOM, attaching event listeners, and reconciling with the server-rendered HTML. If the JavaScript bundle is large or the DOM structure is complex, this process can be slow, leading to a period where the page appears interactive but isn't (known as "jank" or "jank during hydration"). This impacts TBT and TTI.
Example Nuxt.js: A large number of interactive components on a complex page will result in a larger JavaScript bundle and more work for hydration.
Solutions:
- Reduce JavaScript Bundle Size:
- Bundle Analysis: Use tools like
webpack-bundle-analyzer
to identify large dependencies and remove unused code (tree-shaking). - Dynamic Imports (Lazy Loading): Load components only when they are needed, especially for components below the fold or those triggered by user interaction.
// Next.js import dynamic from 'next/dynamic'; const MyComponent = dynamic(() => import('../components/MyComponent')); // Nuxt.js // components/MyComponent.vue // In template: <client-only><MyComponent /></client-only> // In script: const MyComponent = () => import('@/components/MyComponent.vue')
- Bundle Analysis: Use tools like
- Minimize DOM Complexity: A flatter and smaller DOM tree requires less work for hydration.
- Virtualization: For long lists or tables, use virtualization libraries (e.g.,
react-virtualized
,vue-virtual-scroller
) to render only visible items, reducing DOM size. - Throttling/Debouncing Event Handlers: Optimize client-side event handlers to prevent excessive re-renders during hydration.
Static Site Generation (SSG) Bottlenecks
SSG's challenges primarily manifest during the build phase and when dealing with dynamic content.
1. Long Build Times
Problem: For large websites with thousands or millions of pages, generating all pages at build time can take a very long time, sometimes hours. This impacts developer velocity and the ability to deploy continuous updates.
Example Next.js:
// pages/blog/[slug].js export async function getStaticPaths() { const posts = await fetch('https://api.example.com/posts').then(res => res.json()); const paths = posts.map(post => ({ params: { slug: post.slug } })); return { paths, fallback: false }; } export async function getStaticProps({ params }) { const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(res => res.json()); return { props: { post } }; }
If posts
contains 10,000 entries, getStaticProps
will be called 10,000 times during the build.
Solutions:
- Incremental Static Regeneration (ISR): Next.js offers ISR, allowing you to update static pages after they've been built and deployed, without requiring a full rebuild. Pages can be re-generated in the background when a request comes in, serving the stale page while a fresh version is built.
Nuxt.js 3 has a similar concept withexport async function getStaticProps({ params }) { const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(res => res.json()); return { props: { post }, revalidate: 60 }; // Revalidate every 60 seconds }
revalidate
inuseAsyncData
. - Distributed Builds: Utilize build tools and CI/CD pipelines that support parallel builds across multiple machines.
- Cache Build Artifacts: Cache dependencies and previous build outputs to speed up subsequent builds.
- Selective Pre-rendering: Only pre-render the most critical pages at build time and use SSR or client-side rendering for less critical or highly dynamic pages (
fallback: 'blocking'
orfallback: true
ingetStaticPaths
for Next.js). - Optimize Data Fetching During Build: Ensure data fetches for
getStaticProps
are as efficient as possible (e.g., batching API calls).
2. Staleness of Content
Problem: Since SSG pages are built at deploy time, their content can become stale if the underlying data changes frequently. This requires a full rebuild and redeploy to reflect updates, which can be impractical for dynamic content like stock prices or live scores.
Solutions:
- Incremental Static Regeneration (ISR): As discussed above, ISR is the primary solution for keeping SSG content fresh without sacrificing performance.
- Client-Side Data Fetching (CSR): For highly dynamic sections of a static page, use client-side fetching after the initial page load. The page structure is static, but specific components fetch and display real-time data.
// pages/stock/[symbol].js // Static page structure, but stock price fetched on client function StockPage({ initialData }) { // initialData could be company info const [price, setPrice] = useState(initialData.price); useEffect(() => { const interval = setInterval(async () => { const res = await fetch(`/api/realtime-price?symbol=${initialData.symbol}`); const newPrice = await res.json(); setPrice(newPrice); }, 5000); return () => clearInterval(interval); }, []); return ( <div> <h1>{initialData.companyName}</h1> <p>Current Price: ${price}</p> </div> ); } export async function getStaticProps() { /* ...initial company data */ } export async function getStaticPaths() { /* ...pre-render popular stocks */ }
- Webhooks for Rebuilds: Configure your CMS or data source to trigger a webhook to your CI/CD pipeline, initiating a build and deploy whenever content changes.
3. Large Build Output
Problem: For very large sites, the sheer number of static HTML files generated can consume considerable disk space and make deployment and CDN synchronization take longer.
Solutions:
- Efficient Asset Optimization: Ensure images are optimized (WebP/AVIF, lazy loading), and other assets (CSS, JS) are minified and compressed.
- CDN for Assets: Leverage a CDN to serve static assets, distributing the load and improving delivery speed globally.
- Selective Pre-rendering: Carefully choose which pages genuinely benefit from SSG. Pages with highly dynamic or personalized content might be better suited for SSR or CSR.
- Archive Older Content: If content has a limited lifespan, consider archiving or removing very old static pages that are rarely accessed.
Conclusion
Both SSR and SSG, when implemented with Next.js or Nuxt.js, offer compelling advantages for web performance and SEO. However, they come with their own set of performance bottlenecks. For SSR, the primary concerns revolve around server load, data fetching latency, and client-side hydration. SSG, while offering superior initial load times, faces challenges with long build times for large sites and content staleness. By understanding these nuances and applying strategies like parallel data fetching, ISR, dynamic imports, and leveraging CDNs, developers can effectively mitigate common bottlenecks, ensuring high-performing, user-friendly applications. The choice between SSR and SSG, or a hybrid approach, ultimately depends on the specific requirements of your application, balancing build time, data freshness, and interactivity with optimal performance.