Skip to main content

Introduction

Your JavaScript repository has grown large enough that you have turned to using a monorepo to help you organize your code as multiple packages inside a repository. That's great! However, you realized quickly that the build scripts defined inside the workspace have to be run in package dependency order.

There exist many tools in the market that provide ways for you to run these npm scripts in the correct topological order. So why choose lage for your repository?

  1. lage is battle tested - it is in use by many JavaScript repositories number in the millions lines of code each
  2. lage can be easily adopted - all it takes is just one npm package install with a single configuration file for the entire repository
  3. lage supports remote cache as a fallback - never build the same code twice
  4. lage is optimized for modern multi-core development machines - don't waste your CPU resources waiting on a single core when you have so many to spare!

How does lage schedule tasks?

lage has a secret weapon: it has a "pipeline" configuration syntax to define the implicit relationship between tasks. Combined with a package graph, lage knows how to schedule which task to run first and which one can be run in parallel. Let's look at an example:

Package graph with task graph equals target graph

How does lage make builds faster?

So how does lage make builds faster? To fully appreciate how lage gives you the best build performance compared to other monorepo task runners, take a look at this example. Here we have a repo with this dependency graph:

FooCore
BuildTool
BarCore
FooApp1
FooApp2
BarPage

Level 1: Legacy Workspace Runners

First, let's take a look at the typical workspace runners. Lerna (before), pnpm recursive, rush and wsrun all will run one task at a time. This creates a sort of "build phase" effect where test scripts are not allowed to run until build.

00 15 30 45 00 15 30 45Level 1Level 2Level 3prepareprepareskippedskippedbuild build preparetest test buildpreparetestskippedbuildtestTotalBuildToolFooCoreFooApp1FooApp2BarCoreBarPage *

Level 2: Scoping

One of the first ways to speeding up build jobs is to use "scoping." Usually a change only affects a subset of the graph. We can get rid of the builds of FooCore, FooApp1 and FooApp2 if the only changes are inside BarCore. However, we'll note that BarPage is still affected, resulting in this.

00 15 30 45 00 15 30 45Level 1Level 2Level 3preparepreparebuild preparebuildbuild skippedskippedtest testtest buildprepareskippedtestTotalBuildToolFooApp1FooApp2BarCoreBarPage *FooCore

Level 3. Caching

To further improve build times, we can take advantage of build caches. If we had previously built certain packages, we should be able to speed up the build with a cache. Here, the BarCore packages have already been built and tested, and so

00 15 30 45 00 15 30 45Level 1Level 2Level 3prepareprepareskippedtestskippedbuild preparebuild preparetest skippedbuildtest buildtestTotalBuildToolBarCoreBarPage *FooCoreFooApp1FooApp2

Level 4. Pipelining

Finally, the last thing we can to speed things up is to break down the wall between build phases from the task runner. In lage, we define the relationship between scripts in the pipeline configuration.

00 15 30 45 00 15 30 45 00Level 1Level 2Level 3preparepreparetest buildtestbuild skippedtest skippedpreparetestbuildbuild prepareskippedTotalBuildToolFooApp1BarPage *FooApp2BarCoreFooCore