Published on

How to Improve Your Next.js App Page Load with Server Actions and NextAuth

Authors
  • avatar
    Name
    JRG
    Twitter

When building a Next.js application with server actions and NextAuth, you're often faced with a performance dilemma. Since you're using server-side functionality, you can't leverage static site generation (SSG) for optimal page load speeds. However, there's a powerful alternative that can make your app feel just as fast: prefetching.

The Challenge: Server Actions vs. Static Generation

Next.js server actions and NextAuth provide powerful server-side capabilities, but they come with a trade-off:

  • Server Actions: Enable dynamic server-side operations, form handling, and data mutations
  • NextAuth: Provides authentication and session management on the server
  • The Problem: These features prevent you from using getStaticProps or static generation
  • The Result: Every page request hits the server, potentially slowing down your app

But what if you could make your app feel instant without sacrificing these powerful features?

The Solution: Strategic Prefetching

Prefetching is a technique that loads page data in the background before the user actually navigates to it. When implemented correctly, it can make your app feel as fast as a statically generated site.

How Prefetching Works

import { useRouter } from 'next/router'

// Prefetch pages when the current page loads
const router = useRouter();

useEffect(() => {
  router.prefetch('/dashboard')
  router.prefetch('/profile')
  router.prefetch('/settings')
}, [router])

Implementing Prefetching in Your Next.js App

1. Basic Prefetching Strategy

Start by identifying the most common navigation paths in your app and prefetch them:

import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

export default function Layout({ children }) {
  const router = useRouter()

  useEffect(() => {
    // Prefetch main navigation pages
    router.prefetch('/dashboard')
    router.prefetch('/profile')
    router.prefetch('/settings')
    router.prefetch('/analytics')
  }, [router])

  return <>{children}</>
}

2. Smart Prefetching with User Behavior

Take it a step further by prefetching based on user interaction patterns:

import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'

export default function Navigation() {
  const router = useRouter()
  const [hoveredLink, setHoveredLink] = useState(null)

  const handleLinkHover = (href) => {
    setHoveredLink(href)
    // Prefetch on hover for even faster navigation
    router.prefetch(href)
  }

  return (
    <nav>
      <a 
        href="/dashboard"
        onMouseEnter={() => handleLinkHover('/dashboard')}
      >
        Dashboard
      </a>
      {/* More navigation links */}
    </nav>
  )
}

3. Conditional Prefetching Based on User Role

With NextAuth, you can prefetch different pages based on user permissions:

import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'

export default function ConditionalPrefetch() {
  const { data: session } = useSession()
  const router = useRouter()

  useEffect(() => {
    if (session?.user?.role === 'admin') {
      // Prefetch admin-only pages
      router.prefetch('/admin/users')
      router.prefetch('/admin/settings')
      router.prefetch('/admin/analytics')
    } else if (session?.user?.role === 'user') {
      // Prefetch user pages
      router.prefetch('/profile')
      router.prefetch('/orders')
      router.prefetch('/support')
    }
  }, [session, router])

  return null
}

Advanced Prefetching Strategies

1. Route-Based Prefetching

Create a centralized prefetching system:

// utils/prefetchRoutes.ts
export const ROUTES = {
  HOME: '/',
  DASHBOARD: '/dashboard',
  PROFILE: '/profile',
  SETTINGS: '/settings',
  ANALYTICS: '/analytics',
} as const

export const prefetchRoutes = (router, routes: string[]) => {
  routes.forEach(route => {
    router.prefetch(route)
  })
}

// Usage in components
useEffect(() => {
  prefetchRoutes(router, [
    ROUTES.DASHBOARD,
    ROUTES.PROFILE,
    ROUTES.SETTINGS
  ])
}, [router])

2. Progressive Prefetching

Load pages in priority order:

export const prefetchProgressive = async (router, routes) => {
  // High priority routes first
  const highPriority = ['/dashboard', '/profile']
  const mediumPriority = ['/settings', '/analytics']
  const lowPriority = ['/help', '/about']

  // Immediate prefetch for high priority
  highPriority.forEach(route => router.prefetch(route))

  // Medium priority after a short delay
  setTimeout(() => {
    mediumPriority.forEach(route => router.prefetch(route))
  }, 100)

  // Low priority after user interaction
  setTimeout(() => {
    lowPriority.forEach(route => router.prefetch(route))
  }, 1000)
}

3. Intersection Observer Prefetching

Prefetch pages when they become visible in the viewport:

import { useInView } from 'react-intersection-observer'

export default function LazyPrefetch({ href, children }) {
  const { ref, inView } = useInView({
    triggerOnce: true,
    threshold: 0.1,
  })
  const router = useRouter()

  useEffect(() => {
    if (inView) {
      router.prefetch(href)
    }
  }, [inView, href, router])

  return <div ref={ref}>{children}</div>
}

Performance Monitoring and Optimization

1. Measure the Impact

Track your prefetching performance:

// utils/performance.ts
export const measurePrefetchPerformance = (route: string) => {
  const start = performance.now()
  
  return {
    start: () => start,
    end: () => {
      const end = performance.now()
      const duration = end - start
      console.log(`Prefetch ${route}: ${duration.toFixed(2)}ms`)
      return duration
    }
  }
}

// Usage
const perf = measurePrefetchPerformance('/dashboard')
router.prefetch('/dashboard').then(() => {
  perf.end()
})

2. Smart Prefetching with Analytics

Use analytics data to optimize prefetching:

export const getPopularRoutes = () => {
  // Analyze user navigation patterns
  // Return routes ordered by frequency
  return [
    '/dashboard',
    '/profile', 
    '/settings',
    '/analytics'
  ]
}

export const prefetchPopularRoutes = (router) => {
  const popularRoutes = getPopularRoutes()
  popularRoutes.forEach(route => router.prefetch(route))
}

Best Practices for Prefetching

1. Don't Over-Prefetch

  • Limit concurrent prefetches: Don't prefetch more than 3-4 routes simultaneously
  • Prioritize user intent: Focus on likely next actions
  • Respect bandwidth: Consider mobile users and slower connections

2. Timing is Everything

  • Immediate: Critical navigation paths
  • On hover: Likely user interactions
  • On scroll: Content that's about to become visible
  • Delayed: Secondary pages after user engagement

3. Handle Errors Gracefully

export const safePrefetch = async (router, route) => {
  try {
    await router.prefetch(route)
  } catch (error) {
    console.warn(`Failed to prefetch ${route}:`, error)
    // Don't let prefetch failures break your app
  }
}

Real-World Implementation Example

Here's how to implement prefetching in a typical Next.js app with NextAuth:

// components/AppLayout.tsx
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

export default function AppLayout({ children }) {
  const { data: session, status } = useSession()
  const router = useRouter()

  useEffect(() => {
    if (status === 'loading') return

    if (session) {
      // User is authenticated - prefetch authenticated routes
      const authRoutes = [
        '/dashboard',
        '/profile',
        '/settings',
        '/orders'
      ]
      
      authRoutes.forEach(route => {
        router.prefetch(route)
      })

      // Role-based prefetching
      if (session.user.role === 'admin') {
        router.prefetch('/admin')
        router.prefetch('/admin/users')
      }
    } else {
      // User is not authenticated - prefetch public routes
      router.prefetch('/login')
      router.prefetch('/register')
      router.prefetch('/about')
    }
  }, [session, status, router])

  return (
    <div className="app-layout">
      <Navigation />
      <main>{children}</main>
    </div>
  )
}

Measuring Success

Track these metrics to ensure your prefetching is working:

  • Time to Interactive (TTI): Should decrease significantly
  • First Contentful Paint (FCP): Faster perceived loading
  • Navigation performance: Smoother page transitions
  • User engagement: Higher click-through rates on prefetched pages

Conclusion

While server actions and NextAuth prevent you from using static site generation, strategic prefetching can deliver comparable performance benefits. By intelligently loading pages in the background, you can create a Next.js app that feels instant without sacrificing the power of server-side functionality.

The key is to:

  1. Identify critical user paths and prefetch them immediately
  2. Use user behavior to predict and prefetch likely next actions
  3. Implement progressive loading to balance performance and resource usage
  4. Monitor and optimize based on real user data

Remember, the goal isn't to prefetch everything - it's to prefetch the right things at the right time. With careful implementation, your Next.js app can achieve near-instant navigation while maintaining all the benefits of server actions and NextAuth.

Start implementing these strategies today, and watch your app's perceived performance soar!