#Frontend

Transforming the Web Experience: Next.js at Vodafone Greece

Our journey so far 

In today’s digital age, the web is the first point of contact for millions of customers worldwide and at Vodafone Greece, our web application is no exception. With a feature-rich platform powering critical functionalities, our web presence is as much about seamless performance as it is about delivering user-first experiences.

Central to this vision has been our adoption of Next.js, a robust React-based framework that has revolutionized how modern web applications are built. Combining the power of Server-Side Rendering (SSR), Static Site Generation (SSG) and cutting-edge features like Incremental Static Regeneration (ISR), Next.js has been indispensable in helping us achieve our business goals.

One of the primary drivers for choosing Next.js has been its ability to address the unique demands of a telecommunications enterprise like Vodafone. Our web application is not only logic-heavy—requiring the orchestration of complex back-end systems—but also rich in features designed to simplify the customer journey. At the same time, Search Engine Optimization (SEO) remains a non-negotiable priority, ensuring that our services are discoverable to new and returning customers alike. Next.js strikes the perfect balance, enabling us to deliver blazing-fast pages that rank well on search engines while handling intricate user interactions behind the scenes.

But in the fast-moving world of technology, staying still is not an option. Frameworks like Next.js continue to evolve, introducing updates that bring performance boosts, developer experience improvements and cutting-edge capabilities. Each upgrade represents an opportunity to refine our application’s architecture, streamline workflows and deliver even better experiences for our users.

In this article, we take a deep dive into our journey upgrading from Next.js 12 to the latest versions, 13 and 14 and eventually adopting the new App Router. We’ll share the challenges we faced, the solutions we implemented and the tangible results we achieved. For those navigating similar transitions, our hope is to provide valuable insights and inspiration.

Let’s explore how Vodafone Greece embraced change to stay ahead in the ever-evolving landscape of web development.

Migration to Next.js 13

Our journey toward adopting the App Router began with a pivotal step: upgrading from Next.js 12 to Next.js 13. This version marked a significant shift in the framework's architecture, laying the groundwork for innovations like the App Router while introducing a host of other features, including OpenTelemetry, a valuable tool that provides standardized observability, enabling developers to monitor, trace, and debug applications effectively

While the App Router itself remained optional in Next.js 13, this version served as a critical bridge, enabling us to incrementally adopt modern patterns and prepare our codebase for a smoother transition. For a complex, feature-heavy web application like ours, this meant following a structured migration plan, ensuring minimal disruption to our workflows while leveraging the best practices outlined in the official documentation.

Migration steps

To successfully transition from Next.js 12 to 13, we carefully followed the steps outlined in the official documentation. These steps were crucial for ensuring that our application was compatible with the new features while maintaining stability and performance throughout the migration process.

  1. Upgrade Node.js to version 18 
    Next.js 13 requires Node.js 18 as the minimum version. As part of our migration, we updated our Node.js environment to meet this requirement. This step was essential for ensuring compatibility with the latest features and maintaining a stable development and production environment.
     
  2. Upgrade Next.js and dependencies 
    The next step was to update our project to the latest version of Next.js 13, along with related dependencies such as React, TypeScript and ESLint. Keeping dependencies aligned with the latest supported versions is vital to ensure compatibility and unlock the new features of the framework.

    Although we weren't adopting the App Router immediately, we started by familiarizing ourselves with the new app directory structure. This optional step allowed us to experiment with the new file-based routing system and begin planning for a gradual transition.

  3. Optimize images with the new Next.js Image component 
    The updated Image component brought enhancements in configuration and optimization. We reviewed and updated our usage of images across the platform to align with the new API, ensuring a seamless transition and improved performance.
     
  4. Migrate the Link Component 
    One of the breaking changes in Next.js 13 involved the Link component, which no longer requires a child a element for accessibility. To streamline this migration, we utilized the codemod provided by Next.js. The codemod automatically updated our codebase, refactoring the Link usage to align with the new API. This ensured that all navigation elements were compliant, although a little bit of manual intervention was needed. Still, it saved as a lot of time and it significantly reduced errors.
     
  5. Run comprehensive tests
    To ensure the stability of our platform post-upgrade, we conducted extensive testing across all key features. From SSR and API endpoints to client-side rendering, every functionality was tested to prevent regressions and maintain a high-quality user experience before going live.

By methodically following these steps, we were able to unlock the powerful features of Next.js 13 while laying the foundation for a seamless transition to version 14 and eventually to the App Router. However, the road to this achievement was not entirely smooth.

Challenges

The upgraded versions of React and TypeScript introduced breaking changes that rippled through our codebase. This resulted in numerous broken unit tests and a deluge of TypeScript warnings that demanded careful resolution. Many of our utility functions and components had to be refactored to meet the stricter type definitions and some third-party libraries we relied on, were either incompatible or needed their own updates to function properly.

Even after addressing these issues in development, we encountered challenges during our pre-production testing. Debugging these issues required close collaboration between teams, as the interplay between updated dependencies and existing code created subtle, hard-to-trace bugs.

These challenges acted as a reminder that while following the prescribed steps in the official documentation is critical, real-world migrations often require adaptability and a willingness to dive deep into unexpected issues.

Ultimately, these efforts culminated in a successful migration, reinforcing our ability to deliver a modern, performant web application while setting the stage for the next upgrade.

Outcome

The migration to Next.js 13 delivered transformative results, exceeding expectations and marking a milestone in the evolution of Vodafone Greece's web platform.

The most significant achievement was a notable performance boost across the entire website. For the first time since the digital replatforming initiative began—which involved migrating our services to the cloud, decommissioning legacy tools like Oracle WCS and adopting a modern Next.js stack powered by Contentful and AWS— the Vodafone site achieved an average response time of approximately 1.5 seconds, a critical benchmark that greatly enhances the user experience. This improvement also contributed to higher Core Web Vitals scores, a key metric for both user satisfaction and SEO performance. Faster load times and better responsiveness ensure that visitors can seamlessly navigate the platform, whether they're managing their accounts, exploring new plans, or shopping on our e-commerce platform.

In addition to the enhanced user experience, our development workflow also benefited from the migration. We observed a faster development server, allowing developers to iterate more efficiently, while reducing time spent on debugging or waiting for builds. This improved efficiency, translates directly into faster delivery of new features and updates for our customers.

Finally, as part of our Next.js 13 migration, we also integrated OpenTelemetry. A tool for enhanced observability, allowing us to track performance metrics and user interactions. By combining this with Datadog, we gained real-time insights into response times and overall application health. This integration helps us identify and resolve performance bottlenecks quickly, contributing to a faster, more stable platform.

Migration to Next.js 14

With the successful migration to Next.js 13 behind us, the move to version 14 was a logical next step in our roadmap toward fully adopting the App Router. Next.js 14 introduced several enhancements that further streamlined development, improved performance and offered new capabilities to better meet user expectations, including:

  • Better SSG and ISR performance for dynamic content.
  • Enhanced TypeScript support.
  • Advanced performance monitoring capabilities.
  • Optional Turbopack adoption for blazing-fast development bundling with near-instant updates.

Migration steps

The migration from Next.js 13 to 14 was relatively smooth and we didn't encounter many challenges. Compared to the upgrade to Next.js 13, fewer unit tests broke and TypeScript warnings were significantly reduced. However, we still followed the documentation steps to ensure a smooth transition.

This time, the process was much simpler. The only necessary step was updating to the latest versions of React, React-DOM, ESLint, and TypeScript.

Challenges

The migration from Next.js 13 to 14 was relatively smooth and we didn't encounter many challenges. Compared to the upgrade to Next.js 13, fewer unit tests broke and TypeScript warnings were significantly reduced.

The primary challenge we faced was related to OpenTelemetry, which required a few adjustments to ensure smooth integration with our existing monitoring tools. Specifically, we had to update some configuration settings to properly connect with our Datadog instance, allowing us to track performance metrics effectively.

Other than that, the transition to Next.js 14 was pretty straightforward, with no significant roadblocks hindering our progress.

Outcome

The migration to Next.js 14 brought about a more stable performance boost, though not as dramatic as the leap from Next.js 12 to 13. Nonetheless, the performance improvements were sustained and even slightly improved, contributing to an even smoother experience across the platform. With the new features in place and everything running seamlessly after going live, we could confidently rely on this version to safely adopt the App Router at this point.

While the upgrade didn't yield a massive performance jump like the previous one, we were pleased to see continued stability.

By the time you're reading this, the stable version of Next.js 15 is already out and our friends at Vercel are no doubt preparing for the next big thing. But rest assured, we won't be rushing to upgrade to Next.js 15 before fully adopting the App Router. It's all about taking it one step at a time.

Embracing the Future: App Router

The transition to Next.js 13 and 14 laid the groundwork for adopting one of the most transformative and ground-breaking features of the framework up until now: the App Router. Moving to this new routing paradigm is not just an update—it's a strategic leap forward that redefines how we build modern web applications. The App Router brings a wealth of improvements and core changes that promise to elevate both the developer experience and ultimately the end-user product.

At its core, the App Router introduces a new routing system that simplifies how routes are handled in Next.js, allowing for more flexible and intuitive structures. Another change is the introduction of the new Layouts. They promise to bring a fresh approach to organizing UI components, making it easier to share common elements and manage page-level structures. The ability to define routes using the new file-based routing system allows us to build applications more efficiently while keeping the routing logic clear and maintainable.

One of the most significant changes is the shift to Server Components by default, making server-side rendering the norm rather than the exception. This approach ensures faster page loads, reduced JavaScript bundle sizes and ultimately, a better user experience. Coupled with the introduction of new Route Handlers, the App Router makes it easier to organize and manage complex backend logic, resulting in cleaner and more modular code.

Additionally, another innovative feature, Server Actions provide a powerful new way to handle interactions directly within the server context, further improving performance by reducing the amount of client-side JavaScript required. 

Along with these innovations, the App Router offers new caching mechanisms that allow developers to cache not only data but entire pages, or API routes in a much more granular way, drastically improving load times and overall site speed.

A new Metadata API is introduced in the App Router. It allows for more dynamic and customizable page metadata, such as titles, descriptions and other meta tags, without needing to manually set them in the document head. This simplifies the management of SEO and social sharing metadata, improving the overall flexibility and performance of the application.

Data fetching has received huge changes as well. It is now more granular and we have more control over how and when data is retrieved, improving both performance and flexibility. The App Router even supports deduping API calls out of the box! However, our beloved data fetching functions like getServerSideProps, getStaticProps and getStaticPaths are gone!

Finally, the App Router introduces a range of powerful new APIs that unlock even greater capabilities, each designed to streamline the development process and enhance the final product.

Lots and lots of changes!

In summary, adopting the App Router is a game-changer, not only for the technical advancements it introduces but also for the strategic advantages it offers in terms of maintainability, performance, and scalability. As we continue to explore and integrate these new features, the potential for creating modern, high-performance applications seems limitless.

Loads of changes, loads of challenges

While the groundbreaking changes brought by the App Router are undoubtedly exciting, they come with a catch: a plethora of changes means a plethora of work for us. The shift to the App Router isn’t just an upgrade—it’s a rethinking of fundamental aspects of our codebase, requiring substantial refactoring and adaptation.

One of the most immediate challenges is our styling strategy. We currently use Emotion for styling, a CSS-in-JS library, but guess what? CSS-in-JS libraries aren’t compatible with Server Components, the backbone of the App Router. You might be wondering why they don’t work with Server Components. Well, RSCs (short for React Server Components) run exclusively on the server. Once they get rendered, they are sent to the client as serialized HTML and JavaScript. No need for client-side JavaScript to be run. No need for client-side hydration. That’s quite big for performance. Sounds cool right? But libraries like Emotion rely on that client-side JavaScript in order to dynamically inject their styles during runtime. In particular, Emotion uses the document API to handle that. The document API is browser-specific and as you may have noticed by now, RSCs do not have access to those APIs, hence CSS-in-JS can’t work with RSCs. So then, should we just switch to client components to preserve Emotion? Well, that’s quite a bad idea—it totally defeats the purpose of adopting the App Router! Instead, we now need to explore alternatives that work seamlessly with RSCs. This has led us to initiate Proof of Concepts (POCs) for styling solutions like Tailwind CSS and CSS Modules, both of which are fully supported in the new architecture and recommended by Vercel itself. It’s a major undertaking that will influence the maintainability and scalability of our codebase moving forward. Plus, it requires extensive refactoring!

Another major change involves switching from Axios to the native fetch API, a quite necessary step to leverage the App Router’s built-in caching and deduping capabilities. This shift not only simplifies API calls but also changes how we think about data flow in our application. For example, with the native fetch handling caching and deduplication automatically, props between components could become optional. Components can independently request the data they need and no matter how many components in the tree make the same request, only one actual API call is executed. This approach has the potential to greatly simplify data sharing while minimizing unnecessary complexity in component hierarchies.

Additionally, the App Router's recent introduction means that many third-party libraries lack support for it. For instance, we relied on the next-session library to manage user sessions in our logged-in environment. Unfortunately, this library isn’t compatible with the App Router, forcing us to find or build alternative solutions.

While we’re excited about the long-term benefits of these changes, the transition is undeniably a challenging journey, requiring extensive planning, experimentation and reengineering.

App Router Migration steps

The migration process involves several steps and while it may seem like a daunting task, the benefits offered by the App Router make it a worthwhile endeavor. Below, we'll outline the steps we followed to transition from the Pages Router to the App Router.

  1. Creating the app directory 
    The first step in migrating to the App Router is to introduce the new app directory at the root of our project. This directory serves as the foundation for the App Router's structure, housing all routes, layouts and components in a more modular and scalable manner. We began by creating the app directory and gradually moving features and pages from the pages directory into it. This process allowed us to adopt the new routing system and the default Server Components while maintaining backward compatibility during the migration. It's pretty important to note that Next.js allows us to gradually adopt the App Router. Both routers can co-exist, but the App Router takes priority over Pages.
     
  2. Creating the Root Layout 
    The next step in the migration process was setting up the Root Layout, a required server component in the app directory that provides a consistent structure across all pages. This layout allows you to define shared elements such as the html and body tags, headers, footers and other persistent UI components. In the App Router, the Root Layout replaces the functionality previously handled by the _app and _document files in the Pages Router. By centralizing these responsibilities, the new layout system simplifies the setup and enforces a cleaner structure. Establishing the root layout was a foundational step in the migration, setting the stage for adopting other core features of the App Router, like nested layouts and server components, while enhancing performance and scalability.
     
  3. Migrating next/head 
    Moving on with replacing the use of next/head with the new Metadata API. This allowed us to define metadata like titles and meta tags in a structured way, directly within our pages, instead of manually adding elements to the <head> using next/head. This transition streamlined our metadata management and ensured compatibility with the App Router's server-first approach.
     
  4. Migrating routing hooks 
    As part of the migration, we updated our routing hooks to align with the App Router. The new hooks, including useRouter, are now imported from next/navigation instead of next/router. This adjustment allowed us to simplify our navigation logic while ensuring compatibility with the updated system.
     
  5. Migrating pages 
    To migrate pages to the App Router, we started transitioning our existing pages to the new folder structure within the app directory. Each page was moved to its own folder, with a dedicated page.tsx file acting as the entry point. This new structure also enabled the use of colocated page components, providing more modularity and control over shared UI elements. By following this approach, we were able to adopt the App Router's improved organization while maintaining backward compatibility during the migration process. However, this part remains tricky due to the styling challenges we've already discussed—and will explore further shortly.
     
  6. Migrating data fetching methods 
    Migrating data fetching methods involved updating how we handle server-side data fetching in our project. With the App Router, traditional methods like getStaticProps and getServerSideProps are replaced by a more unified and flexible approach using async functions directly within our pages, so we updated our components to fetch data using the new fetch API in Server Components. This shift streamlined data fetching by allowing colocated logic and leveraging features like built-in caching and deduplication. These changes promise to not only simplify our codebase but also improve performance and maintainability.
     
  7. Styling 
    As we mentioned earlier, styling in the App Router posed a significant challenge, as CSS-in-JS libraries like Emotion are not compatible with RSCs. To address this, we began investigating alternative approaches to maintain consistent and scalable styling throughout our codebase. Our team conducted POCs on multiple options, including Tailwind CSS, CSS Modules and Vanilla Extract, to determine the best fit for our needs. Each solution offers its own set of advantages, such as Tailwind's utility-first approach and tree-shaking, CSS Modules' scoped styling and Vanilla Extract's type-safe CSS. This step remains a work in progress, as selecting the optimal strategy will play a critical role in ensuring compatibility with Server Components while meeting our design and performance requirements.

Continuing our journey of innovation

The migration to the App Router has been—and continues to be—a challenging yet rewarding endeavor. It marks a significant step forward in how we build, scale and maintain our application. While there’s still work ahead, we’re genuinely excited about the opportunities this transition brings, as it has the potential to transform not just our development practices but also the experience we deliver to our users.

This migration is just one part of our broader commitment to innovation. Staying up to date with the latest technologies isn’t merely a necessity in today’s fast-paced digital landscape—it’s a chance to refine our skills, streamline our processes and deliver exceptional value to our customers. This journey has underscored the importance of continuous learning, adaptability and collaboration as a team.

At Vodafone, we’re dedicated to leveraging cutting-edge tools and practices to ensure our products are robust, scalable, and future-ready. With every step we take, we become more resilient and better equipped to overcome the challenges that lie ahead. So stay tuned.

Here's to a future powered by innovation!

Loading...