JAMstack Origins: A deep dive into the architecture with Next.js

The evolution of frontend JS frameworks has drastically changed the ways users see and interact with a website. Numerous libraries and frameworks have taken birth to push the boundaries of what a website is capable of. Our focus for today is going to be on React which has been the library of choice for a multitude of our projects.

Server-Side Rendered Apps

  • A user requests a website on a browser by entering a URL (Eg: www.cantthinkofaname.com).
  • The server we just spoke of verifies that the request is in order, compiles the HTML and sends it over to the client.
  • The browser receives this ready to display static page and renders it to the user immediately. However, the website is not interactive at this point.
  • The client (browser in this case), then also downloads the JS to enable and allow interactivity upon execution.

Advantages of Server-Side Rendering

  • Ensures proper SEO (Search Engine Optimization) and webpages are indexed correctly.
  • Better performance for smaller sites.
  • Better performance on slow network devices.

Disadvantages

  • This was a time when websites mostly only contained images and text, which is no longer the case. Websites today involve messaging, videos, heavy user interaction all of which mean that the HTML to be generated by the server will be huge, necessitating the server to spend a large time compiling the page resulting in a slow TTFB (Time To First Byte).
  • A busy server also means that it can serve fewer requests.

Client-Side Rendering

Client-side rendering (CSR) means rendering pages directly in the browser using JavaScript. All logic, data fetching, templating and routing are handled on the client rather than the server.

In order to alleviate the problems of a pure server-side rendered app, the solution of client-side rendering became hugely popular especially with the advent of client-side frameworks like Angular, React, etc. The explanations and code examples mentioned in the article references React.

  • A user requests a website on a browser by entering a URL (Eg: www.cantthinkofaname.com)
  • The server responds with a blank HTML file with no content along with the Javascript application.
  • The browser now downloads the Javascript and styles, applies them, and compiles the page before rendering the same.

This method has some significant advantages over a purely server-rendered app. The client is responsible for compiling the page resulting in huge performance gains post initial load as nothing needs to be downloaded from the server again.

Navigation is also noticeably faster courtesy of client-side routing. All of this translates to a lower server load thereby freeing it up to serve more requests.

The convenience of developing CSR apps as well as the smooth user experience and performance benefits it offered resulted in its massive adoption through numerous frameworks as this seemed like the ideal choice for applications that had a fair share of user interactivity. LiftOff was no exception to this trend as React was our library of choice for the numerous projects that we built over the last few years.

In spite of the seemingly huge gains that CSR offers, unfortunately, it is not without tradeoffs. As the browser does all the hard work, this would mean that the initial load can be pretty slow depending on the size and kind of the application. The JS bundle would increase in size as the codebase grows, necessitating aggressive code splitting and lazy-loading to ensure performance.

The performance can also turn out to be unpredictable depending on the capability of the user’s device and the speed of the network over which the website is being requested.

There also needs to be additional considerations when it comes to SEO. Though web crawlers can now crawl JS, there are still limitations and a CSR app involves additional work such as the use of pre-rendering solutions to make it work. Client-side routing can also result in delayed crawling.

A Hybrid Approach

Enter JAMstack

At its core, JAMstack is an architecture that leverages client-side Javascript, prebuilt markup, and APIs to create websites. It encompasses the core principles of pre-rendering and decoupling.

In the case of a JAMstack application, the entire frontend is static HTML pages during the build process. These files are then served directly from a CDN, thereby massively increasing load speeds while reducing complexity.

JAMstack aims at decoupling frontend code from the server code. The server exposes GraphQL or REST endpoints which are used to statically generate the entire app during the build process.

Rendering Limitations

This can be tackled in 2 ways:

  • Saving the new book through an API and subsequently triggering a rebuild upon every new addition. This however is only feasible on a very small scale. A huge application with lots of new books added frequently would be hard-pressed to make such a system work.
  • Render the books client-side and prevent them from getting pre-built. The books will always be fetched upon request and rendered entirely client-side. But this would mean the very goal of pre-rendering is defeated.

We shall see how Next.js solves this problem in a later section.

Static Site Generation with NextJS

Next.js is a React based framework which supports Static Site Generation (SSG) as well as SSR.

It goes well with JAMstack as it allows us to dictate how each page in the app is rendered. Next.js also solves the limitation of expensive rebuilds where changes have to reflect immediately.

React Query is an open source data-fetching library for React which also takes care of caching, synchronizing and updating server state.

Forms of pre-rendering

Next.js supports two forms here: Static Generation and Server-side Rendering. The difference is when it generates the HTML for a page.

  • Static generation: The HTML is generated at build time and will be reused on each request. The page needs to export a function named getStaticProps where the data required to render the page is fetched.
  • Server-side generation: The HTML is generated on each request. The page needs to export a function named getServerSideProps where the data required to render the page is fetched.

Next.js lets us choose the form we like for each page. There is also room to create a hybrid app by using Static generation for the majority of the pages and Server-side Rendering for the ones that need it.

Static Generation is the preferred way owing to performance reasons. Statically generated pages can be cached by CDN with no extra configuration to boost performance.

CSR can also be used along with SSG or SSR meaning that parts of a page can be rendered entirely client-side.

Incremental Static Regeneration

Incremental Static Regeneration

An Example

The site would have two pages (routes):

  • /characters Statically generated list of all characters, along with incremental static re-generation to re-generate the page every 5 minutes.
  • /characters/[id] page which displays a single character in more detail. Also statically generated.

Let us set the / route to redirect to /characters

We start by creating a new Next.js app:

mkdir star-wars && cd star-wars && npx create-next-app .

Characters Page

The getStaticProps function handles data fetching and the incremental re-generation configuration with the revalidate key in the returned object. By setting it to 5 minutes, we ensure that any new characters will reflect at around 5 minutes time.

The corresponding component:

The service below is responsible for fetching the list of characters on the client. The useCharacters hook makes use of the useQuery hook provided by React Query to cache all the characters under a unique key characters. It also caches each individual character under a compound key so that when useCharacterDetail data would already be ready to be rendered while the actual API call happens in the background. A stale time is specified after which the cache is purged and the data is re-fetched. The client is a function that makes the call using axios.

The data from the getStaticProps method is passed as a prop to the page which is then passed as initialData to React Query which bypasses the first fetch and caches this instead as data is already present.

Though this step can be avoided altogether by just using the characters returned by the getStaticProps function, useQuery acts as a fallback on the client-side in case there was an error fetching the data during build time. The prebuilt data is rendered and the client-side initial fetch is skipped if getStaticProps successfully returns the list of characters and the same is provided through the initialData property. If not, the same list is fetched client-side so that the user can still interact with the site as intended. It also helps maintain consistency of usage with other client-side calls.

As the open-source API does not provide a unique id field for each of the characters, we are leveraging a makeshift id using the index property. This is not ideal and should not be done in the real world.

Basic styling is handled via Emotion css props and styled components.

Avatar, List, etc are styled components written to add some minimal flair to the components. We shall skip the details of these as this is outside the scope of our topic.

Individual Character Page

  • getStaticPaths: Responsible for fetching all characters' ids to generate the corresponding pages.
  • getStaticProps: Responsible for fetching the data for a given character. This function will run once for every character id returned by the getStaticPaths function.

fallback is set to true so that if a request was made to a character that wasn't available during the build, it can be incrementally re-generated.

The corresponding component:

Demo

Here’s a lighthouse performance run for the same:

SSG lighthouse result

SSR Demo

Here’s the lighthouse performance run for the SSR version:

SSR lighthouse result

Although the score is still 100 owing to the simplicity of the app, it is clear that there’s room for improvement on the SSR version. The initial server response time was quite large, and the first two frames are empty when we look at the trace. There is also a significant difference in FCP times evident from the trace graphs for both.

CSR Demo

A working demo of this can be found here.

We again see similar lighthouse results to the SSR

CSR Lighthouse result

Despite blazing-fast loads, it can be observed that it isn’t quite as quick as a statically generated site as there is no pre-built HTML to render. The browser has to fetch the data, execute JS and compile the HTML before rendering it. This contributes to a significant dip in performance as the size and complexity of the app grows.

Advantages of JAMstack

  • Scale: Where traditional apps add logic to cache commonly visited views, JAMstack provides this by default. With JAMstack sites, everything can be cached in a content delivery network. With simpler deployments, built-in redundancy, and incredible load capacity
  • Stability: With fewer moving parts between the site and the content, there are fewer points of failure.
  • Portability: JAMstack sites are pre-generated. That means that we can host them from an array of hosting providers and have a greater ability to move them to your preferred host. Any simple static hosting solution should be able to serve a JAMstack site. Few examples of such providers include Vercel, Netlify, etc.

Hosting

Automated Builds

Conclusion

Next.js offers a best of both worlds model that allows the developer to choose between server-side and static generation depending upon the need of any given page. This can always be combined with a client-side rendering for parts of the application, thereby offering an ideal solution for a majority of the apps.

Combined with React Query, this further sweetens the pot, making data fetching, mutations, client-side cache handling dead simple.

Check out the Next.js, Jamstack, and React Query docs in order to learn more.

We are business accelerator working with startups / entrepreneurs in building the product & launching the companies.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store