Speed up monorepo build times with Turborepo

Eze Sunday Eze

In this article we learn how to leverage turborepo to speedup a monorepo's build time.

Speed up monorepo build times with Turborepo

It's a common practice for engineers to create unique repositories for different projects. This pattern of organizing and managing code projects is referred to as a polyrepo. There are good reasons you might want to use this approach, for example:

  • It makes managing repository access control for different projects a lot easier
  • Your team can rapidly implement and independently deploy various products
  • It reduces complexity

A monorepo is the opposite. It's simply a repository that contains more than one project or application. That means a monorepo can contain a shared library, micro-services, and client applications in one repository.

While technically, these projects may be related, they can logically run independently of each other. In fact, they are mostly run by different teams, but the codebase is shared with everyone.

A monorepo allows teams to have a unified version control system (as a single source of truth) for their entire system. It makes code sharing across projects and libraries a lot easier.

More so:

  • it simplifies dependency management across projects
  • it improves collaboration
  • allows for easier large-scale code refactoring
  • allows you to see and understand how every part of the system is connected in one code base
  • and simplifies your entire system deployment pipeline

As you can imagine, building a large monorepo code base for deployment can be a time-consuming process. Depending on the scale of your project, it could run into several minutes or even hours. That is not easily scalable.

That's where Turborepo comes in, especially if your tech stack is entirely JavaScript based.

Turborepo is a high-performance build system for JavaScript and TypeScript-based monorepos. It literally reduces your monorepo built time to milliseconds. It was built by Jared Palmer and acquired by Vercel in 2021.

So, what does Turborepo do to increase your monorepo build time?

Never recompute work that has been done before

Turborepo is designed to build your code faster, incrementally, and never to recompute work that has been done before using the following architecture:

Caching

Generally, caching is a transient data-storage mechanism that stores a subset of data so that further requests for data are served up faster than it's normally possible when accessing the same data from the primary storage location. Think Memcache and Redis.

Turborepo uses this approach to optimize your build system for speed and scale. Best of all, it allows you to cache your remote builds as well as your local build.

Local caching

When you run a build, Turborepo takes account of the metadata of that build and creates a cached record for it on your computer to ensure that it doesn't repeat that build if nothing changes in the related files.

Let's create a sample application to see this in action:

Run the following code on your Terminal to install and set up a fresh monorepo with Turborepo integrated.

$ npx create-turbo

The prompt from that command will guide you to scaffold a basic monorepo containing two Next.js projects with shared ts-config, eslint, and a UI library.

Your project structure should look like so:

.
├── README.md
├── apps
│   ├── docs
│   │   ├── README.md
│   │   ├── next-env.d.ts
│   │   ├── next.config.js
│   │   ├── package.json
│   │   ├── pages
│   │   │   └── index.tsx
│   │   └── tsconfig.json
│   └── web
│       ├── README.md
│       ├── next-env.d.ts
│       ├── next.config.js
│       ├── package.json
│       ├── pages
│       │   └── index.tsx
│       └── tsconfig.json
├── package.json
├── packages
│   ├── eslint-config-custom
│   │   ├── index.js
│   │   └── package.json
│   ├── tsconfig
│   │   ├── README.md
│   │   ├── base.json
│   │   ├── nextjs.json
│   │   ├── package.json
│   │   └── react-library.json
│   └── ui
│       ├── Button.tsx
│       ├── index.tsx
│       ├── package.json
│       └── tsconfig.json
├── turbo.json
└── yarn.lock

Once that's completed, run a build or a lint task.

I'll run the lint task first;

lint task output with no cache
lint task output with no cache

Notice that it took 7.454 seconds to run the first lint operation, and it says 0 cached, 3 tasks in total.

Now, if we run it again without changing anything in the files, the time it will take to process this operation is going to be way less. In my case, it took 669 milliseconds with the 3 tasks cached, as shown in the screenshot below;

lint task output with cache
lint task output with cache

That's more than 99% time savings. So, what happened is that Turborepo cached the first lint task the first time it was run, and since we haven't updated the project files, it technically just replayed the previous lint result. This is the same thing that will happen for a build operation as well.

You can find the general cache log files in the node_modules directory ./node_modules/.cache/turbo. However, there are also app-specific caches saved in the app directory.

This works all nice, but when you create a build with your CI, you might not get the same experience since your CI has no record of any caches. It might take a long time each time you or anyone in your team create a deployment.

Remote Caching

To solve this problem, we use the Turborepo remote caching option, which stores your cache history in the cloud. The Turborepo partnership with Vercel allows them to securely store the result of your cache remotely on the Vercel servers.

Here is how to configure remote caching for your Turborepo setup. Run the following command:

You'll need to authenticate with Vercel before you can use the remote caching option with Vercel. You can create a free account on Vercel in case you don't have an account yet. Authenticate your Turborepo with your Vercel account by running the following command;

$ npx turbo login

Next, link your Turborepo to your remote cache:

$ npx turbo link

login and link
login and link

You can verify that it works by deleting your local Turborepo cache by running:

$ rm -rf ./node_modules/.cache/turbo

And then run the build again. You'll notice that it's still fast. For me, it took 166 milliseconds.

cached run
cached run

Interestingly, you can use your own custom remote caching host if you want as long as it complies with Turborepo's Remote Caching Server API.

Here is a sample command to help you set it up.

$ turbo run build --api="https://my-server.example.com" --token="xxx"

You can refer to the docs for more details on how to set a custom remote caching for your project.

Multitasking

Turborepo can multitask and use your available CPU resources effectively in other to allow you to optimize your workflow for speed.

Build tasks before Turborepo
Build tasks before Turborepo

Take a look at the image above for a moment. There are two apps and a shared library running 3 tasks. This is a result of a task that was run with the yarn workspaces in a monorepo with the following command:

$ yarn workspaces run lint
$ yarn workspaces run test
$ yarn workspaces run build

You'll notice that the tasks are running one after the other with lint going first, build follows, and it exits with the test.

This is an extremely slow process as each task needs to wait for the previous one. But with Turborepo's multitasking capability and how it understands your monorepo dependencies, it can schedule your tasks for maximum speed by understanding the dependencies between the tasks. It does this with the turbo.json configuration file in your root directory. See the example below:

{
    "$schema": "https://turborepo.org/schema.json",
    "pipeline": {
        "build": {
            "dependsOn": ["^build"],
            "outputs": ["dist/**", ".next/**"]
        },
        "lint": {
            "outputs": []
        },
    }
}

With the turborepo configuration above, Turborepo will run as many tasks as possible in parallel over available CPUs. That means your task will run as shown below:

Build tasks with Turborepo
Build tasks with Turborepo

The way Turborepo understands the above task is that since both lint and test tasks do not have a dependency, they'll run immediately. In comparison, the build task for the shared directory runs next before the web and docs task since they both depend on the shared task because they have no dependsOn specified in the turbo.json file.

This is one of the ways Turborepo execution makes your build blazing fast.

Integration

Integrating Turborepo in your monorepo is straightforward. The best part with Turborepo is that you can integrate it into your existing monorepo and remove it anytime you want as far as your monorepo follows the workspaces architecture.

To integrate Turborepo into your monorepo, you'll first install Turborepo with your project package manager (yarn, npm, or pnpm):

$ npm install turbo -D

The turbo.json file is the configuration file for your Turborepo. This is where you'll define how you expect Turborepo to behave.

Here is an example of the turbo.json file and the pipeline:

{
    "$schema": "https://turborepo.org/schema.json",
    "pipeline": {
        "build": {
            "dependsOn": ["^build"],
            "outputs": ["dist/**", ".next/**"]
        },
        "lint": {
            "outputs": []
        },
    }
}

That's all you need to integrate Turborepo into your existing monorepo, and you can remove this configuration if you don't want it anymore or want to test another monorepo tool. So, you don't even have to be worried about being locked in. There are a lot more configuration options to help you configure Turborepo as it best suits you. You should read the docs for more.

Conclusion

In the cause of this article, we've seen that Turborepo can speed up your monorepo build time exponentially, and it's easy to integrate into your workflow. Google and Microsoft use a whole bunch of tools to achieve the best result for their monorepos, as their monorepo source code runs into billions of files. If you are not Google and you use the monorepo project architecture design, Turborepo can give your project a significant boost and allow you to scale your codebase drastically.

Visually test your app

VisWiz.io is a visual regression testing service which helps catch unwanted UI changes, by integrating with your testing framework and analyzing visual differences on your project.

Try VisWiz.io now

Share article on:

All Articles