Skip to content

Performance Enhancement

After the initial completion of the MeetMate project, several performance bottlenecks and inefficient data management patterns were identified in the production application. The frontend began to show noticeable latency during data fetching operations, unnecessary re-renders, and suboptimal routing transitions. Additionally, as the application architecture evolved over time, maintaining consistency between different parts of the codebase became increasingly challenging. These issues eventually contributed to a bad user experience and difficulty in implementing new features.

Analysis of Performance Issues

Authentication System Inefficiencies

The initial authentication middleware implementation suffered from multiple inefficiencies that impacted overall application performance. Each page navigation triggered redundant authentication checks, resulting in excessive API calls and increased page load times. This led to longer load times throughout the entire application.

Analysis of the network traffic revealed that a typical user session could generate a large amount of unnecessary authentication related requests. The middleware implementation performed full user object fetching on every navigation, despite the fact that user data rarely changes during a session. This not only increased server load but also delayed content rendering as the application waited for authentication processes to complete.

The original middleware used verbose logging on every request, adding unnecessary performance issues to production deployments. More problematically, the token refresh mechanism attempted to refresh tokens even in cases where it was not necessary, creating additional network traffic. The middleware was also missing proper error handling for authentication failures, often triggering multiple API calls when a simple retry or redirect would have been more fitting.

// Original middleware implementation with inefficiencies
export async function middleware(req: NextRequest) {
  let user = await getUser(req.cookies.get("accessToken")?.value);
  let response = NextResponse.next();

  // Verbose logging on every request
  if (user === null) {
    console.log("No user found, attempting to refresh access token.");
    const refreshToken = req.cookies.get("refreshToken")?.value;

    if (refreshToken !== undefined || refreshToken !== "") {
      const newAccessToken = await refreshAccessToken(refreshToken);
      // [...] More code and logging...
    }
  }
  // Role-based routing logic...
  return response;
}

The lack of caching in this authentication flow meant that even quick navigations between pages would trigger complete authentication cycles, which users then experienced as application sluggishness. This authentication problem was especially noticeable when users navigated between frequently accessed sections using a large amount of data like the dashboard and bookings pages.

State Management Fragmentation

The application's state management architecture had been developed organically during the projects progress, resulting in a fragmented approach where data fetching logic was duplicated across multiple components. This came as features were implemented incrementally, with each component implementing its own data retrieval logic rather than leveraging a centralized approach.

In a typical component, separate useEffect hooks would handle different data types, each maintaining its own loading and error states. Thus, creating multiple serious performance issues. First, it resulted in duplicate network requests as multiple components independently requested the same data. The application would often make three or four identical API calls within seconds, unnecessarily increasing both server load and client-side processing.

// Common pattern of component-level data fetching
function UserDashboard() {
  const [userData, setUserData] = useState(null);
  const [userLoading, setUserLoading] = useState(true);
  const [appointments, setAppointments] = useState([]);
  const [appointmentsLoading, setAppointmentsLoading] = useState(true);

  // User data fetching
  useEffect(() => {
    async function fetchUser() {
      setUserLoading(true);
      const token = await getAccessToken();
      const data = await getUser(token);
      setUserData(data);
      setUserLoading(false);
    }
    fetchUser();
  }, []);

  // Appointments data fetching - separate and uncoordinated
  useEffect(() => {
    async function fetchAppointments() {
      setAppointmentsLoading(true);
      const token = await getAccessToken();
      const data = await getAppointments(token);
      setAppointments(data);
      setAppointmentsLoading(false);
    }
    fetchAppointments();
  }, []);

  // Component rendering with multiple loading states
}

Error handling in this split-up approach was similarly problematic. Each data fetching operation implemented its own error handling, leading to inconsistent error messages and recovery mechanisms. In some cases, errors in one data fetch would render a component unusable, while in others, the errors were silently logged without user notification. This inconsistency created confusion for users when operations failed.

Analysis using React DevTools Profiler showed that some components were re-rendering up to five times during a single data loading cycle, creating noticeable lag. Each state update triggered multiple re-renders throughout the component tree, consuming CPU resources and delaying user interactions. This was particularly problematic on mobile devices, where the excessive rendering cycles could cause significant battery drain.

Implementation of Solutions

Middleware Optimization with Intelligent Caching

To address the authentication performance issues, a caching system was implemented in the middleware. This system completely changed the approach to user authentication by introducing time-based token caching, reducing unnecessary network requests while maintaining security.

The core of this optimization was an in-memory cache that stored user data with associated timestamps. When a request required user data, the system would first check this cache before making a network request. If valid cached data existed, it would be used immediately, eliminating the network latency entirely. This approach maintained a configurable cache duration, balancing security needs with performance optimization.

// Optimized middleware with user caching
const CACHE_DURATION = 60 * 1000; // 1 minute cache
const userCache = new Map<string, { user: User; timestamp: number }>();

async function getUserWithCache(accessToken: string | undefined) {
  if (accessToken === null) return null;

  // Check cache first before network request
  const cachedData = userCache.get(accessToken!);
  const now = Date.now();

  if (cachedData !== null && cachedData !== undefined && now - cachedData.timestamp < CACHE_DURATION) {
    return cachedData.user;
  }

  // Only fetch from network when necessary
  const user = await getUser(accessToken!);

  if (user !== null) {
    userCache.set(accessToken!, { user, timestamp: now });
  }

  return user;
}

export async function middleware(req: NextRequest) {
  const accessToken = req.cookies.get("accessToken")?.value;
  let user = await getUserWithCache(accessToken);
  const response = NextResponse.next();

  // Token refresh logic only when needed
  if (user === null) {
    const refreshToken = req.cookies.get("refreshToken")?.value;
    // refresh token flow...
  }

  // Role-based routing logic
  return response;
}

The token refresh mechanism was also significantly improved. Instead of attempting refreshes over and over again, the new implementation only initiated the refresh process when truly necessary. This prevented refresh attempts that occurred in the original implementation. Additionally, verbose logging statements were removed from production code reducing JavaScript execution time during navigation.

The middleware was further optimized by implementing more efficient error handling for authentication failures. Instead of triggering complex recovery processes for every error the system now categorizes authentication issues and applies appropriate responses. For example, network-related failures might trigger a retry, while invalid credentials would trigger a redirect to the login page. This then reduces wasted processing and provides a more predictable user experience.

These middleware optimizations reduced authentication-related network requests, directly improving page transition times and reducing server load. The more efficient approach to authentication made the application feel significantly more responsive, particularly during frequent navigation between different sections.

State Management Consolidation

The scattered state management throughout the application was addressed through a refactoring of the context providers. This approach centralized data fetching and state management, thus eliminating the redundant network requests and inconsistent loading states.

This refactoring was a unified data fetching approach. Instead of individual components implementing their own data retrieval logic this was unified within context providers that manage related data entities. These providers implement a coordinated data fetching strategy where related data is retrieved together using a single authentication token and unified loading state.

// Unified data fetching in context provider
export function DashboardProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<ClientUser | null>(null);
  const [appointments, setAppointments] = useState<Appointment[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  // Single data fetching function
  const fetchAllData = async () => {
    setIsLoading(true);
    setError(null);

    try {
      const accessToken = await getAccessToken();

      if (accessToken === null) {
        setIsLoading(false);
        return;
      }

      // Fetch all related data with one access token
      try {
        const userData = await getUser(accessToken);
        setUser(userData as ClientUser);

        const appointmentsData = await getAppointments(accessToken);
        setAppointments(appointmentsData || []);
      } catch (e) {
        setError(e as Error);
      }
    } finally {
      setIsLoading(false);
    }
  };

  // Initial data load
  useEffect(() => {
    void fetchAllData();
  }, []);

  // Components now use this context instead of fetching directly
  return (
    <DashboardContext.Provider 
      value={{
        user,
        loading: isLoading,
        refreshData: fetchAllData,
        appointments
      }}
    >
      {children}
    </DashboardContext.Provider>
  );
}

This combined approach provides several important performance benefits. First, it eliminates duplicate network requests by ensuring that each data entity is requested only once regardless of how many components need that data. Second, it implements a unified loading state that prevents the UI flickering issues present in the original implementation. All components dependent on the context receive consistent loading signals, creating a more coherent visual experience during data fetching.

Error handling was also significantly improved through this consolidation. The context now implements centralized error management with consistent error reporting and recovery mechanisms. Errors are captured at the context level and made available to all consuming components ensuring that error states are displayed consistently throughout the application. This approach makes the application more resilient, as components can gracefully handle error conditions rather than breaking or displaying inconsistent states.

Components that previously implemented their own data fetching were refactored to use the centralized contexts. This simplification dramatically reduced component complexity, eliminating dozens of lines of data fetching code from individual components. The simplified components focus solely on rendering and interaction logic, retrieving all necessary data from contexts. This separation of concerns not only improves performance but also enhances code maintainability by establishing clear architectural boundaries.

GraphQL Query Optimization

The application's GraphQL implementation was thoroughly optimized to take full advantage of Apollo Client's caching and data management capabilities. Each query was reviewed and configured with appropriate fetch policies to balance data freshness with performance.

Queries for relatively static data now implement cache-first policies that minimize network requests. For more dynamic data, a cache-and-network approach ensures users see cached data immediately while updates load in the background. This strategic approach to caching dramatically reduces perceived loading times for frequently accessed data.

// Optimized GraphQL queries with proper fetch policies
const { data: companies } = useQuery(GET_COMPANIES, {
  pollInterval: 300000, // 5-minute polling
  fetchPolicy: "cache-and-network", // Use cache first, then refresh
  nextFetchPolicy: "cache-first", // Use cache for subsequent requests
  onError: (error) => {
    // Error handling
  }
});

The polling interval of each query was also carefully adjusted so that information that changes frequently is refreshed at appropriate intervals, while more static data is cached for longer periods. This balanced approach ensures data remains reasonably current without generating excessive network traffic.

Error handling for GraphQL operations was enhanced with retry logic and fallback mechanisms. Network errors now trigger automatic retries with exponential backoff, improving resilience without requiring user intervention. This reduces the impact of temporary connectivity issues, creating a more reliable experience especially for users on mobile networks.

Performance Measurement and Results

Measurement Methodology

To show the impact of these optimizations multiple measurements where conducted. All measurements were conducted under controlled conditions to ensure reliable comparisons between the original and optimized implementations.

Lighthouse, Google's web performance testing tool, was used to measure core web vitals and overall performance metrics. Tests were conducted using the same hardware and network conditions for both versions, with simulated Fast 4G throttling to show performance differences more clearly. The Chrome DevTools Network panel provided detailed analysis of network requests, while React DevTools Profiler measured component rendering performance. Each test was conducted multiple times with cleared caches, and results were averaged to ensure statistical validity.

Core Web Vitals Improvements

Lighthouse testing of the production builds revealed modest but meaningful improvements in several key performance metrics:

Metric Original Optimized Change
Performance Score 96 96 No change
First Contentful Paint 0.4s 0.3s 25% faster
Largest Contentful Paint 1.4s 1.3s 7% faster
Total Blocking Time 50ms 50ms No change
Cumulative Layout Shift 0.029 0.011 62% improvement
Speed Index 0.8s 1.2s 50% slower*

Note

The Speed Index regression is likely due to the addition of more complex components and improved error handling, which was a conscious trade-off for better reliability.

While the overall performance score remained stable at an excellent 96/100, specific metrics showed notable improvements. Particularly significant is the 62% improvement in Cumulative Layout Shift, which measures visual stability during page load. This enhancement creates a more professional user experience with fewer jarring visual changes as content loads.

First Contentful Paint improved by 25%, indicating faster initial rendering of page content. The Largest Contentful Paint also showed improvement, though more modest at 7%. These improvements contribute to a more responsive feeling application, particularly during initial page loads.

Network Request Optimization

Network request analysis showed efficiency improvements in several areas:

Metric Original Optimized Improvement
Total Requests 47 38 19% reduction
Authentication Requests 12 5 58% reduction
Average Response Time 208ms 195ms 6% faster
Script Size Reduction - 322KB Potential savings

The most notable improvement was the 58% reduction in authentication-related requests, which directly results from the middleware caching implementation. This significant reduction decreases both server load and client-side processing time associated with authentication operations.

The overall request count decreased by 19%, from 47 to 38 requests. This reduction minimizes HTTP overhead and browser processing time, contributing to more efficient page loads.

Analysis of the network waterfall shows more efficient request patterns in the optimized version, with better parallelization and fewer redundant calls. This improvement is particularly evident in data fetching operations, where the unified context approach eliminates duplicate requests for the same data.

Component Rendering Efficiency

Code analysis and profiling during development showed significant improvements in component efficiency:

Estimated Metric Before After Improvement
Re-render Frequency ~4-5 per data update ~1-2 per data update ~60% reduction
Component Code Complexity ~85 lines avg. ~50 lines avg. ~40% reduction
Data Fetching Duplication 18 instances 3 instances 83% reduction

The refactored context system reduced component complexity by approximately 40% by moving data fetching logic from individual components to centralized providers. This architectural improvement not only enhances performance but also significantly improves code maintainability.

The most substantial rendering improvement came from eliminating cascading re-renders through the unified state management approach. Components that previously re-rendered 4-5 times during a data loading cycle now typically render only once or twice, creating a smoother, more responsive user experience.

Real-World Impact

Beyond the raw metrics, the optimizations created tangible improvements in user experience:

  1. More Consistent Loading States: The unified loading approach eliminated the visual flickering that occurred when different parts of the interface updated at different times.

  2. Faster Authentication: The 58% reduction in authentication requests makes page transitions feel significantly more responsive, particularly when navigating between protected routes.

  3. Improved Error Handling: The centralized error management provides more consistent feedback when operations fail, enhancing user confidence in the application.

  4. Enhanced Developer Experience: The architectural improvements reduced code duplication and complexity, making the application easier to maintain and extend.

These enhancements collectively create a more professional, responsive application that better serves both users and developers. While some performance metrics show modest gains rather than dramatic improvements, this reflects the already well-optimized state of the original codebase.

Conclusion

Working on these performance optimizations has been both challenging and rewarding. Despite my professional background, rethinking the application's architecture from a performance perspective presented unique technical challenges, much like the authentication system did during the initial development.

The decision to move from component-level data fetching to a centralized context approach mirrors my earlier experience with state management, where I found that React Contexts provided a more elegant solution than Redux. Both decisions led to cleaner code and better data flow throughout the application.

Implementing the middleware caching system was particularly challenging due to the limited documentation available for Next.js App Router middleware. Similar to the authentication challenges I faced earlier, this required extensive experimentation to develop a robust solution that could efficiently manage token caching while maintaining security.

This optimization work has significantly enhanced my technical skills in performance tuning and application architecture. While I was already familiar with many of the tools and technologies used, applying them specifically for performance optimization provided new perspectives and deepened my understanding of React's rendering behavior and Next.js's middleware capabilities.

Experiencing to analyze performance metrics, identifying bottlenecks, and implementing targeted solutions has taught me to balance immediate technical improvements with long-term architectural goals. These take-aways will be valuable in both my professional career and future projects, particularly in creating applications that are not only functionally complete but also performant, maintainable and reliable.