Boosting Revenue with Next.js v13+: A Thorough Analysis of Migrating to App Router (Part 1)
John Kaufmann
Director of Software Engineering @ Econify | Full-stack development, team leadership
In this series of articles we’re going to be examining the business case for using Next.js app router.
Migrating to newer technologies, even in the same metaframework, can be a giant pain in the hard drive. The first step, which is where most of these types of initiatives die, is getting buy-in from the business.
This conversation usually approximates to the following questions:
“How much is it going to cost me in time and resources? and how much is it going to make me in the near and long term?”
This is an engineer's worst nightmare. As an engineer myself, you can ask me about the performance upgrades, or the developer experience, or the underlying language paradigms, anything but to connect it to a business case.
That’s why I wrote this article, to connect the two worlds of engineering and business together so that we could all enjoy Next.js App Router together.
At the end of this article hopefully you’ll understand:
The Technical Highlights
Let's go through some highlights of App router and React Server Components (RSC) which we'll map to some business cases later.
Less Client-Side JavaScript
The main benefit with React Server Components (RSC), in my opinion, is minimizing client side JS and rehydration. This improves web vital metrics like the First Contentful Pain(t) (FCP), the initial size of the page resources, and other core web vitals.
React Server Components allow you to write UI that can be rendered and optionally cached on the server. In Next.js, the rendering work is further split by route segments to enable streaming and partial rendering, and there are three different server rendering strategies:
RSC in Next.js allows UI rendering and caching on the server, reducing the need for client-side JavaScript or SSR. This minimizes the need for client side javascript to hydrate the page and decreases the time needed by the browser to get the user an interactive page.
Because the App Router is built with React Server Component’s in mind, we see overall less Javascript. From the blog announcement for Next 13.1:
The?app?directory now includes?9.3kB?less client-side JavaScript than the?pages?directory. This baseline doesn't increase whether you add 1 or 1000 Server Components to your application. The React runtime is temporarily slightly larger, the increase is caused by the React Server Components runtime, which handles mechanics that Next.js previously handled. We are working to reduce this further.
Interaction to Next Paint Improvements
On March 12, 2024, INP will?officially replace First Input Delay (FID)?as the third?Core Web Vital. From the site’s that I’ve surveyed, this is going to be a problem for those on Next.js 12 and lower. A major reason for this is the large client javascript bundle and hydration processes across the page.
This article, discussing RSC’s impact on Interaction to Next Paint (INP), makes a great point:
领英推荐
Keep in mind that, by default, JavaScript is single-threaded. If you’re loading a large JS script, nothing else can happen on your page until the main thread is idle—even?reacting to a user’s click on a plain HTML link.
The other thing that has gotten improved with the App router is the infamous window.__**NEXT_DATA__** hydration object that gets loaded through the DOM and then sits in memory. We’ve had instances on our performance monitoring platform where that object has ballooned to be multiple MB’s large. On this page alone, the window.__NEXT_DATA__ object is 136kb large. That object needs to be fetched, parsed, loaded and accessed on the page, which all contributes to main thread time.
Component Level Caching
Next.js now allows for granular control over caching strategies, ranging from server fetches to individual components, and even entire pages. This level of control enables developers to target requests that are large, frequent, or both, and cache them for near-instantaneous access. This translates to quicker page loads, faster interactivity, and increases in both experience and Core Web Vital metrics. You can see the extend of this level of caching available by the image below:
This level of detail can come with complexity. But, fine-tuned cache control can lead to significant performance improvements, reducing data fetching time and improving overall page rendering efficiency. The fastest site you can serve is going to be static, with these updates we’re isolating dynamic rendering requirements down to the component level, while maintaining traditional caching strategies such as a CDN.
Bundling Optimization
Next.js 13 and on ships with the Next.js compiler which has it’s own performance benefits to both local development performance, build time, and actual bundle.
For those build times (locally and production):
Compilation using the Next.js Compiler is 17x faster than Babel and enabled by default since Next.js version 12. If you have an existing Babel configuration or are using?unsupported features, your application will opt-out of the Next.js Compiler and continue using Babel.
And then for performance improvements, the Next.js compiler has this built-in bundling improvements. From the Next.js blog:
Selective Hydration and Streaming
In the Pages router we have Lazy loading which could be accomplished by either use of Dynamic imports or React.lazy() and Suspense directly. Both accomplish the same thing which is Suspense which is essentially code splitting with a fallback. Using this pattern you can selective hydrate the page meaning you can decide which components to load in with the initial bundle. A great use case for this would be something like only loading elements important to SEO or above the fold on initial load and lazy load everything else. This often looks like a skeleton component while the real component is loading from the server.
Below is an example of what this might look like.
import dynamic from 'next/dynamic'
const DynamicHeader = dynamic(() => import('../components/header'), {
loading: () => <p>Loading...</p>,
})
export default function Home() {
return <DynamicHeader />
}
In Next.js App router, Suspense is a first class citizen. Just like the caching strategy, it’s baked into the router and can then be enabled with fine-grained control on a component basis. The entire router architecture in Next.js 13/14 is designed for streaming and supports Suspense, allowing for a more dynamic and progressive delivery of UI components from the server to the client. On the Next.js site itself this selective hydration strategy led to some major improvements:
We were able to reduce the?Total Blocking Time (TBT)?of?nextjs.org?from 430ms to 80ms using selective hydration with Suspense while validating changes with Lighthouse (”lab” metrics).
The Business Case for Performance
Next up, we're going to review the business case for features like we reviewed above and tie some business value and case studies to this content.
Director of Software Engineering @ Econify | Full-stack development, team leadership
1 年I’m going to be posting the article over the next few days in pieces. If anyone wants the full thing in a PDF just reach out.