Transcript

00:00:00(upbeat music)
00:00:02- Okay, hello everyone.
00:00:06My name is Aurora.
00:00:07I'm a web developer from Norway.
00:00:09I work as a consultant at Crane Consulting and also,
00:00:12and I'm actively building with the Next.js app router
00:00:14in my current consultancy project.
00:00:16Today, I'm gonna be teaching you
00:00:18patterns regarding composition, caching, and architecture
00:00:20in modern Next.js that will help you ensure
00:00:22scalability and performance.
00:00:24Let me first refresh the most fundamental concept
00:00:28for this talk, static and dynamic rendering.
00:00:30We encountered them both in the Next.js app router.
00:00:33Static rendering allows us to build faster websites
00:00:36because pre-rendered content can be cached
00:00:39and globally distributed,
00:00:40ensuring users can access it quicker.
00:00:42For example, the Next.js Conf website.
00:00:46Static rendering reduces server load
00:00:47because content does not have to be generated
00:00:49for each user request.
00:00:51Pre-rendered content is also easier
00:00:54for search engine callers to index
00:00:56as the content is already available on page load.
00:00:58Dynamic rendering, on the other hand,
00:01:01allows our application to display real time
00:01:03or frequently updated data.
00:01:05It also enables us to serve personalized content,
00:01:07such as dashboards and user profiles.
00:01:09For example, the Vercel dashboard.
00:01:12With dynamic rendering, we can access information
00:01:14that can only be known at request time.
00:01:16In this case, which user is accessing their dashboard,
00:01:19which is me.
00:01:19There are certain APIs that can cause a page
00:01:23to dynamically render.
00:01:25Usage of the params and search params props
00:01:27that are passed to pages or their equivalent hooks
00:01:30will cause dynamic rendering.
00:01:32However, with params, we can predefine
00:01:34a set of pre-rendered pages using generic static params,
00:01:37and we can also cache the pages
00:01:38as they're being generated by users.
00:01:40Furthermore, reading incoming request cookies and headers
00:01:44will opt the page into dynamic rendering.
00:01:46Unlike with params, though,
00:01:47trying to cache or pre-render anything using headers
00:01:50or cookies will throw errors during build
00:01:52because that information cannot be known ahead of time.
00:01:56Lastly, using fetch with a data cache configuration
00:01:58no store will also force dynamic rendering.
00:02:00So these are the few, there are a few more APIs
00:02:03that can cause dynamic rendering,
00:02:04but these are the ones we most commonly encounter.
00:02:06In previous versions of Next,
00:02:09a page would be rendered either as fully static
00:02:11or fully dynamic.
00:02:13One single dynamic API on a page
00:02:15will opt the whole page into dynamic rendering.
00:02:17For example, doing a simple auth check
00:02:19for the value of a cookie.
00:02:20By utilizing React server components with suspense,
00:02:24we can stream in dynamic content
00:02:26like a personalized welcome banner
00:02:27or recommendations as they become ready
00:02:29and provide only fallbacks with suspense
00:02:31while showing static content like a newsletter.
00:02:34However, once we add in multiple async components
00:02:38on a dynamic page like a feature product,
00:02:40they too would run at request time
00:02:42even though they didn't depend on dynamic APIs.
00:02:45So to avoid blocking the initial page load,
00:02:48we would suspend and stream those components down as well,
00:02:52doing extra work, creating skeletons
00:02:53and worrying about things like human-to-layout shift.
00:02:56However, pages are often a mix of static and dynamic content.
00:03:01For example, an e-commerce app dependent on user information
00:03:04while still containing mostly static data.
00:03:07Being forced to pick between them,
00:03:11between static or dynamic,
00:03:12causes lots of redundant processing on the server
00:03:15on content that never or very rarely changes
00:03:17and isn't optimal for performance.
00:03:19So to solve this problem, at last year's Next.js Conf,
00:03:23the use cache directive was announced.
00:03:26And this year, as we saw on the keynote,
00:03:28it's available in Next.js 16.
00:03:30So with use cache, pages will no longer be forced
00:03:34into either static or dynamic rendering.
00:03:36They can be both.
00:03:37And Next.js no longer has to guess what a page is
00:03:40based on whether it accesses things like params.
00:03:43Everything is dynamic by default
00:03:45and use cache lets us explicitly opt into caching.
00:03:47Use cache enables composable caching.
00:03:51We can mark either a page, a React component,
00:03:53or a function as cacheable.
00:03:55Here, we can actually cache the feature products component
00:03:59because it does not need the request and processing
00:04:01and doesn't use dynamic APIs.
00:04:03And these cache segments can further be pre-rendered
00:04:07and included as a part of the static shell
00:04:09with partial pre-rendering,
00:04:10meaning the feature products is now available on page load
00:04:13and does not need to be streamed.
00:04:14So now that we have this important background knowledge,
00:04:18let's do a demo.
00:04:19An improvement of a code base with common issues
00:04:22often encountered in Next.js apps.
00:04:24These include deep prop drilling,
00:04:25making it hard to maintain a refactor features,
00:04:27redundant client-side JavaScript and large components
00:04:30with multiple responsibilities,
00:04:31and lack of static rendering,
00:04:33leading to additional server costs and degraded performance.
00:04:36So yeah, let's begin.
00:04:37And give me one sec over here.
00:04:50All right, great.
00:04:54So this is a very simple application.
00:04:56It's inspired by an e-commerce platform.
00:04:59And let me do an initial demos here.
00:05:01So I can load this page.
00:05:03I have some content like this feature product.
00:05:06I have feature categories, different product data.
00:05:09There's also this Browse All page over here
00:05:13where I can see all of the products in the platform
00:05:18and page between them.
00:05:20Then we have this About page over here, which is just static.
00:05:24I can also sign in as a user.
00:05:27And that will log me into my user.
00:05:30And also get then personalized content on my dashboard here.
00:05:33Like for example, recommended products
00:05:35or this personalized discounts here.
00:05:38So notice here, there's a pretty good mix.
00:05:42Oh, also one more page that I forgot to show you.
00:05:45The product page, the most important one.
00:05:47Also here, we can see product information
00:05:49and then save it if we like for our user.
00:05:52So notice that there's a pretty good mix here
00:05:54of static and dynamic content in this app because of all
00:05:57of our user dependent features.
00:05:59Let's also have a look at the code, which would be over here.
00:06:05So I'm using the app router here, of course, in Next.js 16.
00:06:08I have all of my different pages, like the About page,
00:06:11the All page, our product page.
00:06:13I also have-- I'm using feature slicing here
00:06:15to keep my app folder clean.
00:06:17I have different components and queries talking
00:06:20to my database with Prisma.
00:06:23So yeah, and I purposely slowed all of this down.
00:06:25So that's why we have this really long loading
00:06:27stage, just so we can easier see what's happening.
00:06:30So the common issues that we wanted to work on here
00:06:33that we actually have in this application
00:06:34was the prop drilling, making it hard to maintain
00:06:37a refactor features, access client-side JavaScript,
00:06:41and lack of static rendering leading to additional server
00:06:44cost integrated performance.
00:06:47So the goal here of the demo is basically
00:06:48just to improve this app with some smart patterns regarding
00:06:53composition, caching, and architecture
00:06:54to fix those common features and make it faster and more
00:06:58scalable and easier to maintain.
00:07:01So let's begin with that.
00:07:02The first issue we want to fix is actually
00:07:04related to prop drilling.
00:07:05And that would be over here in the page.
00:07:10Notice right here, I have this logged in variable
00:07:12at the top here.
00:07:14And you can see I'm passing it down to a couple of components.
00:07:17It's actually been passed multiple levels through
00:07:19into this personal banner.
00:07:20So this is going to be making it hard to reuse things here
00:07:23because we're always having this logged
00:07:25in dependency for our welcome banner.
00:07:28So with server components, the best practice
00:07:30would be to actually push data fetching down
00:07:33into the components that's using this and resolve promises deeper
00:07:36into the tree.
00:07:37And for this to get as authenticated,
00:07:39as long as this is using either fetch or something
00:07:41like React cache, we can duplicate multiple calls
00:07:44of this, and we can just reuse it anywhere
00:07:46we like inside our components.
00:07:48So that would be totally fine to reuse.
00:07:50So now we can actually move this into the personized section
00:07:53here.
00:07:53And we don't-- not going to need this prop anymore.
00:07:57And just put it directly--
00:07:59whoops-- in here.
00:08:01And we're not going to need to pass this anymore.
00:08:04And since we're now moving this asynchronous call
00:08:06into the personized section, we're
00:08:07no longer blocking the page.
00:08:09We can go ahead and suspend this with just a simple suspense
00:08:13here.
00:08:13And we're not going to need this fallback.
00:08:16As for the welcome banner, I suppose
00:08:18we're going to do the same.
00:08:22But trying to use the-- get the logged in variable here
00:08:26or value, it doesn't work, right?
00:08:27Because this is a client component.
00:08:29So we need to solve this a different way.
00:08:30And we're going to do a pretty smart pattern here
00:08:32to solve this.
00:08:33We're actually going to go into the layout
00:08:35and wrap everything here with a auth provider.
00:08:39So I'm just going to put this around my whole app here
00:08:42and get this logged in variable over here.
00:08:45And I definitely don't want to block my whole root layout.
00:08:48Let's go ahead and remove the await here.
00:08:50And just pass this down as a promise into this auth provider.
00:08:55And this can just contain that promise.
00:08:57It can just be chilling there until we're ready to read it.
00:09:01So now we have this set up.
00:09:03That means we can actually go ahead
00:09:05and we'll get rid of this prop, first of all.
00:09:09And we'll get rid of this one drilling down
00:09:11to the personal banner.
00:09:12And we'll get rid of the prop drilling also here
00:09:14or the signature.
00:09:16And now we can use this auth provider
00:09:18to fetch this logged in value locally
00:09:20inside the personal banner with use auth with that provider we
00:09:24just created.
00:09:26And read it with use.
00:09:28So this will actually work kind of like in a way
00:09:30where we need to suspend this while it's resolving.
00:09:33So now I just co-located that little small data fetch
00:09:36inside the personal banner.
00:09:37And I don't have to pass those props around.
00:09:40And while this is resolving, let's just
00:09:41go ahead and suspend this one also with a fallback.
00:09:44And let's just do a general banner over here
00:09:47to avoid any weird cumulative shift.
00:09:51And finally, also get rid of this one.
00:09:53So now this welcome banner is composable.
00:09:58It's reusable.
00:09:59We don't have any weird props or dependencies in the homepage.
00:10:02And since we're able to reuse this so easy,
00:10:05let's actually go ahead and add it also to this browser page
00:10:07over here, which will be here.
00:10:11And I can just go ahead and use it over here
00:10:14without any dependencies.
00:10:15So through these patterns, we're able to maintain
00:10:22good component architecture utilizing
00:10:25React cache, React use, and make our components
00:10:27more usable and composable.
00:10:30All right.
00:10:31Let's tackle the next common challenge,
00:10:34which would be excessive client-side JavaScript
00:10:36and large components with multiple responsibilities.
00:10:40Actually, that's also in the All page here.
00:10:43And again, we'll have to work on this welcome banner.
00:10:46It's currently a client component.
00:10:48And the reason it's a client component
00:10:49is because I have this very simple dismissed state here.
00:10:53I can just click this.
00:10:54It's a nice UI interaction.
00:10:56That's fine.
00:10:57What's not so fine, though, is because of that,
00:10:59I convert this whole component into a client-side component
00:11:03or a client component.
00:11:04And I even use swr to client-side fetch.
00:11:07I have now this API layer here.
00:11:08I don't have type safety anymore in my data.
00:11:11Yeah, this is not necessary.
00:11:12And we're also breaking the separation of concerns here
00:11:14because we're involving UI logic with data.
00:11:18So let's go ahead and use another smart pattern to fix this.
00:11:21It's called the donut pattern.
00:11:23Basically, what I'm going to do is extract this
00:11:25into a client-side wrapper.
00:11:27So let's create a new component here.
00:11:29And let's call it banner container.
00:11:32And this is going to contain our interactive logic
00:11:34with the use client directive.
00:11:37We can create the signature.
00:11:38We can paste everything we just had earlier.
00:11:42And instead of using these banners,
00:11:44I'm just going to slot a prop here, which
00:11:46is going to be the children.
00:11:48So this is why it's called the donut pattern.
00:11:50We're just making this wrapper UI logic around server
00:11:53render content, or it could be server render content.
00:11:56And then since we no longer have this client-side dependency,
00:11:59we can go ahead and remove the use client.
00:12:01We can use our isAuth asynchronous function here
00:12:05instead.
00:12:06We can make this into an async server component.
00:12:09We can even replace client-side fetching
00:12:10with server-side fetching.
00:12:11So let me go ahead and just get the discount data directly here.
00:12:16Discount data.
00:12:18And just utilize our regular mental model
00:12:20like before with type safety.
00:12:24And that means I can also delete this API layer
00:12:26that I don't want to work with anyway.
00:12:29Finally, for the isLoading, we can just
00:12:31export a new welcome banner here with our donut pattern banner
00:12:35container containing server render content.
00:12:38And that means we don't need this isLoading anymore.
00:12:40So we basically refactor this whole thing
00:12:42into a server component and extract a UI logic point.
00:12:46But what is that?
00:12:48It looks like I have another error.
00:12:51So this is actually because of Motion.
00:12:52Do use Motion.
00:12:54It's a really great animation library,
00:12:56but it requires the useClient directive.
00:12:59And again, we don't have to make this useClient just
00:13:02for animation.
00:13:03We can create, again, a donut pattern wrapper
00:13:07and just extract wrappers for these animations.
00:13:10And that means we don't have to convert anything here
00:13:12into client-side.
00:13:14And I'm probably missing something down here.
00:13:17Yep.
00:13:18There we go.
00:13:21So now everything here has been converted to server.
00:13:23We have the same interaction.
00:13:24We still have our interactive logic here,
00:13:26but now we have this one way to fetch data.
00:13:29And we have a lot less client-side JS.
00:13:31Actually, I'm using this donut pattern myself
00:13:38for this UI boundary helper, which looks like this.
00:13:42Do you see that?
00:13:43So this kind of shows, again, what I mean, right?
00:13:45With the donut pattern, we have this client component
00:13:48around a server component.
00:13:49I also marked a lot of my other components
00:13:51with this UI helper here.
00:13:53Also here, I have more server components.
00:13:56Let's go ahead and improve those also,
00:13:59since we're getting pretty good at this by now.
00:14:01They are in the footer.
00:14:03These categories-- I mean, I have this nice component
00:14:06fetching its own data.
00:14:08And I just wanted to add this showMore thing, just
00:14:12in case it gets really long.
00:14:14And with the donut pattern, I can just wrap a showMore component
00:14:16here.
00:14:20And this will contain my UI logic.
00:14:23And it looks like this, right?
00:14:26Pretty cool.
00:14:28And this is now containing the client logic,
00:14:31allowing us to use state.
00:14:33We're using the children count and to array to slice this.
00:14:36And what's so cool here is that these two are now
00:14:38entirely composable, reusable components
00:14:40that work together like this.
00:14:42So this is really the beauty of these patterns
00:14:44that we're learning here.
00:14:45You can use this for anything.
00:14:50I also use it for this modal over here.
00:14:52Yeah, just remember this next time you're considering adding
00:14:54any sort of client logic to your server components.
00:14:59OK, we know the donut pattern.
00:15:01We know how to utilize it to create
00:15:04these composable components and avoid plans.js,
00:15:06so we can move further to the final issue.
00:15:10Let me go ahead and close this again.
00:15:13So that would be with lack of static rendering strategies,
00:15:16right?
00:15:18Looking at my build output, I actually
00:15:20have every single page be a dynamic page here.
00:15:24So that means that whenever I load something here,
00:15:27this is going to be running for every single user.
00:15:29Sorry.
00:15:30Every single user that's open to this
00:15:31is going to get this loading state.
00:15:33It's going to be wasting server costs,
00:15:34making the performance worse.
00:15:36And that means also that something inside my pages
00:15:40is causing dynamic rendering or forcing dynamic rendering
00:15:42for all of my pages.
00:15:45Actually, it's inside my root layout.
00:15:48I don't know if you experienced this.
00:15:51It's over here.
00:15:53In my header, I have this user profile.
00:15:57And this is, of course, using cookies
00:15:58to get the current user, and that means that everything else is
00:16:01also dynamic rendered.
00:16:02Because again, pages could be either dynamic or static,
00:16:05right?
00:16:06This is a pretty common problem and something
00:16:08that has been solved before in previous versions of Next,
00:16:11so let's just see what we might do.
00:16:13One thing we could do is create a route group
00:16:16and split our app into static and dynamic sections that would
00:16:21allow me to extract my About page.
00:16:23I could render this statically.
00:16:25It's OK for some apps, but in my case,
00:16:27the important page is the product page,
00:16:29and this is still dynamic, so not really helpful.
00:16:33How about this strategy?
00:16:35So here I'm creating this request context param encoding
00:16:38a certain state into my URL, and then I
00:16:41can use generate static params to generate all
00:16:44of the different variants of my pages.
00:16:46That would actually, combined with client-side fetching
00:16:49the user data, allow me to get this cached on my product page.
00:16:54Definitely a viable pattern.
00:16:55It's recommended by the Vercel Flags SDK called
00:16:59the precompute pattern, I think.
00:17:00But this is really complex, and I have multiple ways
00:17:03to fetch data.
00:17:04And actually, I don't want to rewrite my whole app into this.
00:17:07So what if we didn't have to do any of those workarounds?
00:17:09What if there was a simpler way?
00:17:12Well, there is.
00:17:14Let's get back to our application again.
00:17:17So we can actually go to the next config
00:17:19and just enable cache components.
00:17:23Oh, nice.
00:17:25OK, and what this does, as you know from the keynote,
00:17:28will actually opt all of our asynchronous calls
00:17:31into request time or dynamic.
00:17:34And it will also give us errors whenever
00:17:35we have some asynchronous call not suspended,
00:17:38and it will give us this use cache directive
00:17:40that we can use to granularly cache either a page, a function,
00:17:44or a component.
00:17:48So yeah, let's go ahead and utilize this.
00:17:51We can begin with the home page here.
00:17:55Let's have a look.
00:17:56So again, I have this mix of static and dynamic content.
00:17:59I have my welcome banner for me, something for you also for me.
00:18:03Let's have a look at that with this UI helper again.
00:18:06So for example, the banner is dynamically rendered
00:18:09with this over here.
00:18:10Whereas I marked this as hybrid rendering because the hero,
00:18:14it's fetching this asynchronous thing
00:18:17and it's going pretty slowly.
00:18:18But it doesn't depend on any sort of user data or dynamic APIs.
00:18:21So that means that everything that is hybrid rendered here
00:18:24can actually be reused across requests and across users.
00:18:27And we can use the use cache directive on that.
00:18:30So let's add the use cache directive here
00:18:33and mark this as cached.
00:18:35And that will allow me to-- whenever I reload this page--
00:18:38I didn't save this.
00:18:43There we go.
00:18:44It will not reload this part because it's cached.
00:18:47It's now static, right?
00:18:49And there's also other related APIs like the cache tag
00:18:55to allow me to type this or validate the specific cache
00:18:58entry granularly or define my revelation period.
00:19:01But for this demo, let's just focus on the plain directive.
00:19:05Now that I have this use cache directive,
00:19:06I can actually remove my suspense boundary around this hero.
00:19:10And that means-- well, what this will do
00:19:13is that partial prerendering can actually
00:19:15go ahead and include this in the statically prerendered shell
00:19:19so that this hero will, in this case,
00:19:21be a part of my build output.
00:19:23Let's do the same thing for everything else
00:19:25on this page that can be shared.
00:19:28For example, I have this feature categories over here.
00:19:31Let's go ahead and do the same there.
00:19:33And add the use cache directive and mark this as cached.
00:19:37Like that.
00:19:39And we can remove the suspense boundary.
00:19:40We're not going to need this anymore.
00:19:43Same for the feature products.
00:19:44Let's add use cache and mark this as cached.
00:19:48Oops.
00:19:50And then remove the suspense boundary.
00:19:52So notice how much complexity I'm just able to remove here.
00:19:55I don't have to worry about my skeletons, my cumulative layout
00:19:57shift that I was doing before.
00:20:00And the page is no longer-- or we no longer
00:20:03have this page-level static versus dynamic limitation.
00:20:07So now when I load this page, you'll
00:20:10see everything here is cached except for this truly user
00:20:14specific content.
00:20:16Right.
00:20:18So that's pretty cool.
00:20:19Let's go to the Browse page and do the same thing over there.
00:20:24Yeah.
00:20:25I already marked all of my boundaries
00:20:26here so you can easily understand what's happening.
00:20:29And I want to at least cache these categories.
00:20:33Looks like I'm getting an error, though.
00:20:37Maybe you recognize this.
00:20:38So it means I have a blocking route.
00:20:40And I'm not using suspense boundary
00:20:42when I should be doing it.
00:20:43Refreshing this, it's true, huh?
00:20:46This is really slow.
00:20:47And it's causing performance issues and bad UX.
00:20:50So this is great.
00:20:51Use cache or cache components is helping
00:20:53me identify my blocking routes.
00:20:55Let's actually see what's happening inside that.
00:20:57So this is the problem, right?
00:20:59I'm fetching these categories top level
00:21:00and I don't have any suspense boundary above it.
00:21:03Basically, we need to make a choice.
00:21:05Either we add a suspense boundary above
00:21:07or we opt into caching.
00:21:09Let's do the simple thing first and just add a loading TSX here.
00:21:12And let's add a loading page over here, some nice skeleton UI.
00:21:20That's pretty good.
00:21:21It resolved the error, but I don't
00:21:23have anything useful happening on this page while I'm waiting.
00:21:25I can't even search.
00:21:27So with cache components, dynamic is like--
00:21:31or static versus dynamic is like a scale.
00:21:33And it's up to us to decide how much
00:21:35static we want in our pages.
00:21:37So let's shift this page more towards static
00:21:40and just delete this loading TSX again.
00:21:43And then utilize the patterns that we were learning earlier
00:21:46to push this data fetch into the component
00:21:48and co-locate it with the UI.
00:21:50So move this down into my responsive category filters
00:21:53here.
00:21:54I have two because responsive design.
00:21:57I can actually go ahead and just add it here.
00:22:01Oops.
00:22:03And import this.
00:22:05I don't need this prompt anymore.
00:22:06Actually, my component is becoming more composable.
00:22:09And instead of suspending it, let's just
00:22:11add the use cache directive.
00:22:14And that should be enough.
00:22:16So notice how I'm being forced to think more about where
00:22:18I'm resolving my promises and actually
00:22:21improving my component architecture through this.
00:22:24I don't need to suspend this.
00:22:25This will just be included in the static shell here.
00:22:28The product list, let me just keep this fresh.
00:22:35So I can reload that every time.
00:22:37Whereas the categories at the bottom,
00:22:40I also want to cache this.
00:22:41So let's go ahead and go to the footer.
00:22:44And since I'm using the donut pattern over here,
00:22:48this can actually be cached even though it's
00:22:50inside this part of the UI that's interactive.
00:22:54So this is totally fine.
00:22:55So that pattern was not only good for composition,
00:22:57but also for caching.
00:22:58I think I have one more error there.
00:23:03Let's see what that is.
00:23:04Still have this error.
00:23:08This is actually because of these search frames.
00:23:10Search frames, as we know, is a dynamic API.
00:23:12I can't cache this.
00:23:13But I can resolve it deeper down to reveal more of my UI
00:23:17and make it static.
00:23:18So let's go ahead and move this down, pass it down
00:23:20as a promise to the product list.
00:23:24We'll make this typed as a promise over here, like that.
00:23:30Let's resolve it inside the product list,
00:23:32use the resolved search parameters over here
00:23:34and over here.
00:23:36And since this is suspended here, the error will be gone.
00:23:40So reloading this, the only thing that's reloading here
00:23:45is just the part that I picked specifically to be dynamic.
00:23:47Everything else can be cached.
00:23:49And that means that I can interact with my banner
00:23:51or even search because that part has been already pre-rendered.
00:23:57All right, let's do the final page here,
00:23:59which is the product page, which is the most difficult
00:24:03and the most important one.
00:24:05It's really bad right now.
00:24:08This is super important for an e-commerce platform,
00:24:10apparently.
00:24:11All right, let's go ahead and fix that one, too.
00:24:15So here I have this product page.
00:24:18Let's start caching just the reusable content here,
00:24:21for example, the product itself.
00:24:23And just add use cache here and mark this as cached.
00:24:27That should be fine.
00:24:28That means we can remove the suspense boundary over here.
00:24:33All right, and this is no longer reloading on every request
00:24:36here, right?
00:24:38For the product details, let's do the same thing.
00:24:40Let's add use cache.
00:24:41Let's mark it as cached and see if that will also work.
00:24:47It did not.
00:24:48Actually, this is a different error.
00:24:49It's telling me that I'm trying to use dynamic APIs
00:24:52inside of this cached segment.
00:24:54And that is true.
00:24:54I'm using the Save Product button, right?
00:24:56That allowed me to click and toggle the saved state.
00:25:00So what do you think we can do with this?
00:25:03We can use the donut pattern again.
00:25:06Actually, we can also slot in dynamic segments
00:25:09into cache segments.
00:25:10So we're interleaving them just like before, but with cache.
00:25:12So this is pretty cool.
00:25:14Let's go ahead and add the children here like that.
00:25:19And this will remove the error.
00:25:21And I can just wrap this around this one dynamic segment
00:25:25of my page here, remove the suspense boundary,
00:25:28and add in just a very small bookmark
00:25:31UI for that one dynamic piece of the page.
00:25:34And let's see how that looks now.
00:25:40So notice how almost the entire UI is available,
00:25:42but I have this one small chunk that is dynamic,
00:25:45and that's fine.
00:25:47Everything else is still there.
00:25:48And let's leave the reviews dynamic
00:25:50because we could keep those fresh.
00:25:53There's still one more error.
00:25:54Let's just quickly tackle that.
00:25:56Again, this is the params.
00:25:58I'm getting help that I need to make a choice either add
00:26:01a loading fallback or cache this.
00:26:04Let's just use generate static params in this case.
00:26:07Kind of depends on your use case and your data set.
00:26:10But for this case, I'm just going
00:26:11to add a couple pre-rendered predefined pages
00:26:14and then just cache the rest as they're generated by users.
00:26:17And this will remove my error here.
00:26:20So I think I'm actually done with my refactor.
00:26:22Let's go ahead and have a look at the deployed version
00:26:25and see what that looks like.
00:26:26So I just deployed this on Vercel.
00:26:27And remember, I purposely slowed down a lot of data fishes here.
00:26:35And still, when I load this page initially,
00:26:39everything is just available already.
00:26:40The only thing here is just those few dynamic segments
00:26:43like the discounts and the for you.
00:26:46Same with the browse all.
00:26:47All of the UI is already available.
00:26:50And for the product itself, it just feels instant.
00:26:54And remember, again, that all of these cache segments
00:26:57will be included with the static shell with partial pre-rendering.
00:27:00And it can be prefetched using the improved prefetching
00:27:03in the new Next 16 client router.
00:27:05So that means that every navigation just--
00:27:08it just feels so fast, right?
00:27:09All right, to summarize, with cache components,
00:27:15there is no more static versus dynamic.
00:27:17And we don't need to be avoiding dynamic APIs
00:27:24or compromising dynamic content.
00:27:28And we can skip these complex hacks and workarounds
00:27:31using multiple data fetching strategies just for that one--
00:27:34this cache hit, as I showed you.
00:27:37So in modern Next.js, dynamic versus static is a scale.
00:27:40And we decide how much static we want in our apps.
00:27:43And as long as we follow certain patterns,
00:27:45we can have one mental model, which
00:27:47is performant, composable, and scalable by default.
00:27:50So let's get back to the slides.
00:27:52So if you're not-- we're not already
00:27:54impressed by the speed of that, this is the Lighthouse score.
00:27:56So I collected some field data with the Vercel Speed Insights.
00:28:00So we have a 100 score on all of the most important pages,
00:28:03the home page, the product page, and the product list,
00:28:05even though they are highly dynamic.
00:28:08So let's just finally summarize the patterns
00:28:10that will ensure scalability and performance in Next.js apps
00:28:13and allow us to take advantage of the latest innovations
00:28:15and get scores like this.
00:28:18So firstly, we can refine our architecture
00:28:20by resolving promises deep in the component tree
00:28:23and fetching data locally inside components using React Cache
00:28:26to do duplicate work.
00:28:28We can avoid excessive prop passing to client components
00:28:30by using context providers combined with React Use.
00:28:35Second, we can compose serving client components
00:28:37using the donut pattern to reduce client-side JavaScript,
00:28:40keep a clear separation of concerns,
00:28:41and allow for component reuse.
00:28:43And this pattern will further enable
00:28:45us to cache our composed server components later.
00:28:50And finally, we can cache and pre-render with use cache
00:28:52either by page, component, or function
00:28:54to eliminate redundant processing,
00:28:56boost performance and SEO, and let partial pre-rendering
00:28:58statically render these segments of the app.
00:29:01And if our content is truly dynamic,
00:29:03we can suspend it with appropriate loading fallbacks.
00:29:07And remember that all of this is connected.
00:29:09So the better your architecture, the easier it is to compose,
00:29:11and the easier it will be to cache and pre-render
00:29:13with the best results.
00:29:15For example, resolving dynamic APIs deep in the tree
00:29:17will allow you to create a bigger partially-printed static shell.
00:29:22And with that, this is the repo of the completed
00:29:24version of the application.
00:29:25There's so many things that I didn't even
00:29:27show in there that you can check out.
00:29:29And you can scan the QR code to find my socials there
00:29:32alongside the repo if you don't want to take
00:29:34a picture and type it in yourself.
00:29:36So yeah, that's it for me.
00:29:37Thank you Next.js Conf for having me here.
00:29:39[MUSIC PLAYING]

Key Takeaway

Modern Next.js 16, with its `use cache` directive and patterns like data co-location and the donut pattern, empowers developers to build highly performant, scalable, and composable applications by intelligently mixing static and dynamic content.

Highlights

Next.js 16 introduces the `use cache` directive, enabling granular and composable caching to mix static and dynamic content on a single page.

The 'donut pattern' is a powerful technique to reduce client-side JavaScript by wrapping server-rendered content with minimal client-side interactive logic.

Refining component architecture by resolving promises deeper in the tree and co-locating data fetching with React Cache improves reusability and maintainability.

The `cache components` feature helps identify blocking routes and encourages developers to implement better suspense boundaries or caching strategies.

Prop drilling can be effectively solved by pushing data fetching closer to the components that use it and utilizing context providers with React Use for client components.

Modern Next.js allows developers to choose the balance between static and dynamic rendering, moving away from the previous 'all or nothing' approach.

Applying these architectural, compositional, and caching patterns can lead to significant performance improvements, including 100 Lighthouse scores on highly dynamic applications.

Timeline

Introduction and Rendering Fundamentals

Aurora introduces the talk on composition, caching, and architecture in modern Next.js, focusing on scalability and performance. She refreshes the concepts of static and dynamic rendering, explaining that static rendering offers faster websites, global distribution, reduced server load, and better SEO, using the Next.js Conf website as an example. Dynamic rendering, conversely, enables real-time, frequently updated, or personalized content, such as the Vercel dashboard. Key APIs like `params`, `search params`, cookies, headers, and `fetch` with `no-store` are highlighted as triggers for dynamic rendering.

Limitations of Previous Next.js Versions

In previous Next.js versions, a single dynamic API call on a page would force the entire page into dynamic rendering, even if most content was static. This 'all or nothing' approach led to redundant server processing for content that rarely changes, impacting performance. While React Server Components with Suspense allowed streaming dynamic content, multiple async components on a dynamic page would still run at request time. This often resulted in extra development work like creating skeletons and managing cumulative layout shifts, especially for applications with a mix of static and user-dependent data.

Introducing the `use cache` Directive

To solve the rigid static/dynamic rendering problem, the `use cache` directive was announced and is now available in Next.js 16. This directive allows pages to be both static and dynamic, eliminating the need for Next.js to guess a page's rendering type. By default, everything is dynamic, and `use cache` explicitly opts components or functions into caching. This enables composable caching, allowing specific React components or functions, like a 'feature products' component, to be pre-rendered and included in the static shell through partial pre-rendering, making content available on page load.

Demo Application and Identified Issues

The speaker introduces a demo e-commerce application, intentionally slowed down to showcase common Next.js issues. The app features a mix of static pages (e.g., About) and dynamic, user-dependent content (e.g., personalized dashboard, product pages). The identified problems in the codebase include deep prop drilling, making features hard to maintain and refactor. Additionally, there is excessive client-side JavaScript and large components with multiple responsibilities, alongside a general lack of static rendering, leading to increased server costs and degraded performance. The goal of the demo is to improve this app using smart patterns for composition, caching, and architecture.

Fixing Prop Drilling with Data Colocation

The first issue addressed is deep prop drilling, specifically with a `loggedIn` variable passed through multiple component levels. The solution involves pushing data fetching down into the components that actually use the data, resolving promises deeper in the component tree. For client components, an `AuthProvider` is created in the root layout to pass the `loggedIn` promise, which is then consumed locally using `useAuth` and `use`. This pattern, combined with React Cache for deduplication, makes components like the `WelcomeBanner` more composable and reusable, eliminating unnecessary prop dependencies and allowing for suspense boundaries.

Reducing Client-Side JS with the Donut Pattern

The speaker tackles excessive client-side JavaScript and large components by introducing the 'donut pattern.' This involves extracting minimal interactive UI logic into a client-side wrapper component, while keeping the core content as a server component. For instance, a `WelcomeBanner` that was entirely client-side due to a simple dismissible state is refactored; its interactive part becomes a client wrapper, and the data fetching and main display become a server component. This approach allows for server-side data fetching with type safety, significantly reduces the client-side bundle, and maintains a clear separation of concerns. The pattern is also applied to other components, including those using animation libraries like Framer Motion.

Addressing Lack of Static Rendering

The demo application initially shows all pages as dynamically rendered, leading to performance issues and increased server costs. This is traced to a `UserProfile` component in the root layout that uses cookies, forcing dynamic rendering across the entire application. The speaker discusses traditional workarounds like creating route groups to split static and dynamic sections, which might work for simple static pages but fails for critical dynamic pages like the product page. Another complex strategy, the 'precompute pattern' involving URL state encoding and `generate static params` with client-side fetching, is also mentioned but deemed too intricate for the current application, highlighting the need for a simpler solution.

Enabling Cache Components and Granular Caching

The speaker enables `cache components` in `next.config.js`, which defaults all asynchronous calls to dynamic and provides the `use cache` directive. This feature also helps identify unsuspended asynchronous calls, guiding developers to optimize. On the homepage, components like `Hero`, `FeatureCategories`, and `FeatureProducts`, which do not rely on dynamic APIs, are marked with `use cache`. This allows these segments to be statically pre-rendered and included in the static shell via partial pre-rendering, eliminating the need for suspense boundaries and skeletons. The result is a page where only truly user-specific content is dynamic, while the rest is instantly available.

Optimizing the Browse Page with Caching and Suspense

On the Browse All page, a blocking route error is identified due to top-level fetching of categories without a suspense boundary. Initially, a `loading.tsx` is added, but it blocks the entire page, limiting interactivity. The preferred solution involves pushing the category data fetching deeper into the `ResponsiveCategoryFilters` component and applying `use cache` to it, making it part of the static shell. For dynamic `search params`, the data is resolved deeper down and passed as a promise to the `ProductList`, which is then suspended. This approach allows most of the UI to be static and interactive, with only the product list dynamically loading based on search parameters, demonstrating the flexibility of the static/dynamic scale.

Improving the Product Page with Donut Pattern and `generate static params`

The critical product page, initially very slow, is optimized by marking the `Product` and `ProductDetails` components with `use cache` for static rendering. An error arises in `ProductDetails` due to the `SaveProductButton` using dynamic APIs, which is resolved by applying the donut pattern: wrapping the dynamic button with a client component while keeping the rest of the `ProductDetails` cached. For dynamic `params`, `generate static params` is used to pre-render a few predefined product pages, with the rest cached as users generate them. This results in an almost entirely available UI, with only small, truly dynamic chunks like the save button and reviews loading separately.

Demo Results and Performance Metrics

The deployed, refactored application on Vercel demonstrates remarkable performance improvements. Despite intentionally slowed data fetches, initial page loads are nearly instantaneous, with only truly user-specific dynamic segments loading separately. Cached segments are included in the static shell via partial pre-rendering and can be prefetched by the Next 16 client router, resulting in extremely fast navigations. The application achieved perfect 100 Lighthouse scores on key pages like the home, product, and product list, as verified by Vercel Speed Insights, showcasing the tangible benefits of the applied patterns.

Summary of Patterns for Scalability and Performance

The speaker summarizes that modern Next.js eliminates the rigid static vs. dynamic distinction, allowing developers to choose the desired level of static content. Key patterns include refining architecture by resolving promises deep in the tree and using React Cache to deduplicate work, avoiding prop drilling with context providers and React Use, and composing server and client components with the donut pattern to reduce client-side JS and enable caching. Finally, `use cache` allows granular caching and partial pre-rendering, with Suspense for truly dynamic content. The better the architecture, the easier it is to compose and cache, leading to performant, composable, and scalable applications by default.

Community Posts

View all posts