How To Fix NextJS 13's N+1 Problem

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
next js13's react server components is forcing  us to fundamentally rethink how we get data onto   our servers and how we render our pages with  next.js12 we only have one option we had to   use get server side props to get the data all  at one point and then we would move it around   using props and context with async react server  components you can get the data wherever you   want on the page which is awesome but with that  awesome power comes a potential downside of the   N plus one anti-patter so I'm going to show  you what that M plus 1 anti-pattern is how to   avoid it and how to test the performance  of your page let's get right into it a lot of you said you were bored with Pokemon  examples so in this one we're going to use iPhones   so we have a database of iPhones here on this  big list and it's a pretty simple API over here   on port 8080 we have our iPhone API and it gives  us back a list of all of the IDS of our iPhones   and then we can get information about any specific  iPhone by just using slash and then its ID in this   case iPhone one one I actually had one of these  back in the day I think it's up in some Gadget   box up in the attic so this is a classic rest API  you get a list of all the entities on the slash   route and then if you do slash and then the ID you  get all the information about the specific entity   so let's go build out our next app and see a  couple of different alternatives for building   out this application over here on our project  we have the iPhone API directory that has the   API that I just showed you it's an Express API  it statically serves the images plus it has   routes for slash which gives you all of the phones  identifiers and then slash ID which gives you all   the information about a phone so you can see I put  in a 50 millisecond Timeout on both of these and   that gives us a latency of 50 milliseconds when  it comes to this API a really good API we'll get   back to you in say 10 milliseconds a really slow  API probably 100 milliseconds so 50 milliseconds   kind of splits the difference we also have a  tester directory we'll get into that in just   a bit okay let's go build out our next js13  application let's use MPX create next app on   the latest which is going to give us 13 and call  it n plus one because that's what we're looking   at the N plus one anti-pattern and we'll use the  experimental app directory to get the app router   we'll use typescript eslint tailwind and we'll use  the source directory now go into that directory   and run PM PMI to install our dependencies  npm PM Dev to run the app and now that's up   on poor 3000 let's go take a look okay we've  got our basic nest.js landing page looks good   now let's take our slash route and build out our  first version of this iPhone shower so I'm going   to remove most of this I'm going to give ourselves  a three column layout in medium first thing I want   to do is get all our data so we can ensure that we  have a good connection with our API server to do   that I need to turn this into an async function  so that I can use fetch inside of our home   and now I can use fetch to go get our data so I'm  just going to fetch all of the identifiers the IDS   and get those back as an array of strings  and let's just go put that in there as a Json   stringifying and now the next thing we need  to do is start up our API so I'm going to go   create another terminal window and now I'll go  into that iPhone API and I'll just run the index   file and now we're listening on port 8080 rocking  okay let's see what we do now we actually do have   all the data it's just in Black text we could  actually change the text here it'll be white   and now we can see that we have all of our  IDs good start so far now that we know that   we have a good connection to our server let's  go start building out our grid to do that I'm   going to create a new component called Phones  it's going to take a list of identifiers that   we just got and it's going to go and map through  all those now down here I'll just instantiate that looks pretty good looks like a three column  layout that's good let's go now build out   the cards so that it looks good so I'm going  to build out the phone card it's going to be   an async component because it's going to go off  and get the data that it needs to show that car   it takes an identifier and it then runs the  fetch to go get the specific data for that phone   but for the time being I'm  just going to turn this div   and then I'm going to instantiate this phone  card and we can see that we're getting this   red squiggly so what does that red squiggly  mean well that means that we can't use this   jsx component because it's an async component  except that we can because we're next j13 and   it's a react server component so actually we're  just gonna ignore this and to ignore that we're   going to just use the TS expect error and say  that hey if it's giving us an error about being   an async server component yeah we get it it's  not a problem so now let's go get that data and we're gonna go get the localhost 8080 with  the identifier and we're going to coerce that   into an iPhone except that we don't have an  iPhone typescript type so let's go build that   to do that I'm going to create a new types  file right up here at the top of source and put in there a type for our iPhone and  then we can just import this into our page okay now I'm going to bring in the formatting for  our card it's going to have a card component and   then we're going to lay out the rest of the data  the image which is going to be relative to 8080   and then all the information about  the models let's go take a look   okay issue number one now it's telling us that  next image isn't happy with the image location   that we're giving it we're telling it that we  have an image off of localhost 8080 but it's   not configured to show that so let's go configure  it to do that we go over into our next config.js   and then under experimental here we add a  new key called images and we say that we   have a remote pattern of localhost on 8080  and anything under there is fine for images   so we also need to reboot our server and now it looks good it's not quite  right and that's because I use Daisy   UI to put together the card for this  so I'm going to Quick install Daisy UI   and go back over to our terminal I'll  add Daisy UI as a development dependency   and then over in our Tailwind config I'll add it  as a plugin and that's it that's as easy as it is   to install Daisy UI let's run our server again  and now we have a nice looking display awesome   so now we're all set up everything's looking  great but underneath the hood we're actually   making a whole bunch of requests to  the backend server let's take a look   so we're making one request at the top here to go  get our list of IDs and then we are instantiating   in phones a whole bunch of these phone cards  which are in turn making one request each to   the API that's where n plus one comes from this is  n so we're making n request one for each card and   then the plus one is the original request that we  made so if there are 50 iPhones that are making 51   requests we're getting the one to get the list  of iPhones the 50 and then we're making the 50   individual request to get the data for each iPhone  now to understand the performance impact of this   we kind of need to actually have a baseline point  so we can see if different approaches make our   page faster so we need to know how fast our page  is to do that I'm going to use a tool called OHA   now to do this I'm going to actually make the font  size a little bit smaller so it might be hard to   actually see the command that I run but it's  worth it since the output of OHA is really cool   so I'm going to do OHA and then I'm going to  give it a duration of the test in as 10 seconds   so Z 10 seconds and then I'm going to give  it the URL that we want to hit with our test   and that would just be the slash route on  localhost so let's hit return and now yeah   you can see why I made the font size smaller  so we see all these cool graphs in real time   all right so what does it told us well it told  us that on average we are getting a 0.9 second   response from the server so every page comes  in at about 900 milliseconds which is okay   now since we're doing a couple of  different variations on this I want to   keep track of the different performance  numbers so let's go and copy this and then create a new file called performance.md  where we'll keep a record of the different   performance numbers that we get so we'll call  this the slash route and we'll say that we get a   speed of 0.9098 okay not bad  in fact we can preview that and see that we're going to get a nice  little table out of that using all that   markdown formatting now that we have our Baseline  speed it's important to understand that nexjs   is actually doing a lot of caching for us every  time we do a fetch here we're actually not doing   a fetch to our API server except for the first  time next JS is aggressively caching our requests   once we do it once we never do it again so let's  turn off the caching and see how much slower it is   to do that we add options for cash and  we just say that we don't want cash let's do that for both the initial request  the one and then the n so we're going to   turn off caching in both places and  see how we go let's rerun the OHA now we can see that it takes 1.4 seconds to   build the page because we're no longer  caching let's go and store that data and you know what I think we should restore  the caching and then talk about how you could   potentially invalidate that cache so I'm going  to take away the no cash which restores the cash and now if you want to use this caching what you  can do is you can export a constant out of here   called revalidate and what revalidate does is  it says every say 60 seconds in this case and   you can specify that number for yourself  it should go and invalidate the caches   and so now this route is going to invalidate any  caching it has every 60 seconds or on the next   page fetch if it's been longer than 60 Seconds  now let's see if we can do better and to do   that I want to be able to take each iteration  that we have and store it in a different route   so I'm going to call this route n Plus 1. so  we'll create a new directory called M plus one and put into their page.tsx and copy that page and back up in here we'll just  get rid of all this contents so now our original test is on  N plus one let's go take a look awesome and over in our markdown  let's change the slash to n plus one   now you might be saying to yourself hey they've  come out with this new cache function in react   does that help well let's try that out let's  go take page.tsx and create a new directory   called M plus one cached and create a  page.tsx in there and paste in our code and let's just make sure it  works we'll go over to cached   looks like it works well now let's go bring in  cash and now we need to create cached versions   of the phone by ID getter and the phone list  getter so let's go do the phone list version First so now we created an async function  that does exactly what we did before   and returns a list of phones and then we've  wrapped it in a cache so let's go use that and there we go let's do the same  thing for our get phone by ID now we have get phone by ID  it takes an identifier it's   also async does exactly the same  thing and let's try that one out okay let's see if it works  seems to work now let's oh hi it somebody's that same OHA command but this  time I'm going to fire it at M plus one cached   oh wow this is actually significantly worse huh okay this is actually far worse than I've  seen in other tests it's always worse but this   is actually worse than usual comes around  1.89 seconds jeez so let's compare that   to what we had before so we had 1.42 in the  uncashed version and now you've got 1.89 jeez   now why are we seeing this result well fetch is  already cached and what we're doing is wrapping it   in another cache now I've seen to be bad not this  bad this is extraordinarily bad but it's not good   it's a long story short don't wrap fetch in  another cache it's already cached but clearly   I think we've pushed the spot Solutions here about  as far as they're gonna go and my recommendation   at this point would be for us to create a back end  for front end which would give us an API that is   more performant so what do I mean by that well  we have an ability in next.js to create an API   route inside of our application in this case we  have the hello route that gives us hello nextjs   now obviously an API endpoint that tells us  hello next yes it's pretty worthless but what   if we had an API where it gave us all of the  information about all the iPhones in one shot   well we can't ask our API folks to do that for  us but we can create our own route that does that   let's call it iPhones so create a new directory  called iPhones and then in there a route.ts so this is a route Handler as opposed to  a page Handler which should be page.tsx   it will import next response that's going to  give us an ability to quickly return Json and   we'll give the iPhone's API which we'll say  it's going to be on an 8080 now in startup   I'm going to go get all the data I'm going  to make that fetch that iPhone's API that's   going to give us back our list of identifiers  and with those identifiers we can go off and   make all the subsequent requests and build up our  data that we want to send back as our big payload   so we start off with a data array we go through  each one of our identifiers we do that fetch   we go and get the identifier and then we just  push onto that array the data that we got back   and then at the end we return data so now the  output of the complete data promise should be   all the data and it's going to go get it once when  the application starts up or the first time that   that route is fetched now to return it all I  need to do is create the get verb and return   and a weight of the complete data and you might  be saying to yourself wait hold on if you do it   in a weight here of the complete data then isn't  it just going to go and request it every time   no once the promise is resolved it's just going  to return the resolve data over and over and over   again almost instantaneously so let's go and  try this out so I just go to the iPhones route and we can see all the data for all the  phones this is going to make it a lot easier   to create a much more reformed page because  instead of n plus one we're just doing one   so let's try that out let's see if we can make an  aggregated page that uses this data source instead   of the original 8080 data source and see how much  more performant it is so let's take all of our n   plus 1 and then create a new directory in under  app called aggregated and put a page.tsx in there paste in our implementation and now instead of  talking to 8080 we want to talk to 3000 iPhones   and we're not going to get back identifiers  we're going to get back full iPhones now we don't get identifiers here we get iPhones of type iPhone we're going to go iterate through all  those iPhones and I have an iPhone there   now the ID should be something like identifier but  we want to pass on the iPhone as is as a complete   coherent object and honestly we don't even need  an async server component here because this phone   card is just going to go and display the data  that we have so let's remove that and go back   over here to phone card remove the async no longer  need that and instead of identifier we have iPhone and let's get rid of all this and now  we can just change phone to iPhone okay let's try this out wow and there it is it looks good let's give  it a try under OHA and see how it performs oh yeah that came in way faster this time is  coming in at 0.66 seconds let's go see how   that compares so 660 milliseconds versus  909 milliseconds that's a 240 millisecond   savings that is excellent now if you found  this helpful so far can I ask you a favor   we're running a survey right now on blue color  coder to figure out what interests you and if   you can just take the time to fill that in there's  a link in the description right down below that'd   be super helpful it not only helps me figure out  what content you like it also helps me figure out   who you are and that's going to help me connect  to marketers who want to Market on this channel   and the idea here is simple I'm going to be using  advertising dollars from vendors that we all know   in respect and use that to fund doing the basic  research and bringing all of this to you for free   okay let's get back into it and  start talking a little bit about   how this Compares on the client so I want  to bring in this other tool that I created   and this tool is called tester so  let's pack this up a little bit   and I'll create a new terminal and  then I'll go into that tester directory   and now I'm going to run tester and I'm going  to run it on our original n plus one route   and what this tester does is it looks at  The Returned payload in more depth and   what it tells us is that we have 61k of HTML and  that's what it's actually displayed on the page   and then we have another 75k of Json that's  the persisted vdom that initializes the client   and that Json is actually pretty interesting  it's a combination of the Dom elements so if a   react server components that would have all the  class names any keys that are intended for HTML   and then in the case of client components it has  any props that are sent to those client components   now since we don't have any client components  in this case we have zero kilobytes going to   the client now let's try the same thing with  aggregated and what we should see is almost   exactly the same numbers and we do we get 60 KB  73 KB and again no client data because we have no   components that are registered as use client so I  want to take a look and see if we decide to go and   make these cards interactive at all are we going  to pay any kind of performance penalties for that   and this has actually changed since the first  time that I looked at 13.1 the structure and   content of this Json has become a lot more  optimized so that's actually really good   now I want to see if we take these cards and we  turn them into client components how does that   affect what actually gets output from the server  so to do that I'm going to start with aggregated   I'm going to create another  folder called aggregated client   and I'll create our page.tsx and paste in  our code and now where do we want to cut the   client and my thinking is that we should cut  the client at phones so phones here is going   to be a client-side component so let's go and  copy everything into a new components directory   and we'll call this phone client and  paste that in now we don't need the Home don't need this and we're going to  export phones and importantly we're   going to make this a client components we  need to put the client directive in there   and now over in our aggregated client  we can bring in phones from that file now we brought in phones from our phone's client  which is like now a client component let's go see   how this works in the browser okay looks good  let's hit it with OHA and see how it performs okay it looks like it's  doing a touch slower at 0.87 go put that into our performance so a touch slower than M plus one but still  not as bad interesting now let's look at that   tester and see how the structure of the Json  has changed based on this move to a client   all right so let's put our aggregated client  at the top here and then our aggregated down   below and so we can do a comparison the HTML  Remains the Same at about 60.6 makes sense   with the client version there's now a  little bit more Json but interestingly   about 77 of that is all of the data on  the phones that is going to the client now how do I know that's there because if I go  over to our Arc and then I do view page source and   then I do line wrap I can see all the HTML at the  top okay and then I can see all of that iPhone's   aggregate data going to that phones component  which is then Distributing it to each one of   the Interior phone cards and so that's where that  75k of data comes from that all has to go down to   the client so that now those phone cards can be  rendered on the client and be interactive there   as opposed to with the original aggregated where  we didn't have anything on the client so we had   73k of data which was just all the information  about the HTML now in older versions of the app   writer index.js13 this wasn't as optimized as it  is you generally speaking have the 73k for the   HTML plus the 75k for all of the client data and  in fact you might actually have multiple copies of   the client data depending on how you distributed  it with properties so I'm glad to say that next   js's app writer is really improving as we see the  betas come around now one more thing is I want to   show you how to not do data Distribution on the  client so we're going to create another aggregate   version of aggregated client called aggregated  client bad because this is going to be bad   so again we'll start off with aggregated create  a new other folder called aggregated clientbad and into their page.dsx so this time around I'm  only going to make the phone card the client   component so let's copy that we'll call  it phone card bad and what it's going to   do is pretty nuts it's going to take the  list of iPhones as well as an identifier which will be a string and then it's going  to find in that list of iPhones its current   phone but based on that identifier  and then it's going to render that out   and you know what I'll put a bang here  to say that we're always going to find it   and now let's go into our bad and let's remove the  phone card and import it from the bad phone card and now it's telling us well you're not sending  the right data so we need to send it iPhones   as well as the identifier and I gotta get rid of image and I forgot  to make this a client component so let's   go and make our client component and  let's see how bad this is I'm going to   go over here to Arc I'm going to hit  refresh and it doesn't seem too bad   let's give it a try so I'm going to go over to  our OHA and hit it and oh this is not good okay so yeah this is not good we're getting a six  second return time now let's go over and log that so whatever this is doing we don't want to  do it so let's go over here into our tester   and see what exactly is coming out of there  that is bad so if we do aggregated client bad   and we compare it to our regular client   we can see that we're getting 60k of HTML as  we did before we can see that we're getting   six megabytes of Json of which 4.85 megabytes  is the client data so what's happening here is   we've sent iPhones the entire array of iPhones to  each individual phone card and because each one of   those is individually its own use client that  means that it has persisted the props to that   client component which means that we have as many  phones as we have times the total payload coming   in from the aggregated API so that's really  where you have to watch out when you make   that transition from a react server component  in this case phones to a client component in   this case phone card everything that's going to  that client component is going to be persisted in   this JS data that goes to the client which  can get very big if you're not careful and   seriously impact the performance of your server  all right well I'm posting all of the code here   and all of the results up to GitHub you can  go check those out for yourself and try for   yourself to see if you can reproduce all of this  and get these insights for yourself as to what   kind of one request structures you want to use  with your react server components and then as we   transition into client components how you want  to go and send data to those client components   now I want to know from you your thoughts and your  experiences when it comes to next js13 and react   server components and how that all works together  in the comments right down below in meantime if   you enjoyed the video hit that like button if you  really like the video hit the Subscribe button and   click on the bell and you'll be notified the  next time a new blue collar coder comes out
Info
Channel: Jack Herrington
Views: 32,809
Rating: undefined out of 5
Keywords: next 13, nextjs 13, next.js 13, next js 13, nextjs react 18, mui nextjs, vite nextjs, nextjs, next js, next.js, nextjs react, nextjs typescript, next js typescript, nextjs npm, react 18, vite, react, reactjs, react.js, react app, nextjs redux, redux next js, redux next js 13, redux next js ssr, nextjs redux ssr, next js redux, next js 13 redux toolkit, next js redux ssr, nextjs 13 redux, nextjs 13 redux ssr, next js 13 ssr, nextjs ssr, react 18 ssr, react ssr
Id: FfHsIio4aCU
Channel Id: undefined
Length: 27min 51sec (1671 seconds)
Published: Tue Apr 25 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.