In our previous essay, we introduced you to Next.js, a modern web framework for building React applications. We examined the vision of Next.js, analyzing the set of fundamental concepts and properties of the project and considering the project in its greater context. With this knowledge in mind, we can now try to understand this vision is realized through its architectural elements and relationships, and the principles of its design and evolution.
The Multiple Faces of System Architecture
The architecture of a complex system usually encompasses a multitude of different aspects, ranging from its functional structure, intercommunication protocols to the development and deployment process.
While it might be tempting to capture and address all of these aspects into a single monolithic model, such models are difficult to understand and unlikely to be useful for highlighting the architecture’s key features. They tend to become very complex, operating at multiple abstraction levels at once, and consequently individual stakeholders will struggle to understand the aspects that interests them.
An alternative approach is to instead address the problem from several different directions separately. In this approach, the architecture is comprised of a number of separate but interrelated views, each addressing a distinct aspect of the architecture, collectively representing the entire system architecture.
To establish a common language for discussing system architectures, several authors have proposed frameworks for designing such views. Kruchten1 was first to propose a common set of views, consisting of four reusable views, namely, Logical, Process, Physical and Development. An idea that was later adopted and genericized by IEEE2 into the concept of a viewpoint. They define the principles and language for constructing and analyzing views.
In this post, we will analyze Next.js’ architecture of using the seven core viewpoints proposed by Rozanski et al.3 and depicted below. We will discuss whether these viewpoints are relevant for Next.js and how they apply to the project.
- The Context view considers the broader context of a system, detailing the relationships, dependencies and interactions between the system and its environment. This is important because software does not exist in isolation. This holds true for Next.js as well, which is why we already went into depth into this aspect in our recent post about Next.js’ vision.
- The Functional view of a system characterizes the system’s functional structure, documenting key functional elements and their responsibilities. This forms the foundation of the architectural description, driving the definition of the other views. Undoubtedly, this also forms the basis for the architecture of Next.js.
- The Information view portrays the functional and non-functional properties of information in a system (such as structure, purpose or ownership) and highlights how the information is stored, managed and manipulated in the system. This is only of small importance for Next.js as it is unopinionated, but more importantly unaware of how information is managed throughout the application, leaving it up to the user and only providing generic assistance in the form of data fetching.
- The Concurrency view describes the concurrent structure of the system and details how concurrent execution is coordinated and controlled. Since Node.js is inherently single-threaded4, concurrency management within the Next.js code base is greatly simplified. However, this view is not totally irrelevant for Next.js. For example, several processes of building a Next.js application have been parallelized to improve performance and user productivity. Moreover, anticipating the release of Concurrent Mode and the streaming server renderer in React, Next.js needs to support concurrent renders and thus be careful with global state.
- The Development view addresses the aspects of planning and designing the development environment required to support the system development process. For the Next.js project, this is most certainly a relevant view. Considering that Next.js is open source project and relies on many external contributors, it is key that the development process is clear and transparent.
- The Deployment view characterizes the deployment environment and its interaction with the system during runtime. We already briefly touched upon how Next.js applications can be deployed in our last post . Deployment is an integral process of each Next.js application, given that a Next.js application is of no use if it’s not deployed and we cannot access it.
- The Operational view describes how the system will be operated, administered, and supported when it is running in its production environment. Whether this view is applicable to a Next.js application depends mostly on the particular application. For Next.js itself, this view might not be that interesting as the operational process consists mostly of applying eventual bug fixes and improvements, which we may as well cover in the development view as maintenance component.
Architectural Styles and Patterns
Before we go into depth into the different views of Next.js’ architecture, let’s briefly touch upon the main architectural styles or patterns that have been applied in the code base.
The Next.js project is architecturally organized as a monorepo. That is, its code base is not a single monolith but is instead comprised of several smaller packages that are developed in a single Git repository. We already saw last week that Next.js utilizes Lerna to optimize the multi-package workflow. Monorepos massively simplifies the organization and tooling of a large project such as Next.js, but also allow for atomic commits or large scale refactoring between packages.
Moreover, Next.js is built on the Node.js platform, which
makes heavy use of asynchronous programming due to its single threaded nature.
Due to its reliance on callbacks to implement this pattern, Node.js applications
tend to suffer from what is known as the callback hell5, where
callbacks are nested within callbacks several levels deep making the code
difficult to understand and maintain. Next.js has alleviated this issue by
making use of the
async-await functionality that was introduced in the ES8
standard, which allows developers to “linearize” asynchronous code.
We have analyzed the Next.js repository’s directories and files. In this section this analysis is condensed and explained in an almost neurotically structured way. The goal is that new coming developers and users can use this familiarize themselves with the project.
After analyzing the repository of Next.js we found that every file and directory could be categorized into on of three categories: documentation, testing and actual source code.
examples all three function as documentation
for the developer and the user.
errors contains a collection markdown files. The files are
used to explain the large variety of error messages one might receive. Each
error message has its own file in which is explained what the error is, why it
occurred and possible ways to fix the error.
Developers of Next.js are stimulated to create examples showcasing the features
developed. The results is that in the folder
examples over 200 examples are
committed. These examples vary from the most basic features like the pages
functionality to more advanced examples like progressive rendering.
The documentation in the
docs folder is more traditional. It contains among
other things a getting started guide, a FAQ and an API reference.
As the name suggests the folder
test contains the tests for the project. The
folder is subdivided into sub-folders each for different types of tests, for
instance unit tests and integration tests.
bench contains server-side benchmarks for Next.js. With a few
commands the benchmark can be run to test the performance.
The folder next contains the core components of Next.js.
|bin||The main of next|
|build||Responsible for building next projects. Dependencies like Babel and WebPack are used here.|
|cli||The command line interface commands are implemented in this component|
|client||Client side code is implemented in this component. For example linking, page loading and polyfills.|
|export||The feature to export to a static website is implemented in this package|
|lib||Universal utilities are implemented in lib. For example finding the config and pages folder, constants and checksum checking.|
|next-server||Public API of the next-server. The component contains the source for for example rendering and routing.|
|pages||The pages functionality is implemented here|
|server||Private code for the server. Functionalities like hot-reloading and html escaping are implemented right here.|
|telemetry||Next.js collects by default general telemetry data. This data is collected in a anonymous fashion and can be turned of with a single command [^telemetry]. The package telemetry contains the code for this feature.|
|types||The type definitions for Next.js|
|create-next-app||Production||The folder create-next-app contains the code for the create-next-app command 6. The command to create a fresh new Next.js project.|
|next-mdx||Production||The folder next-mdx contains the code for the optional mdx 7 plug-in, a plug-in for using JSX into Markdown.|
|next-bundle-analyzer||Production||The folder next-bundle-analyzer contains the code for the optional plug-in of the WebPack bundle analyzer 8, to visualize the size of files in an interactive treemap.|
|next-plugin-google-analytics||Alpha||Plug-in for Google Analytics|
|next-plugin-material-ui||Alpha||Plug-in for Material UI|
|next-plugin-sentry||Alpha||Plug-in for Sentry|
When building a Next.js project, with for example
next dev or
next build, a
variety of tasks are performed. Most notable actions taken are that
Secondly, people use a wide variety of different browsers and different versions of those browsers as well. This means that some clients have different functionality in their browsers. To fill some gaps in the functionality polyfills are added to make the project compatible with older browsers.
Thirdly, webpack is used to dynamically process and bundle all source code, dependent styles, assets, and images into static assets.
We also want to take a look at how Next.js is used in reality. How is Next.js distributed and what is needed to deploy a Next.js application? We see that the deployment of Next.js can once again be arranged into two categories:
- The distribution of Next.js framework and related products over package managers.
- The deployment of products/websites created with the Next.js framework.
Distribution of Next.js
The Next.js project consists of a number of NPM packages organized within a directory in the Next.js repository. These packages are distributed via the npm repositories. Next.js publishes all packages for every release done on Github, which results in about 40 updates to the released NPM packages this month alone.
Deploying Websites with Next.js
Next.js is a framework for building websites. To simplify deployment of applications built with Next.js, the company behind it, Vercel (previously ZEIT), offers a platform for easy hosting and deployment of such websites. Additionally, Vercel’s platform offers a significant speedup for Next.js applications by specifically tailoring towards this framework.
Aside from deploying via Vercel, a developer can also choose to host the Next.js application themselves. To host an application yourself, you can just use Node, as a Next.js server can be run like a regular Node server. This mode of deployment lacks the benefits that Vercel’s platform gives (the speedup, ease of deployment, etc.), but it remains flexible and gives companies the possibility to host all their services themselves. In addition to this, Next.js offers an option to export your website as static HTML for even easier deployment.
For this section, we picked a few non-functional aspects from the ISO25000 standard9 to take a look at. We try to see how Next.js introduces features that improve these aspects.
The performance of Next.js over other web frameworks is largely impacted by its main premise: server-side rendering. Server-side rendering means that a bulky server computer handles assembling web pages before they are served. This means that work is taken away from the browser requesting the web page, resulting in a web page that will load a lot faster in comparison to a client-side rendered application. Additionally, Next.js does another optimization called Automatic Static Optimization10, which allows pages to be statically rendered when possible.
A problem currently existing with client-side rendered applications is that they are not easily search-engine optimized11, meaning these pages cannot be searched effectively. Using server-side rendering, Next.js is compatible with the current standard.
Next.js heavily depends on the existing React framework to populate its webpages. This framework is well known in the web development community. As such, it provides solid syntax well-known by those who have used it before. This makes it easier to learn Next.js after having previous experiences with React.
This concludes already the second blog post of our series on the architecture of Next.js. We have learned how distinguish between different aspects of a system architecture by means of viewpoints. Then, we have analyzed for different viewpoints have analyzed whether and how these viewpoints apply to Next.js. Next week, we will put our focus on the quality and architectural integrity of Next.js and asses the technical debt that’s present (if any) in the system.
P. Kruchten, Architectural Blueprints - The “4 + 1” View Model of Software Architecture. IEEE Software 12, 1995 ↩
N. Rozanski and E. Woods, Software systems architecture: working with stakeholders using viewpoints and perspectives. Addison-Wesley, 2012 ↩
Well, technically Node.js now supports multiple threads via the worker_threads module, but shared memory is still rather restricted. ↩