Published on

Next.JS 15 Technologies Shaping the Future

Authors
  • avatar
    Name
    Roy Bakker
    Twitter

As I explore the innovations in web development, Next.js 15 emerges as a notable advancement. This version brings a stable foundation suitable for production, integrating key developments from earlier release candidates. I've found that the newly introduced features enhance both performance and user experience. For instance, the @next/codemod CLI allows a seamless upgrade to the latest Next.js and React versions, streamlining the development process.

Next.js 15 also introduces support for React 19, with improvements like hydration error handling and the experimental React Compiler. Features such as the Turbopack, which is now stable, contribute to significant performance enhancements. The release focuses on new APIs, like instrumentation.js for server lifecycle management, and visual aids like the Static Indicator that assist during development. These updates underscore Next.js's commitment to improving both usability and functionality.

Streamlined Updates with @next/codemod CLI

To enhance the process of upgrading to new Next.js versions, the codemod CLI stands as a highly effective tool. Automated code transformations are provided with each significant release of Next.js, facilitating smooth transitions for any breaking changes. The enhanced CLI lets me swiftly update dependencies and guides me through the available transformations.

Using npx @next/codemod@canary upgrade latest enables updates to either stable or prerelease versions of Next.js. Here, the canary tag ensures I am utilizing the most recent enhancements to the tool, while latest indicates the Next.js version being targeted for upgrade. I recommend opting for the canary version as it receives continuous updates based on user feedback, ensuring a seamless upgrade experience.

Asynchronous HTTP Request Handling Changes

In the evolving landscape of Server-Side Rendering, I've recognized the need to optimize rendering processes by eliminating unnecessary waiting times for HTTP requests. Traditionally, a server would hold off on rendering content until a specific request was made, primarily because components often depend on dynamic request-specific data like headers, cookies, or params. To enhance this, it's important to identify when to actually wait for a request and when to process without delay.

Here's a practical illustration of the new asynchronous model using the cookies API:

import { cookies } from 'next/headers'

export async function AdminPanel() {
  const cookieStore = await cookies()
  const token = cookieStore.get('token')
  // Additional code here
}

This shift represents a breaking change to several APIs:

  • cookies
  • headers
  • draftMode
  • params across various files like layout.js, page.js, route.js, and others
  • searchParams in page.js

I advise developers to migrate these APIs to asynchronous operations. A temporary solution allows accessing these APIs synchronously with accompanying warnings. For an efficient migration process, the command npx @next/codemod@canary next-async-request-api can be employed. It partially automates the transition and points to any sections requiring manual adjustments. I recommend consulting the upgraded guide for detailed guidance on adapting to the new structure.

Caching Concepts

Default Behavior for fetch Calls Modified

In the past, fetch calls in Next.js utilized the Web fetch API with a default setting that favored caching, such as using force-cache if no specific cache directive was given. With the release of version 15, the default strategy has shifted to no-store. This adjustment means that unless specified, a fetch request will not store its response in the cache. However, if I wish to continue using caching, I have the option to explicitly set the cache to force-cache within individual fetch requests or adjust the route configuration with options like force-static. This approach allows me to tailor how and when data is cached based on the specific needs of my applications.

Changing Defaults for GET Method Route Handlers

Previously, handlers for GET requests automatically stored responses in the cache, provided there were no dynamic elements involved. With the changes in version 15, this automatic caching is no longer the norm. Handlers using the GET method now require explicit configuration to cache responses, utilizing static options like dynamic='force-static' to regain the previous behavior. Route handlers meant for specific purposes, such as creating sitemaps or dynamic images, maintain their capacity to be static unless modified by dynamic configurations. This change empowers me to take control over what I cache, ensuring that only necessary data is stored.

Modification Impact on Client Router Cache for Page Components

The client-side caching mechanism for Page components has also undergone substantial adjustments. Although the staleTimes configuration option was introduced to offer flexibility in managing cache expiration, its standard implementation now involves a default staleTime of 0 for key navigational sections. As users traverse my application, this setting ensures that the most recent information is always displayed. Yet, some consistent behaviors remain unaffected. Shared layouts still benefit from server-side caching to support reusable content, while navigation actions like using the back or forward buttons preserve user scroll positions by restoring cached data. Additionally, the loading.js file keeps its default cache duration for a short period—from 5 minutes to a defined static stale time—allowing for smoother transitions. These alterations offer a balanced approach, giving me the ability to decide when to leverage the cache to enhance performance without compromising the delivery of fresh content.

Advancements in React 19

Pages Routing in React 18

I observed that in Next.js 15, sophisticated support is provided for using the Pages Router with React 18. This compatibility is great for users who want to leverage improvements in Next.js 15 while keeping React 18. By listening to community feedback, the team has ensured that users can upgrade to the React 19 environment at their own pace. Running both React 18 for the Pages Router and React 19 for the App Router simultaneously is possible, but it's not ideal due to potential issues like unintended behavior and typing mismatches.

Important Note: While it's technically feasible to run both versions in one application, the underlying differences in their APIs and rendering processes might cause discrepancies. Thus, handling upgrades with consideration is advised.

Innovative React Compiler (Trial Phase)

The experimental React Compiler by the React team offers a novel approach by deeply understanding your code, which optimizes it automatically. By decreasing the burden of manual memoization—usually done with hooks like useMemo and useCallback—the compiler aims to make code maintenance more straightforward and less prone to mistakes. Next.js 15 integrates this experimental compiler, enabling developers to use it via a Babel plugin. Unfortunately, incorporating this feature might lead to longer development and build stages. Therefore, I recommend exploring available Next.js configuration options to optimize its use when employing this experimental tool.

Enhanced Hydration Error Communications

Error messages can be quite cumbersome for developers trying to decipher the issues in their applications. In this aspect, I've seen how Next.js 14.1 took initial steps to improve insights into hydration errors. With Next.js 15, this effort is furthered by making these error messages more informative. Now, when a hydration error occurs, the error view not only highlights the originating source code but also provides suggestions for resolving the issue. This improvement in communication helps developers pinpoint what went wrong rapidly and apply effective solutions, enhancing overall development efficiency.

Before and After Visualization

Consider how hydration errors were presented in Next.js 14.1:

Old Hydration Error Message

Now, let's see the evolved version in Next.js 15:

Improved Hydration Error Message

These updates illustrate the commitment to enhancing developer experience through more precise and actionable error feedback. In summary, maintaining backward compatibility and boosting compiler efficiency, along with refining error communication, reflects a thoughtful progression in the React and Next.js ecosystems, preparing developers for a seamless transition to React 19.

Turbopack Development

I'm excited to share that the next dev --turbo feature is fully stable, enhancing the speed and efficiency of development projects significantly. As I've worked on platforms like vercel.com and nextjs.org, the improvements are evident.

Key performance boosts include:

  • 76.7% faster startup time for local servers, ensuring that I can get to work almost immediately.
  • 96.3% faster updates using Fast Refresh, making code changes almost instantaneous.
  • 45.8% faster initial route compilation without cached data, though Turbopack still lacks disk caching.

These enhancements make Turbopack a powerful tool for streamlining my development process, and I'm eager to leverage this in future projects.

Static Path Display

When working with Next.js, I now have access to a Static Path Display during development. This tool helps me distinguish between static and dynamic routes, providing a visual signal that simplifies the process of enhancing performance by showing how each page is processed.

Additionally, I can utilize the output from next build to assess the rendering strategy across all routes, providing insight into how my application behaves. This feature is a part of our broader initiative to boost the visibility within Next.js, aiding me in monitoring and refining my applications with greater accuracy. Exciting developments for specialized developer tools are on the horizon.

Executing Code After a Response with Post-Response Functionality (Trial Phase)

In web development, servers usually focus on responding to client requests promptly. However, I often encounter situations where secondary tasks such as logging, analytics, or synchronizing with external systems are necessary. These activities should be carried out without delaying the user’s experience.

One of the challenges arises when utilizing serverless functions, as they halt any ongoing computation once a response is returned. To address this, there's an experimental API known as after(), which allows these auxiliary tasks to be executed after the server has finished streaming the main response. This prevents interruptions in the user-facing response.

Activating this feature involves modifying the next.config.js file as follows:

const nextConfig = {
  experimental: {
    after: true,
  },
}
export default nextConfig

Once enabled, I can incorporate the function within Server Components, Server Actions, Route Handlers, or Middleware. Here's an example demonstrating its use:

import { unstable_after as after } from 'next/server'
import { log } from '@/app/utils'

export default function Layout({ children }) {
  // Secondary task
  after(() => {
    log()
  })

  // Primary task
  return <>{children}</>
}

This method efficiently manages background tasks, ensuring they don't impede the immediate user experience.

Monitoring with instrumentation.js

The instrumentation.js file, featuring the register() function, is now stable and allows me to monitor the performance and behavior of the Next.js server. I can use it to detect performance issues and pinpoint errors through integration with observability tools like OpenTelemetry. This enhancement means that the experimental.instrumentationHook configuration is no longer needed.

Furthermore, collaboration with Sentry has led to the development of an onRequestError hook. This tool captures important context for server-thrown errors, covering areas such as:

  • Router: Includes both Pages Router and App Router
  • Server Context: Encompasses Server Components, Server Actions, Route Handlers, or Middleware

The errors can then be sent directly to the observability provider of choice. Here’s an example of how the onRequestError function might be implemented:

export async function onRequestError(err, request, context) {
  await fetch('https://...', {
    method: 'POST',
    body: JSON.stringify({ message: err.message, request, context }),
    headers: { 'Content-Type': 'application/json' },
  })
}

export async function register() {
  // Initialize your chosen observability provider SDK
}

<Form> Element

The new <Form> element extends the basic HTML <form> by offering features like prefetching, client-side navigation, and progressive enhancement. Its use is ideal for forms that redirect to different pages, such as search forms that lead to result pages. Here's an example of how you can implement it:

import Form from 'next/form'

export default function Page() {
  return (
    <Form action="/search">
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  )
}

Some of the advantages include:

  • Prefetching: This allows the layout and loading interface to be prefetched, ensuring rapid navigation.
  • Client-side Navigation: It preserves shared layouts and maintains client-side state upon submission.
  • Progressive Enhancement: The form remains functional through full-page navigation even if JavaScript has not yet loaded.

These additions reduce the need for manual setup previously required for similar functionality.

Support for Configuration Files Using next.config.ts

In my work with Next.js, I've discovered it now supports TypeScript configuration through the next.config.ts file type. This feature offers a NextConfig type for improved autocomplete and type-safe options. Here's a simple example:

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // config options here
}

export default nextConfig

This allows me to manage configurations effectively by leveraging TypeScript’s strong typing capabilities.

Enhancements for Custom Hosting

In managing self-hosted applications, controlling Cache-Control directives becomes crucial. I've implemented adjustments to improve handling strategies, especially for stale-while-revalidate periods on ISR pages.

Key Improvements Include:

  • Configurable Expiration: I now offer the ability to set the expireTime value directly in next.config. This functionality replaces the previously used experimental.swrDelta setting. The default value is now updated to one year. This change supports better alignment with CDN capabilities in applying stale-while-revalidate effectively.
  • Custom Cache-Control Support: By not overriding custom Cache-Control configurations with default values, I provide users with complete control and compatibility with any CDN setup.

Image Optimization has also been enhanced significantly. In earlier versions, installing sharp was necessary for optimizing images on a Next.js server. Now, with Next.js 15, there's no need for this extra step. The sharp library is automatically utilized when using running commands like next start or when in standalone output mode, making production setups more streamlined and efficient.

These improvements help streamline deployment and management in self-hosted environments.

Improved Protection for Server Functions

Server functions are executed on the server and can be directly invoked by the client. By using the 'use server' directive, these functions are defined at the top of a file, making it straightforward to export them.

While such functions can be utilized effectively, they can unintentionally remain accessible as HTTP endpoints, even if not directly imported. This could potentially expose them to unauthorized access.

Security Enhancements

To address these concerns, several improvements have been made:

  • Dead Code Elimination: Unused server functions are stripped away from the client-side JavaScript bundle. This not only enhances security by preventing unintentional exposure but also optimizes performance by reducing bundle size.
  • Secure Action Identifiers: The framework now generates complex, non-predictable identifiers for server functions, allowing clients to call them securely. These IDs are recalculated with each build to maintain heightened security.

Here's a brief example:

// File: app/actions.js

'use server'

// This function is exported securely for client use.
export async function updateUserAction(formData) {
  // Server-side logic here
}

// This function is unused and excluded from the build.
export async function deleteUserAction(formData) {
  // Server-side logic here
}

These improvements underline the importance of treating server functions like any other public HTTP endpoints. By implementing these measures, I can ensure that my client-server interactions remain secure and efficient.

Improving the Bundling of External Packages

Optimizing the bundling of external packages is key for enhancing your application's cold start performance. In the App Router, the default behavior is to bundle external packages, but there's flexibility to exclude specific ones using the serverExternalPackages configuration.

Conversely, the Pages Router does not automatically bundle external packages. You can manually specify which packages to include with the transpilePackages option. To streamline and align configurations across the routers, a new option bundlePagesRouterDependencies has been added, providing automatic bundling akin to the App Router. If necessary, specific packages can still be opted out using serverExternalPackages.

const nextConfig = {
  // Automatically bundle external packages in the Pages Router
  bundlePagesRouterDependencies: true,

  // Opt specific packages out of bundling for both App and Pages Router
  serverExternalPackages: ['package-name'],
}

export default nextConfig

This setup empowers me to efficiently manage dependencies for optimal performance.

Support for ESLint 9

With the release of Next.js 15, there's new support for ESLint 9, as ESLint 8 reached its end of life on October 5, 2024. This means you have the option to use either version 8 or 9, thanks to the backwards compatibility I've ensured. If you decide to upgrade to ESLint 9, and I notice that you haven't moved to the updated config format, I'll automatically apply the ESLINT_USE_FLAT_CONFIG=false escape hatch for a smoother transition.

While running next lint, it’s important to be aware that older configurations, such as —ext and —ignore-path, have been deprecated and removed. These configurations are gradually being phased out, and it's worth initiating your migration soon since ESLint 10 will fully remove support for these as well.

In addition to these updates, the eslint-plugin-react-hooks has been upgraded to v5.0.0, which includes new rules for React Hooks usage. For detailed changes, you might want to explore the changelog for the latest version of the plugin.

Enhancements in Development and Build Efficiency

Real-Time Adjustments for Server Components

When I'm working in development, server components need to be executed again whenever changes are made. This typically involves calling fetch requests to API endpoints or external services. To enhance my development efficiency and manage API call expenses, I've ensured that Hot Module Replacement can utilize fetch responses from earlier renders.

// Example of using fetch in a server component
export default async function handler(req, res) {
  // Fetch data
  const data = await fetch('https://api.example.com/data')
  // Return fetched data
  res.status(200).json(await data.json())
}

Enhanced Static Content Generation for Routing

Improvements have been made to how static content is generated, especially benefiting pages hindered by slow network responses. Initially, the optimization process required pages to be rendered twice: once for generating data for client-side navigation and again to render the initial HTML. By reusing the initial render, I've streamlined the process, effectively cutting down the workload and improving build times.

Moreover, the fetch cache is now shared across pages within static generation workers. If a fetch call allows caching, its results can be utilized by other pages managed by the same worker. This significantly lowers the number of repeated requests for identical data.

Experimental Control over Static Content Creation

For specialized scenarios that need advanced management, I've introduced experimental controls over static content creation. Although sticking to the standard settings is advised, these new options can be beneficial for certain needs. Keep in mind that they can increase resource consumption and might cause out-of-memory issues due to heightened concurrency.

const nextConfig = {
  experimental: {
    staticGenerationRetryCount: 1, // Retry failed page generations
    staticGenerationMaxConcurrency: 8, // Max pages per worker
    staticGenerationMinPagesPerWorker: 25, // Min pages before starting new export worker
  },
}

export default nextConfig

These adjustments in development and build processes simplify my workflow while providing the flexibility needed when managing diverse projects.

Additional Modifications

In the landscape of web development, specific changes often become crucial elements for developers to consider. A notable shift involves image optimization, where the previous dependency squoosh is replaced by sharp. This transition not only streamlines image processing efficiency but also aligns with modern standards.

When addressing security and functionality, certain strategies have been rethought. For instance, the modification in default headers for image content now sets Content-Disposition to attachment, enhancing control over content delivery.

To keep pace with advances in JavaScript frameworks, support for older syntax and external packages has been pruned. One example is the removal of support for external @next/font packages, underscoring a move towards a more consolidated system that prioritizes internal consistency.

Caching policies also see a major adjustment. Now, specifying force-dynamic applies a no-store strategy by default in fetching cache settings. This provides developers with a more predictable model for data retrieval, catering to applications where data freshness is critical.

The removal of auto-instrumentation for Speed Insights marks a shift toward using dedicated packages like @vercel/speed-insights. This change could require developers to reassess their performance monitoring strategies.

Adjustments also extend to sitemap management. The .xml extension for dynamic sitemap routes is dropped, unifying URLs between development and production environments.

Middleware changes necessitate a closer look too. By restricting certain React API imports with a react-server condition, developers benefit from a safeguard against unintentional API use that’s unsuitable for server environments.

Let's touch on some user navigation enhancements. Error handling during rendering now features built-in safeguards. For instance, calling specific functions like revalidateTag and revalidatePath during render results in an error, aiming to prevent misuse.

Significant code infrastructure enhancements involve adopting stable configurations such as swcMinify and outputFileTracing. These are now default settings, emphasizing automated optimization while reducing reliance on outdated options.

Server performance gains do not stop there. The App Router now caters to both React 18 and 19, which promises a seamless developer experience across various builds. Additionally, the build process itself is now safeguarded against potential hang-ups, thanks to proactive work management.

Developers leveraging dynamic imports through next/dynamic see a simplification. The removal of certain properties, particularly within server components, enhances clarity and reduces unnecessary code complexity.

Improvements similarly affect the edge runtime. By omitting the worker module condition, developers focus more on the essentials specific to each runtime environment.

Parallel routing capabilities amplify flexibility with unmatched catch-all routes, helping manage parameters elegantly. This is accompanied by tech updates that refine CSS handling, ensuring modular stylesheets interact without unwanted global spills.

An innovation specific to client-side interaction is the client prefetch mechanism, making use of a priority attribute for better navigational efficiency and user experience.

Exploring further, we find that Next.js has enabled the insertion of a NEXT_CACHE_HANDLER_PATH through settings. This tweak grants developers heightened control over caching processes, allowing for an approach synchronized with specific application needs.

Ultimately, staying ahead in today's tech domain requires adapting to such emerging insights and features. By harnessing these robust advancements, I can elevate the quality of application delivery, ensuring smooth performance and an enhanced user journey in the modern digital landscape.