UPDATE: Makerkit's now ships with Next.js 15 by default. π
It's time to upgrade to Next.js 15! π
I have been busy preparing the migration to Next.js 15, and I am excited to announce that Makerkit is now fully compatible with Next.js 15 and the React 19 Compiler.
In this post, we will cover the changes that you need to make to your project to upgrade to Next.js 15, and how Makerkit will approach the migration.
What is Next.js 15?
Next.js 15 is a major release that includes several new features and improvements. It's the culmination of App Router's evolution, and it's a significant step forward in the evolution of Next.js.
Here are some of the key changes in Next.js 15:
- Improved Turbopack: In Next.js 15, Turbopack will be blazingly fast by default. This means that you will see a significant improvement in the performance of your application when using Turbopack π
- React 19: Next.js 15 uses React 19 by default, a major update to React that includes several new features and improvements, such as the React Compiler
- Caching updates: caching behavior will now be less aggressive by default. It's now more opt-in and configurable. The
fetch
function is no longer cached by default, which is a very pleasant change. - Improved Stacktraces: Hydration errors will now much easier to debug. This has been a long-standing issue, and it's a great improvement.
- Partial Pre-Rendering (PPR): Next.js 15 introduces a new feature called Partial Pre-Rendering (PPR), which allows you to serve the static parts of your application while still server-side rendering the dynamic parts. This will significantly speed up your Vercel-deployed applications.
- Server Functions: Next.js renames Server Actions to Server Functions, hinting at possible future improvements (such as adding more HTTP methods).
What are the Next.js 15 changes?
Let's take a look at the changes that will be made in Next.js 15.
Turbopack got faster in Next.js 15
Turbopack is the new Rust-based bundler that powers Next.js 15. It's the Webpack successor meant to be faster and easier to debug.
While already available in Next.js 14, Turbopack's performance was not the leap forward we had hoped for. According to my tests, it is now a lot faster than the previous version, and we can finally see the benefits of the new bundler.
In my tests, most pages compile in under 1 second, and live reload takes less than 100 milliseconds. While page compilation could be faster, the live reload is extremely satisfactory.
Profiling Turbopack
When it comes to the speed of the bundler, Turbopack seems to be improving rapidly. However, extremely large or unoptimized dependencies (such as barrel files) can still cause severe slowdowns. Therefore, make sure to analyze your dependencies and optimize them as much as possible.
To do so, you can enable the profiling feature in Turbopack. Set the environment variable NEXT_TURBOPACK_TRACING
to 1
and run the application. This will generate a trace.log
file in the .next
directory, which you can analyze to identify any performance bottlenecks. I've successfully used this to locate dependencies that were not necessary and removed them.
For more information, refer to the Turbopack documentation.
Hello, React 19
React 19 is almost here, and Next.js 15 will use it by default. While Next.js was already using React's RC under the hood, allowing Server Components and Server Actions, React 19 will finally become the stable version of React.
This is a major milestone for Next.js, and it's a great step forward in the evolution of React. React 19 brings several new features and improvements, such as Server Components, Server Functions, major hydration improvements, and more.
To familiarize yourself with React 19, I recommend checking out the React 19 documentation.
Introducing the React Compiler
The React Compiler is an experimental tool that aims to improve the performance of React applications through automatic code optimization.
It is important to understand that this compiler is not yet ready for production use and should be approached with caution.
One of the key ways the React Compiler improves performance is by automatically memoizing code within components and hooks. Memoization is a technique where the results of expensive function calls are cached, preventing redundant computations and improving performance.
While React already provides memoization APIs like useMemo
, useCallback
, and React.memo
, these require manual implementation and can be difficult to use correctly.
The React Compiler leverages its knowledge of JavaScript and React's rules to perform this memoization automatically. It primarily focuses on two areas for optimization:
- Skipping Cascading Re-renders: When a component's state changes, React typically re-renders that component and all of its children. The compiler analyzes the code and identifies components that don't need to be re-rendered, preventing unnecessary updates.
- Memoizing Expensive Calculations: The compiler can detect and memoize the results of computationally expensive functions within components and hooks, reducing the need to recalculate these values on every render.
However, there are some important considerations and limitations to keep in mind:
- Still Experimental: The React Compiler is still under active development and may contain undiscovered bugs or limitations. Using it in production environments is not recommended at this stage.
- Reliance on Rules of React: The compiler's effectiveness depends on the code adhering to the Rules of React. While it can handle some deviations from these rules, it might not always be able to detect and address all violations, potentially leading to unexpected behavior.
- Limited Memoization Scope: Currently, the React Compiler's memoization capabilities are limited to React components and hooks. It does not memoize every function in the codebase, and memoization is not shared across different components or hooks. This means that an expensive function used in multiple components would still be executed multiple times.
Makerkit will initially keep the compiler disabled, however, we will be gradually introducing it to our projects as we gain more experience with it. You can of course enable it at any time by setting it to true
in your next.config.mjs
file.
export default {
reactStrictMode: true,
experimental: {
reactCompiler: true,
},
};
How to use the React Compiler
To use the React Compiler, you need to enable it in your next.config.mjs
file and set the `
Please check out the React Compiler documentation
Caching Updates in Next.js 15
Next.js App Router's caching implementation was harshly criticized by the community for the inability to opt-out of caching. This is no longer the case in Next.js 15, as it changes to the default behavior.
The fetch
function is no longer monkey-patched by default, which means that you will need to explicitly opt-in to caching. This is a very pleasant change, as it allows you to opt-in to caching only for the parts of your application that need it.
PPR - Partial Pre-Rendering
Partial Prerendering (PPR) is a new rendering model in Next.js that blends static and dynamic rendering within a single route.
With PPR, a static "shell" of the route, including elements like the navbar and product details, is served initially for fast loading. Dynamic components, such as a user's cart or recommendations, are loaded asynchronously and streamed into designated "holes" in the shell, optimising the overall page load time.
This method ensures a quicker initial page load while still allowing for personalised content. Although still experimental, PPR is projected to be a significant advancement in web development, potentially becoming the standard rendering model in the future.
For more information about PPR, refer to the official Partial Pre-Rendering documentation.
How to enable PPR in Next.js 15
You can enable PPR in Next.js 15 by adding the following configuration to your next.config.mjs
file:
Enabling PPR Globally: Source indicates that adding ppr: 'incremental' within the experimental object of the next.config.mjs file enables PPR for the Next.js application.
export default {
// other config options
experimental: {
ppr: 'incremental',
// other experimental options
},
};
Specific Route Configuration: Export the experimental_ppr
constant from a route's file to enable PPR for that route only (or layout):
export const experimental_ppr = true;
This is still an experimental feature, so please be aware that it may change in future versions of Next.js, and may be unstable. Makerkit will be experimenting with this feature and will update this guide as we make progress.
Improved Stacktraces for Hydration Errors
Hydration errors are a common issue in Next.js applications. They can be difficult to debug, and they can be frustrating to users. In Next.js 15, the stacktraces are much easier to understand, and the errors are much more helpful.
Dynamic APIs are now asynchronous
Dynamic APIs are now asynchrononous, Promise-based APIs. This means that you must use the await
keyword to wait for the API to return a value before using it.
Previously, you could use the cookies
, headers
, and searchParams
APIs to access cookies, headers, and search params. These APIs were synchronous, but it's no longer the case.
In Next.js 15, these APIs are now asynchronous in order to support new rendering mdes, such as Dynamic IO. This is a breaking change.
Before, you would use the following code to use cookies in a Server Component:
import { cookies } from 'next/headers';
function MyServerComponent() {
const cookie = cookies().get('my-cookie');
// ...
}
In Next.js 15, you would use the following code:
import { cookies } from 'next/headers';
async function MyServerComponent() {
const cookieStore = await cookies();
const cookie = cookieStore.get('my-cookie');
// ...
}
Similarly, if you were using the headers
API, you would use the following code:
import { headers } from 'next/headers';
function MyServerComponent() {
const header = headers().get('my-header');
// ...
}
In Next.js 15, you would use the following code:
import { headers } from 'next/headers';
async function MyServerComponent() {
const headerStore = await headers();
const header = headerStore.get('my-header');
// ...
}
And if you were using searchParams
or params
, you would use the following code:
interface MyServerComponentProps {
searchParams: {
query?: string;
};
params: {
id?: string;
};
}
function MyServerComponent({ searchParams, params }: MyServerComponentProps) {
const query = searchParams.query;
const id = params.id;
// ...
}
In Next.js 15, you would use the following code:
interface MyServerComponentProps {
searchParams: Promise<{
query?: string;
}>;
params: Promise<{
id?: string;
}>;
}
function MyServerComponent({ searchParams, params }: MyServerComponentProps) {
const query = (await searchParams).query;
const id = (await params).id;
// ...
}
Other dynamic APIs include draftMode
, which is less commonly used.
Goodbye Server Actions, Hello Server Functions
Server Actions are Server-Side endpoints, but with a signicant particularity: we can call them directly as Javascript functions. These are POST endpoints that Next.js can call directly from the browser.
In Next.js 15, they will be renamed to Server Functions, which seems to point to the fact they will be extended to include more HTTP methods.
Next.js Minor Changes
Below are some of the minor changes that we are aware of in Next.js 15.
Can no longer use "ssr" set to "false" in Server Component's dynamic imports
Previously, you could use ssr: false
in a Server Component's dynamic imports to prevent the Server Component from being rendered on the server. This is no longer possible in Next.js 15. The component needs to first be moved into a Client Component.
'use client';
import dynamic from 'next/dynamic';
export const MyComponent = dynamic(() => import('./MyComponent'), {
ssr: false,
});
You can then use the MyComponent
Client Component in your Server Component.
Removed Functions from NextRequest
Previously, NextRequest was extended with functions such as geo
and ip
. These functions were removed in Next.js 15. To use these functions, you need to import them from @vercel/functions
.
import { geolocation, ipAddress } from '@vercel/functions'
export function GET (req: NextRequest) {
const geo = geolocation(req)
const ip = ipAddress(req)
}
In Next.js 15, these functions are removed. To use them, you need to import them from @vercel/functions
.
Previously, accessing properties of NextRequest was done using destructuring or direct access. This was not possible in Next.js 15.
import { NextRequest } from 'next/server'
export function GET (req: NextRequest) {
const { geo, ip: ipAlias, pathname } = req // destructuring
// direct access
req.geo
req.ip
}
In Next.js 15, you need to use the following code:
import { geolocation, ipAddress } from '@vercel/functions'
export function GET (req: NextRequest) {
const geo = geolocation(req)
const ip = ipAddress(req)
}
How Makerkit is approaching the migration
Makerkit's path to using Next.js 15 is currently underway. We are currently in the process of updating the documentation and the codebase to reflect the changes that need to be made - however, it should be a rather straightforward process.
We will be updating the documentation and the codebase as soon as we have a stable release of Next.js 15. We will also be providing a migration guide that will help you migrate your project to Next.js 15 as soon as the stable release is out.
Updated Dynamic APIs
I have updated all the instances using dynamic APIs (such as headers
, cookies
and the route params) to use the new Promise-based APIs. However, for your existing code, you will need to update the code to use the new APIs - as described above.
Reduced unused dependencies
During development, Turbopack does bundle code even though it ends up not being used due to environment variables' settings, that Makerkit uses to allow you to choose which dependency to use (for example, Sentry vs Baselime, or Nodemailer vs Resend, etc.). This is because Turbopack does not know that the code is not used.
To fix this, we use a module alias that uses mock exports of the unused depedency. This reduces the amount of code Turbopack will compile during development, making it snappier as a result.
Experimental Features
I am in the process of testing both the React Compiler (which works flawlessly) and PPR. These will likely be turned on by default once they are stable, however it's highly likely you'll be able to use them in your project without any issues.
Conclusion
Makerkit is ready for the migration to Next.js 15. After reading this post, so will you! π
If you are a Makerkit customer, please pull the latest changes from the repository and update your project to Next.js 15. Follow this guide's instructions to update your existing code - and you're good to go!
Ciao! π