Skip to content

What is a Monorepo and What Are the Advantages for Using It in Your Project?

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Monorepos are very popular in the tech industry, and many large companies like Microsoft and Google use them. But what exactly is a monorepo, and would it work for your project?

In this article, we are going to explore what the monorepo architecture is, and learn about some of the commonly used tools for monorepos.

Table of contents

What is a monorepo?

A monorepo is a single version controlled repository that contains multiple independent projects. Some monorepos might contain just 2 or 3 projects while others will contain hundreds of projects using different technologies. This differs from the polyrepo structure where the projects are spread out amongst multiple repositories. It is also important to note that a monorepo is not the same as a monolith. A monolith has multiple sub-projects inside one giant project while monorepos have multiple independent projects inside one repository.

Unified build process and CI/CD process

In a monorepo structure, all of CI/CD(Continuous integration/Continuous delivery) lives in the same repository. This provides teams with more flexibility, and allows you to deploy your services together. You can also choose to deploy your projects separately.

Increased cross team communication

In a polyrepo structure, you will have multiple teams working on different projects spread across many repositories. Often times these teams will not be aware of the status of other repositories outside of their main one. With a monorepo structure, teams are more aware of the changes being made to the projects used in the repo and might spot issues in another projects.

When all of your projects are under a single repository, it makes it easier to establish a consistent code style and set of guidelines across all projects. Some of the guidelines can include naming conventions, best practices for code reviews, branching policies, etc.

In situations where there are breaking changes in the main branch, a monorepo can help identify where the breaking change came from and that team would be responsible for fixing it. A monorepo can also help teams discuss different versioning strategies for the projects.

Steeper learning curve and issues of inclusivity for newer developers

Inclusivity of all developers on a team is really important for the success and outcome of a software project. When you have many diverse perspectives and levels it will lead to a stronger finished product. But a monorepo structure could be seen as intimidating to novice developers. It might be their first time seeing a list of independent projects within the same repository and they might not know how to best contribute in a meaningful way especially if it is an open source situation. It can also be difficult for newer developers to understand the git history under a monorepo structure. If you are using a tool like git blame,it will have to sift through a lot of unrelated commits just to generate that information for you. For these reasons, it is important for the team to help newer developers through the monorepo structure and create issues where all levels can meaningfully contribute.

A look into the starter.dev monorepo

Let's take a look at a couple of This Dot Labs open source monorepos and understand why the decision was made to use a monorepo.

Please note: We will not be exploring all of the possible tools used for monorepos. If you are interested in exploring more monorepo tools, then please checkout this curated list.

What is starter.dev?

starter.dev makes it easy to setup a new project with zero configuration involved. Once you install the starter.dev package(npm i @this-dot/create-starter), you can run the npx @this-dot/create-starter command and it will provide you with a list of starter kits to choose from. Each starter kit includes testing, linting, code examples and a basic configuration setup.

starter kit example

starter.dev is an open source project that consists of a documentation website repository and a GitHub showcases repository dedicated to all of the different starter kits.

Why did the team choose a monorepo structure?

When it came time to plan out the project, the team had to decide if they wanted to maintain each kit in isolation or create different folders for each kit under the same repository. The reason for using a monorepo structure is to provide the team the ability to build the CLI(command-line interface) with greater ease and allow the team to still build the kits in isolation.

What are Yarn Workspaces?

The documentation website repository uses Yarn Workspaces which allows you to setup multiple packages and use the yarn install command to install them all at once.

In the root directory, we have starters and packages directories. If we look at the starters directory, we will see all of the starter kits. Each starter kit will have its own package.json, basic configuration, README and code examples setup.

starter.dev repo

Inside the root package.json file, you will notice this "workspaces": [ "packages/*"] key.

{
  "name": "starter.dev",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "lint": "yarn workspace website lint && yarn workspace @this-dot/create-starter lint",
    "build:cli": "yarn workspace @this-dot/create-starter build",
    "build:website": "yarn workspace website build",
    "start:cli": "yarn workspace @this-dot/create-starter start",
    "start:website": "yarn workspace website preview",
    "dev:website": "yarn workspace website dev",
    "watch:cli": "yarn workspace @this-dot/create-starter watch"
  },
  "workspaces": [
    "packages/*"
  ]
}

That will tell Yarn to import all of the packages that are listed inside of the packages folder.

Unified CI/CD pipeline with Amplify

The starter.dev GitHub showcases repository has a unified CI/CD and uses Amplify. Inside the root directory, there is an Amplify yaml file where each application has their own set of build commands. Each project listed in the monorepo has its own deployment process setup.

version: 1
applications:
  - appRoot: next-react-query-tailwind
    frontend:
      phases:
        preBuild:
          commands:
            - nvm install --lts=gallium
            - yarn install
        build:
          commands:
            - yarn run build
      artifacts:
        baseDirectory: .next
        files:
          - "**/*"
      cache:
        paths:
          - node_modules/**/*

  - appRoot: angular-apollo-tailwind
    frontend:
      phases:
        preBuild:
          commands:
            - nvm install --lts=gallium
            - yarn install
            - yarn generate
        build:
          commands:
            - node -v
            - yarn run build
      artifacts:
        baseDirectory: dist/starter-dev-angular
        files:
          - "**/*"
      cache:
        paths:
          - node_modules/**/*

How does the team manage issues?

If you look at the issues tab, you will notice that all of the developers are following this same naming convention for issues: [starter kit] title for issue.

This works well for a variety of reasons:

  1. Any developer interested in contributing to the project, can see at a quick glance which issues fit their skill and interest level. starter dev issue tab
  2. It will be easier to track the status of the issues on a project board and see which areas need more attention.

Conclusion

We have explored the monorepo structure and talked about a few features. We have also compared it to other architectures like monoliths and polyrepos to see how if differs.

Then we took an in depth look into starter.dev and how it uses Yarn Workspaces to help organize the project.

I hope your enjoyed this article and best of luck on your programming journey.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Remix Deployment with Architecture cover image

Remix Deployment with Architecture

Intro Today’s article, will give a brief overview of the Architect framework and how to deploy a Remix app. I’ll cover a few different topics, such as what Architect is, why it’s good to use, and the issues I ran into while using it. It is a straightforward process, and I recommend using it with the Grunge Stack offered by Remix. So let’s jump on in and start talking about Architect. Prerequisites There are a few prerequisites and also some basic understandings that are expected going into this. The first is to have a GitHub account, then an AWS account, and finally some basic understanding of how to deploy. I also recommend checking out the *Grunge Stack* here if you run into any issues when we progress further. What is Architect? First off, Architect is a simple framework for Functional Web Apps (FWAs) on AWS. Now you might be wondering, "why Architect?" It offers the best developer experience, works locally, has infrastructure as code, is secured to the least privilege by default, and is open-source. Architect prioritizes speed, smart configurable defaults, and flexible infrastructure. It also allows users to test things and debug locally. It defines a high-level manifest, and turns a complex cloud infrastructure into a build artifact. It complies the manifest into AWS CloudFormation and deploys it. Since it’s open-source, Architect prioritizes a regular release schedule, and backwards compatibility. Remix deployment Deploying Remix with Architect is rather straightforward, and I recommend using the Grunge Stack to do it. First, we’re going to head on over to the terminal and run *npx create-remix --template remix-run/grunge-stack*. That will get you a Remix template with almost everything you need. *Generating remix template* For the next couple of steps, you need to add your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to your repo’s secrets. You’ll need to create the secrets on your AWS account, and then add them to your repo. *AWS secrets* *GitHub secrets* The last steps before deploying include giving CloudFormation a SESSION_SECRET of its own for both staging and production environments. Then you need to add an ARC_APP_SECRET. *Adding env variables* ` With all of that out of the way, you need to run *npx arc deploy*, which will deploy your build to the staging environment. You could also do *npx arc deploy —-dry-run* beforehand to verify everything is fine. Issues I Had in My Own Project Now let's cover some issues I had, and my solution for my own project. So, I had a project far into development, and while looking at the Grunge Stack, I was scratching my head, trying to figure out what was necessary to bring over to my existing project. I had several instances of trial and error as I plugged in the Architect related items. For instance: the arc file, which has the app name, HTTP routes, static assets for S3 buckets, and the AWS configuration. I also had to change things like the *remix.config* file, adding a *server.ts* file, and adding Architect related dependencies in the package.json. During this process, I would get errors about missing files or missing certain function dirs(), and I dumped a good chunk of time into it. So while continuing to get those errors, I concluded that I would follow the Grunge Stack, and their instructions for a new project. Then I would merge my old project with the new project; and that resolved my issues. While not the best way, it did get me going, and did not waste any more time. That resolved my immediate issues of trying to get my Remix app deployed to AWS, but then I had to figure out what got pushed. That was easy, since it created a lambda, API gateway, and bucket based on the items in my arc file. Then I realized, while testing it out live, that my environmental variables hadn’t carried over, so I had to tweak those on AWS. My project also used a GitHub OAuth app. So that needed to be tweaked with the new URL. Then it was up and running. Conclusion Today’s article covered a brief overview of what Architect is, why it’s good to use, how to deploy a Remix app and the issues you might have. I hope it was useful and will give others a good starting point....

Leveling Up Your Work Through Architecture Design and Time Estimation cover image

Leveling Up Your Work Through Architecture Design and Time Estimation

> This post can be useful for developers at any level! However, it is written mostly for entry-level developers who are starting to transition into a more intermediate role. You’re rocking your first development job. You’re completing tasks, the team loves you, you still have tons of questions but you can get answers and build what you’re asked to build. But you want to keep improving your skills and giving yourself more options. Some of that skill can only come from actually building things over time. But surely there’s something you can do to level up besides that, right? There is! One way to help deepen your understanding of the software you’re writing and make yourself look more impressive is by improving your ability to estimate your time, and how you talk about your work. That’s what we’re going to dig into today! Some terminology You might have heard of, or have read terms like “software architecture” and “quality attributes” and, upon trying to look into them, been met with a LOT of jargon and dense terminology. So before we get too far into this, let’s go over a few terms we’ll use. (This is by no means meant to be a complete description of these terms. The aim is to be clear enough to continue our discussion here, and let you get started.) - Software architecture: This term means how your system is organized. It’s like the blueprint for the application or website you’re building. How do all the pieces we build fit together and interact with each other? What’s the main goal we’re trying to achieve with this codebase? That’s what this term is talking about. - Software design: Once we have the blueprint, we need to figure out how to build the pieces to make that structure come to life. This is where the design comes in. It’s more code-specific and focuses more on the specifics of what we need to build and how. What languages are we using? How do we get this component to be interactive or usable like we want? This is all the design. - Software quality attributes: Sometimes referred to as “ilities” because a lot of them end with those letters, these are words we use to describe the software we write. Common ones would be accessibility, reliability, or performance. These are words used to describe some of the key qualities we want our software to have. You can start with this list of quality attributes or do a search for “software quality attributes”, and find all sorts of articles covering a lot of the common ones. - Time estimation: This is the skill of being able to consider a task and estimate how long it would take you to complete the task. Time can be a funny thing, and it’s pretty common to think something will be easy and have it take way longer, or think something will be complex, and it turns out to be straightforward. - Trade-offs: This is the idea that two things can’t be equally balanced and/or important. There’s a joke about how people want things fast, cheap, and good and you can’t have all three. This is the same idea. Very often, by increasing one quality, you have to decrease another. The balance of those, and the choice to focus on one quality more than another, is called a trade-off. You’re trading the strength of one quality for another. When we’re using the phrase “architecture design” in this article, we’re combining all these concepts together. How do we get started? Ok, so we’ve got some terms now. But what do we do with them? How do we get better at designing software, and estimating our time? I’ve got two templates to share with you that can help you start to develop these skills. Both will involve writing, but I’ve tried to make them as straightforward to fill out as possible so it’s not a chore to use them. I’ll share links for the templates later on. But first, we’ll go over the actual contents of both of them and talk about how to use them. These templates can be useful for any task, no matter how complex. However, super simple tasks like fixing a broken link on a page, or adjusting a color value to improve its contrast may not be the best choice for it. It is useful to use this template on a task that you fully understand, so you get some practice with thinking about the tradeoffs you’re making, and get used to some of the terminology and how to talk about the software you build. But this can really help when you start getting larger tasks or features- ones that might contain multiple parts working together or some complex logic to get the task working properly. The architecture design template For this template, the goal is to complete this first before you do any work on your task. Use this as a tool to help you think through your work before you get started on it so you can have a better understanding of what the task is asking of you, and let you start thinking about the attributes you’re building for. There are 7 sections for this document, each with a title and description. Let’s go over each piece to see how they work. Big picture: What’s the ask? *What’s the ticket/task trying to solve? What’s the end goal of this body of work? Describe the problem in the most ELI5 way you can.* Use this section to describe the task you’re working on. Act like you’re talking to someone with very little understanding of the situation, like a project manager on another team or to a friend of yours that doesn’t work with you. This section helps you make sure you have a strong handle on the work you’re being asked to do, and what the end result should be. Requirements *Is there acceptance criteria? A specific way it needs to work? What makes this ticket / task count as complete? Steps to reproduce and/or screenshots are helpful here too, if available.* Some of this can often be copied over from the ticket you’re working on. Make note of how you can tell that this task is complete, and any specifics that you need to make sure work as expected when you’re ready to have someone review it. Constraints *Are there specific things you can / can’t use? Something you have to make sure doesn’t break? Tools you have to use because of where you have to work in the codebase?* Making note of constraints can be super helpful. Maybe you have to use a specific UI library to complete this because the rest of the project uses it. Or perhaps this task has been attempted before, and you don’t want to repeat a version that didn’t work. Getting used to thinking about the constraints you’re working within can help prep your brain to think around these concepts. Architecture and Tradeoffs *Where does this code live? (login, ui, db, etc) What software quality attributes are we focused on for this task? List out the primary and secondary attributes (maybe a third if it feels necessary).* In this section, we’re focusing on developing our architecture and design skills in detail. Talk a little about how the codebase is structured, and where the work for this task will be located. Also, pick one or two software quality attributes that you think fit this task. Does the work you’re doing help improve the site’s overall accessibility? Does it make the application more reliable for end users? Does it help to keep our codebase’s maintainability within a reasonable level? Use one of the lists above or your own searches to build out a list of the attributes you might build for regularly, and pick out one or two of them that relate the most to this task. Then, talk a little about why you picked those traits and your thoughts about why they’re applicable. Being able to explain your reasoning here will both help you gain more understanding of the work you’re doing, and the words you’re using to describe it, and give whoever reads this document a better glimpse into the work you do and how you think about it! Initial Gameplan *Considering all the above, what seems like the path forward? What are the steps for how you think it can be solved/completed? Where are you starting?* Now that you’ve spent a little time thinking through the task, and some of the things you should remember about it (like how it’s structured, the tools you have to use, and what’s important to keep in mind while you’re solving it) - write out the steps you think you should take to accomplish the task. It’s perfectly okay to not fully follow this plan as you start to build! We can never fully predict everything that might happen as we start to work in a codebase. The goal here is to give yourself a solid place to start, with all the details we’ve listed fresh in your mind. Process Notes *As you solve this - keep this area updated with rough notes. What did you try that didn’t work? Why not? If you have to pivot, what did you switch to and why? Maybe something worked but you still pivoted - what tradeoffs were made, and what’s the reasoning behind it?* As the description implies, this space is all for you. Try to keep notes as you go. If you do have to change course from your game plan, why and what did you change? Did you find another problem you didn’t even know existed? Or maybe you were able to try a new concept out and it worked! Celebrate your process here. None of this needs to be in complete sentences or easy for anyone but you to understand. This space is all yours to help you keep thinking through the work as you’re doing it! Final Solution *You did it! Form a narrative. Now that you’ve got the issue solved, what does it look like? How did you do it? What did you learn along the way? Is the final solution the same as what you thought, or did the ask pivot along the way? Share screenshots if available.* The final wrap-up! Try to write this similarly to how you started with the big picture. Did your initial plan end up working, and if not why? Were there any interesting plot twists throughout the process? Share the wins, the struggles, and the end result here. Screenshots can be perfect here to see a visual of your work! I also typically leave a little space at the bottom to wrap up any lessons I learned for myself, and leave a little room to talk about my time estimate and reasonings behind how it turned out the way it did. But those are completely optional. --- Because I use a Notion database to help me keep track of these documents, I also have a few properties I can fill out related to the project this document is for, what my time estimate and result were, and the quality attributes I selected. Having those visible at a glance is super helpful for me. The biggest thing I’d recommend to track with this is the date you filled it out. Having that date to help you organize and find your documents is super helpful as your collection grows. I typically title mine based on the project it’s for and the name of the task, but you can name yours whatever you’d like! Now let’s talk about how to track our time for this task. The time estimate template This one is a lot less detailed, but just as important! Being able to improve your knowledge of how long different tasks take you will be a super handy skill as your career continues. The main idea with this template is to keep track of two sections: how long you think something will take you (the estimate), and how long it truly takes you. Our goal is to get those two sections to be as close to the same number as possible. Remember that our goal here is NOT perfection. That’s impossible to reach. Our goal is simply to get them to be close to each other. Most people (even managers) understand that an estimate is not a guarantee. But the closer you can get your estimates to the true time it takes you, the more reliable you seem and the more accurately your team can make progress. There are three main sections to this template. The actual numbers Most importantly, we want an estimated total time and an actual total time, as well as a calculation of the difference between those two numbers. Is our estimate higher, or is the actual total time higher? That difference is what we’re trying to keep as close to zero as possible. I like to break my time tracking down into categories, so I can also see if particular parts of my work take me longer (or shorter) than I think. For each of these, I do the same thing: make an estimate, and record the actual value. The categories I like to track are: Investigation: time to fill out my architecture design, do a little looking into the task, make sure I understand fully what I’m being asked to do, and that I have all the information I need to complete it. Coding: time to do the actual work. Most of my time calculation goes into this category. Testing: I use this for either writing actual unit or end-to-end tests, or for manual testing. This is where I double-check to make sure I didn’t break anything or track time spent on that one last piece of functionality that doesn’t quite work right. Review: any feedback I get on my Pull Requests or code reviews that require me to make changes go into this category. Space to record the numbers as I go The Pomodoro technique works really well for me with time tracking, though I change up my “working time” numbers to make them easier for me to calculate. I’ve found the most straight forward way for me to do this is to have a title for each section of time that I’m tracking, with space underneath each one. Then, I have a legend of colored dot emojis related to a different amount of time: 15 minutes, 30 minutes, and 60 minutes. Then, as I do my rounds of working time, each time I’m done with a round, I select the right colored dot for the amount of time I did, copy it, and paste it under the section the work I did belongs to. Keep repeating this until the work is complete! Once your task is done, all you need to do is count your dots, and record the total number they represent in your actual time section from above. Here’s a screenshot of what one of my completed sections look like, so you can better see what I mean. From this tracking section, I can quickly see that I spent 15 minutes on both testing and review, and an hour and 45 minutes on coding. If I don’t happen to spend any time on a section (in this instance, I’d done some investigating in a separate ticket, so I already knew the work that needed to be done), I just leave it blank. I have a section at the top of my page for tracking the total number for each category. So I use this area to keep track as I’m doing small rounds of work. Then, when I’m done, I add up each number and put it in the section at the top of the page for that category. Notes I also have a section for notes at the bottom of my document. I keep this area for anything relating to my time tracking. Did something break unexpectedly, causing my estimate to be off? Did I not need a section for some reason? Or did something work way better than I thought it would? Those are the kinds of things worth keeping notes of here. Can you tell why your estimate and actual times were off? Sometimes we simply don’t know, but being able to keep notes on the things we can realize as we’re doing them helps us get better the next time! Tying these together and talking about it with others You can use both of these templates together or on their own, and you’ll gain a great amount of knowledge about your skill level and your growth over time from them! But they can also really shine using them together. While these are great tools for your own personal knowledge and growth, I also highly recommend sharing them with someone else. It’s a great idea to set a goal to be better with estimating your time, and sharing that goal with your manager. Then, you can use the time estimation template to build up some estimates, and share those with your manager. Maybe you have a senior developer on your team that you really respect, and you’d like to get their opinion on the task you’re working on. If you’ve filled out the architecture design for that task, you can share it with them and have a conversation about it. They can potentially provide you with things to consider for your next task, or get you thinking more deeply about the quality attributes you selected, and why they may or may not have been the best fit for your work. The design documents are also great to share with your manager! It’s a great way to show that you’re starting to think about your work on a deeper level and starting to consider the quality and complexity of your work. It can also be helpful reference material for them when promotions and new work becomes available. They’re more likely to think you might be a good pick for the next big thing if you’re already showing them you’re starting to think about things at a higher level! The Notion template links The links for both of these documents as Notion templates are below. Please feel free to duplicate a copy for yourself if you like using Notion, or just take a peek at them to see the actual layout of them and adapt that for whatever tool works best for you. These documents are set up to be used within a Notion database. We won’t cover that in detail here, but the Notion documentation should be able to take care of those details for you (and we have a link to get you started below as well). In general, you can create a database within Notion, and then set a template to use for each new entry. That way, when you go to create a new item, it will automatically pull up these templates for you, so you don’t have to copy and paste them every time! And the fields at the top will be visible when you go to look at your database, which makes it a little nicer to get a quick overview of your documents and the progress you’re making. Now go forth and deepen your knowledge! - Time estimation template - Architecture design template - Notion database templates documentation...

Introducing the Next.js 12 and Chakra UI Starter Kit cover image

Introducing the Next.js 12 and Chakra UI Starter Kit

Next.js is a very popularly used React framework, and to help support developers using Next, This Dot Labs has just created a new starter kit that they can use to bootstrap their next projects. This Next.js 12 starter kit comes with formatting, linting, example components, unit testing and styling with Chakra UI. In this article, we will take a deeper look into what this kit has to offer. Table of Contents - How to initialize a new project - Technologies and tools included with the kit - Next.js v.12 - Chakra UI - Jest - Storybook - ESLint and Prettier - A note about state management - Deployment options - Reasons for using this kit - Conclusion How to initialize a new project 1. Run npm create @this-dot/starter -- --kit next12-chakra-ui or yarn create @this-dot/starter --kit next12-chakra-ui 2. Follow the prompts to select the next12-chakra-ui starter kit, and name your new project. 3. cd into your project directory and run npm install or yarn install . 4. Run npm run dev or yarn run dev to start the development server. 5. Open your browser to http://localhost:3000 to see the included example code running. Technologies and tools included with the kit Next.js v.12 This starter kit uses version 12 of Next.js with the TypeScript configuration. We have also included an example inside the src/pages/api/hello.ts file on how to work with the built-in types for API routes. ` Chakra UI This starter kit uses Chakra UI for all of the styling. We have already setup the ChakraProvider inside the src/pages/_app.tsx file, which includes extended theme objects for colors, font weights, and breakpoints that you can customize to your liking. ` You can take a look at any of the example components inside of the src/components folder on how to best use Chakra's components. Jest This starter kit uses the Jest testing framework for its unit tests. The unit tests for the home page can be found in the tests directory. ` Storybook This starter kit comes with Storybook so you can test out your UI components in isolation. We have also included the @storybook/addon-a11y addon, which is used to check for common accessibility errors in your components. When you run Storybook, each story will show detailed explanations with suggested fixes if errors are found. Examples of stories can be found in the components directory. ESLint and Prettier This start kit uses ESLint for linting and Prettier for formatting. All of the configurations have been setup for you so you can get to building out your project faster. A note about state management This starter kit does not use a global state management library. Instead we are managing state within the routing system. For examples, please look at the /src/pages/counter-example.tsx and src/pages/fetch-example.tsx files. Deployment options You can use services like Netlify or Vercel to deploy your application. Both of these services will come with a built-in CI/CD pipeline and live previews. Reasons for using this kit Next.js is a versatile framework, and can be used for a variety of situations. Here are some examples of what you can use our starter kit for. - personal blog - e commerce application - user dashboard application - MVP (Minimum Viable Product) Conclusion Next.js has a lot to offer, and this new starter kit will help you bootstrap your next project. Study the example components to learn about best practices with Next.js and Chakra UI. Get started building out new features for your project with our new starter kit!...

Next.js Rendering Strategies and how they affect core web vitals cover image

Next.js Rendering Strategies and how they affect core web vitals

When it comes to building fast and scalable web apps with Next.js, it’s important to understand how rendering works, especially with the App Router. Next.js organizes rendering around two main environments: the server and the client. On the server side, you’ll encounter three key strategies: Static Rendering, Dynamic Rendering, and Streaming. Each one comes with its own set of trade-offs and performance benefits, so knowing when to use which is crucial for delivering a great user experience. In this post, we'll break down each strategy, what it's good for, and how it impacts your site's performance, especially Core Web Vitals. We'll also explore hybrid approaches and provide practical guidance on choosing the right strategy for your use case. What Are Core Web Vitals? Core Web Vitals are a set of metrics defined by Google that measure real-world user experience on websites. These metrics play a major role in search engine rankings and directly affect how users perceive the speed and smoothness of your site. * Largest Contentful Paint (LCP): This measures loading performance. It calculates the time taken for the largest visible content element to render. A good LCP is 2.5 seconds or less. * Interaction to Next Paint (INP): This measures responsiveness to user input. A good INP is 200 milliseconds or less. * Cumulative Layout Shift (CLS): This measures the visual stability of the page. It quantifies layout instability during load. A good CLS is 0.1 or less. If you want to dive deeper into Core Web Vitals and understand more about their impact on your website's performance, I recommend reading this detailed guide on New Core Web Vitals and How They Work. Next.js Rendering Strategies and Core Web Vitals Let's explore each rendering strategy in detail: 1. Static Rendering (Server Rendering Strategy) Static Rendering is the default for Server Components in Next.js. With this approach, components are rendered at build time (or during revalidation), and the resulting HTML is reused for each request. This pre-rendering happens on the server, not in the user's browser. Static rendering is ideal for routes where the data is not personalized to the user, and this makes it suitable for: * Content-focused websites: Blogs, documentation, marketing pages * E-commerce product listings: When product details don't change frequently * SEO-critical pages: When search engine visibility is a priority * High-traffic pages: When you want to minimize server load How Static Rendering Affects Core Web Vitals * Largest Contentful Paint (LCP): Static rendering typically leads to excellent LCP scores (typically < 1s). The Pre-rendered HTML can be cached and delivered instantly from CDNs, resulting in very fast delivery of the initial content, including the largest element. Also, there is no waiting for data fetching or rendering on the client. * Interaction to Next Paint (INP): Static rendering provides a good foundation for INP, but doesn't guarantee optimal performance (typically ranges from 50-150 ms depending on implementation). While Server Components don't require hydration, any Client Components within the page still need JavaScript to become interactive. To achieve a very good INP score, you will need to make sure the Client Components within the page is minimal. * Cumulative Layout Shift (CLS): While static rendering delivers the complete page structure upfront which can be very beneficial for CLS, achieving excellent CLS requires additional optimization strategies: * Static HTML alone doesn't prevent layout shifts if resources load asynchronously * Image dimensions must be properly specified to reserve space before the image loads * Web fonts can cause text to reflow if not handled properly with font display strategies * Dynamically injected content (ads, embeds, lazy-loaded elements) can disrupt layout stability * CSS implementation significantly impacts CLS—immediate availability of styling information helps maintain visual stability Code Examples: 1. Basic static rendering: ` 2. Static rendering with revalidation (ISR): ` 3. Static path generation: ` 2. Dynamic Rendering (Server Rendering Strategy) Dynamic Rendering generates HTML on the server for each request at request time. Unlike static rendering, the content is not pre-rendered or cached but freshly generated for each user. This kind of rendering works best for: * Personalized content: User dashboards, account pages * Real-time data: Stock prices, live sports scores * Request-specific information: Pages that use cookies, headers, or search parameters * Frequently changing data: Content that needs to be up-to-date on every request How Dynamic Rendering Affects Core Web Vitals * Largest Contentful Paint (LCP): With dynamic rendering, the server needs to generate HTML for each request, and that can't be fully cached at the CDN level. It is still faster than client-side rendering as HTML is generated on the server. * Interaction to Next Paint (INP): The performance is similar to static rendering once the page is loaded. However, it can become slower if the dynamic content includes many Client Components. * Cumulative Layout Shift (CLS): Dynamic rendering can potentially introduce CLS if the data fetched at request time significantly alters the layout of the page compared to a static structure. However, if the layout is stable and the dynamic content size fits within predefined areas, the CLS can be managed effectively. Code Examples: 1. Explicit dynamic rendering: ` 2. Simplicit dynamic rendering with cookies: ` 3. Dynamic routes: ` 3. Streaming (Server Rendering Strategy) Streaming allows you to progressively render UI from the server. Instead of waiting for all the data to be ready before sending any HTML, the server sends chunks of HTML as they become available. This is implemented using React's Suspense boundary. React Suspense works by creating boundaries in your component tree that can "suspend" rendering while waiting for asynchronous operations. When a component inside a Suspense boundary throws a promise (which happens automatically with data fetching in React Server Components), React pauses rendering of that component and its children, renders the fallback UI specified in the Suspense component, continues rendering other parts of the page outside this boundary, and eventually resumes and replaces the fallback with the actual component once the promise resolves. When streaming, this mechanism allows the server to send the initial HTML with fallbacks for suspended components while continuing to process suspended components in the background. The server then streams additional HTML chunks as each suspended component resolves, including instructions for the browser to seamlessly replace fallbacks with final content. It works well for: * Pages with mixed data requirements: Some fast, some slow data sources * Improving perceived performance: Show users something quickly while slower parts load * Complex dashboards: Different widgets have different loading times * Handling slow APIs: Prevent slow third-party services from blocking the entire page How Streaming Affects Core Web Vitals * Largest Contentful Paint (LCP): Streaming can improve the perceived LCP. By sending the initial HTML content quickly, including potentially the largest element, the browser can render it sooner. Even if other parts of the page are still loading, the user sees the main content faster. * Interaction to Next Paint (INP): Streaming can contribute to a better INP. When used with React's <Suspense />, interactive elements in the faster-loading parts of the page can become interactive earlier, even while other components are still being streamed in. This allows users to engage with the page sooner. * Cumulative Layout Shift (CLS): Streaming can cause layout shifts as new content streams in. However, when implemented carefully, streaming should not negatively impact CLS. The initially streamed content should establish the main layout, and subsequent streamed chunks should ideally fit within this structure without causing significant reflows or layout shifts. Using placeholders and ensuring dimensions are known can help prevent CLS. Code Examples: 1. Basic Streaming with Suspense: ` 2. Nested Suspense boundaries for more granular control: ` 3. Using Next.js loading.js convention: ` 4. Client Components and Client-Side Rendering Client Components are defined using the React 'use client' directive. They are pre-rendered on the server but then hydrated on the client, enabling interactivity. This is different from pure client-side rendering (CSR), where rendering happens entirely in the browser. In the traditional sense of CSR (where the initial HTML is minimal, and all rendering happens in the browser), Next.js has moved away from this as a default approach but it can still be achievable by using dynamic imports and setting ssr: false. ` Despite the shift toward server rendering, there are valid use cases for CSR: 1. Private dashboards: Where SEO doesn't matter, and you want to reduce server load 2. Heavy interactive applications: Like data visualization tools or complex editors 3. Browser-only APIs: When you need access to browser-specific features like localStorage or WebGL 4. Third-party integrations: Some third-party widgets or libraries that only work in the browser While these are valid use cases, using Client Components is generally preferable to pure CSR in Next.js. Client Components give you the best of both worlds: server-rendered HTML for the initial load (improving SEO and LCP) with client-side interactivity after hydration. Pure CSR should be reserved for specific scenarios where server rendering is impossible or counterproductive. Client components are good for: * Interactive UI elements: Forms, dropdowns, modals, tabs * State-dependent UI: Components that change based on client state * Browser API access: Components that need localStorage, geolocation, etc. * Event-driven interactions: Click handlers, form submissions, animations * Real-time updates: Chat interfaces, live notifications How Client Components Affect Core Web Vitals * Largest Contentful Paint (LCP): Initial HTML includes the server-rendered version of Client Components, so LCP is reasonably fast. Hydration can delay interactivity but doesn't necessarily affect LCP. * Interaction to Next Paint (INP): For Client Components, hydration can cause input delay during page load, and when the page is hydrated, performance depends on the efficiency of event handlers. Also, complex state management can impact responsiveness. * Cumulative Layout Shift (CLS): Client-side data fetching can cause layout shifts as new data arrives. Also, state changes might alter the layout unexpectedly. Using Client Components will require careful implementation to prevent shifts. Code Examples: 1. Basic Client Component: ` 2. Client Component with server data: ` Hybrid Approaches and Composition Patterns In real-world applications, you'll often use a combination of rendering strategies to achieve the best performance. Next.js makes it easy to compose Server and Client Components together. Server Components with Islands of Interactivity One of the most effective patterns is to use Server Components for the majority of your UI and add Client Components only where interactivity is needed. This approach: 1. Minimizes JavaScript sent to the client 2. Provides excellent initial load performance 3. Maintains good interactivity where needed ` Partial Prerendering (Next.js 15) Next.js 15 introduced Partial Prerendering, a new hybrid rendering strategy that combines static and dynamic content in a single route. This allows you to: 1. Statically generate a shell of the page 2. Stream in dynamic, personalized content 3. Get the best of both static and dynamic rendering Note: At the time of this writing, Partial Prerendering is experimental and is not ready for production use. Read more ` Measuring Core Web Vitals in Next.js Understanding the impact of your rendering strategy choices requires measuring Core Web Vitals in real-world conditions. Here are some approaches: 1. Vercel Analytics If you deploy on Vercel, you can use Vercel Analytics to automatically track Core Web Vitals for your production site: ` 2. Web Vitals API You can manually track Core Web Vitals using the web-vitals library: ` 3. Lighthouse and PageSpeed Insights For development and testing, use: * Chrome DevTools Lighthouse tab * PageSpeed Insights * Chrome User Experience Report Making Practical Decisions: Which Rendering Strategy to Choose? Choosing the right rendering strategy depends on your specific requirements. Here's a decision framework: Choose Static Rendering when * Content is the same for all users * Data can be determined at build time * Page doesn't need frequent updates * SEO is critical * You want the best possible performance Choose Dynamic Rendering when * Content is personalized for each user * Data must be fresh on every request * You need access to request-time information * Content changes frequently Choose Streaming when * Page has a mix of fast and slow data requirements * You want to improve perceived performance * Some parts of the page depend on slow APIs * You want to prioritize showing critical UI first Choose Client Components when * UI needs to be interactive * Component relies on browser APIs * UI changes frequently based on user input * You need real-time updates Conclusion Next.js provides a powerful set of rendering strategies that allow you to optimize for both performance and user experience. By understanding how each strategy affects Core Web Vitals, you can make informed decisions about how to build your application. Remember that the best approach is often a hybrid one, combining different rendering strategies based on the specific requirements of each part of your application. Start with Server Components as your default, use Static Rendering where possible, and add Client Components only where interactivity is needed. By following these principles and measuring your Core Web Vitals, you can create Next.js applications that are fast, responsive, and provide an excellent user experience....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co