NextJS Loading UI and Streaming

Ken Fowler


Fri Oct 13 2023

blog post image

I like seeing healthy web vitals when viewing my web applications dashboard. Web vitals are performance indicators developed by google that help you measure if your web page is delivering an excellent user experience.

Unfortunately, I added a feature that tanked my Largest Contentful Paint score (LCP) and First Contentful Paint (FCP) - these metrics measure loading performance. I went from having a lighthouse report full of healthy green scores to sickly red ones. There was a noticeable delay before I was able to view my home page. I couldn't understand what I had done.

Today I solved that issue! Here's what was wrong and how I fixed it.

I recently added a server component to my homepage that makes a call for some data. This told NextJS that I needed to use SSR (server side rendering) in order to construct the page since each time it's loaded there is potentially new data to be fetched. Since server components must fetch all data they require before they can be rendered this added a significant amount of time to my page load.

I hadn’t run into this problem before because other pages in my application that rely on dynamic data substituted the special loading.js page that NextJS provides when routing. I learned that, under the hood, this loading.js file wraps the output of a routes layout.js component in a suspense boundary. That means your page.js file in a given route will be wrapped in your layout.js and if you add a loading.js file it will also be wrapped in suspense. The loading element you provide will actually be the suspense components fallback argument. Now all children of page will default to this fallback if they cannot be loaded immediately.

What I also learned when digging deeper into this concept is that it's super easy to wrap components in your own suspense boundary. Doing this can allow you to create a suspense boundary anywhere in your component tree so that static content on the page can be immediately served and dynamic content can be loaded in afterwards by way of streaming. Streaming allows you to break down the page's HTML into smaller chunks and progressively send those chunks from the server to the client.

I wrapped my new server component in a suspense boundary and gave it a nice loading skeleton as a fallback. Boom! My FCP score went from 2.44s to 0.49s. My LCP score went from 2.66s to 0.41s. Feels snappy.


If you're building a NextJS app and your initial page load speed takes a hit then ask if a component on that page is a server component that fetches some data. Consider wrapping that component in suspense. Give it a nice fallback ui like a loading spinner or skeleton.


No comments yet.