This series is my Next.js study resume, and despite it’s keen to a vanilla Next.js, all the features are applicable with Sitecore SDK. It is similar to the guide I recently wrote about GraphQL and aims to reduce the learning curve for those switching to it from other tech stacks.
- In part 1 we covered some fundamentals of Next.js – rendering strategies along with the nuances of getStaticProps, getStaticPaths, getServerSideProps as well as data fetching.
- In part 2 we spoke about UI-related things coming OOB with Next.js – layouts, styles and fonts powerful features, Image and Script components, and of course – TypeScript.
- In part 3 we went through the nuances of Next.js routing and explained middleware
In this post we are going to talk about pre-going live optimizations sich as caching and reducing bundle size as well as authentication.
Going live consideration
- use caching wherever possible (see below)
- make sure that the server and database are located (deployed) in the same region
- minimize the amount of JavaScript code
- delay loading heavy JS until you actually use it
- make sure logging is configured correctly
- make sure error handling is correct
- configure
500
(server error) and404
(page not found) pages - make sure the application meets the best performance criteria
- run Lighthouse to test performance, best practices, accessibility, and SEO. Use an incognito mode to ensure the results aren’t distorted
- make sure that the features used in your application are supported by modern browsers
- improve performance by using the following:
next/image
and automatic image optimization- automatic font optimization
- script optimization
Caching
Caching reduces response time and the number of requests to external services. Next.js automatically adds caching headers to statics from _next/static
, including JS, CSS, images, and other media.
Cache-Control: public, max-age=31536000, immutable
To revalidate the cache of a page that was previously rendered into static markup, use the revalidate
setting in the getStaticProps
function.
Please note: running the application in development mode using next dev
disables caching:
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Caching headers can also be used in getServerSideProps
and the routing interface for dynamic responses. An example of using stale-while-revalidate
:
// The value is considered fresh and actual for 10 seconds (s-maxage=10). // If the request is repeated within 10 seconds, the previous cached value // considered fresh. If the request is repeated within 59 seconds, // cached value is considered stale, but is still used for rendering // (stale-while-revalidate=59) // The request is then executed in the background and the cache is filled with fresh data. // After updating the page will display the new value export async function getServerSideProps({ req, res }) { res.setHeader( 'Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59' ) return { props: {} } }
Reducing the JavaScript bundle volume/size
To identify what’s included in each JS bundle, you can use the following tools:
- Import Cost – extension for VSCode showing the size of the imported package
- Package Phobia is a service for determining the “cost” of adding a new development dependency to a project (dev dependency)
- Bundle Phobia – a service for determining how much adding a dependency will increase the size of the build
- Webpack Bundle Analyzer – Webpack plugin for visualizing the bundle in the form of an interactive, scalable tree structure
Each file in the pages
directory is allocated into a separate assembly during the next build
command. You can use dynamic import to lazily load components and libraries.
Authentication
Authentication is the process of identifying who a user is, while authorization is the process of determining his permissions (or “authority” in other words), i.e. what the user has access to. Next.js supports several authentication patterns.
Authentication Patterns
Each authentication pattern determines the strategy for obtaining data. Next, you need to select an authentication provider that supports the selected strategy. There are two main authentication patterns:
- using static generation to load state on the server and retrieve user data on the client side
- receiving user data from the server to avoid “flushing” unauthenticated content (in the meaning of switching application states being visible to a user)
Authentication when using static generation
Next.js automatically detects that a page is static if the page does not have blocking methods to retrieve data, such as getServerSideProps
. In this case, the page renders the initial state received from the server and then requests the user’s data on the client side.
One of the advantages of using this pattern is the ability to deliver pages from a global CDN and preload them using next/link
. This results in a reduced Time to Interactive (TTI).
Let’s look at an example of a user profile page. On this page, the template (skeleton) is first rendered, and after executing a request to obtain user data, this data is displayed:
// pages/profile.js import useUser from '../lib/useUser' import Layout from './components/Layout' export default function Profile() { // get user data on the client side const { user } = useUser({ redirectTo: '/login' }) // loading status received from the server if (!user || user.isLoggedIn === false) { return <Layout>Loading...</Layout> } // after the request is completed, user data is return ( <Layout> <h1>Your profile</h1> <pre>{JSON.stringify(user, null, 2)}</pre> </Layout> ) }
Server-side rendering authentication
If a page has an asynchronous getServerSideProps
function, Next.js will render that page on every request using the data from that function.
export async function getServerSideProps(context) { return { props: {} // will get passed down to a component as props } }
Let’s rewrite the above example. If there is a session, the Profile
component will receive the user
prop. Note the absence of a template:
// pages/profile.js import withSession from '../lib/session' import Layout from '../components/Layout' export const getServerSideProps = withSession(async (req, res) => { const user = req.session.get('user') if (!user) { return { redirect: { destination: '/login', permanent: false } } } return { props: { user } } }) export default function Profile({ user }) { // display user data, no loading state required return ( <Layout> <h1>Your profile</h1> <pre>{JSON.stringify(user, null, 2)}</pre> </Layout> ) }
The advantage of this approach is to prevent the display of unauthenticated content before performing a redirect. Тote that requesting user data in getServerSideProps
blocks rendering until the request is resolved. Therefore, to avoid creating bottlenecks and increasing Time to First Byte (TTFB), you should ensure that the authentication service is performing well.
Authentication Providers
Integrating with users database, consider using one of the following solutions:
- next-iron-session – low-level encoded stateless session
- next-auth is a full-fledged authentication system with built-in providers (Google, Facebook, GitHub, and similar), JWT, JWE, email/password, magic links, etc.
- with-passport-and-next-connect – old good node Password also works for this case