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 graphPackage 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:

graph TD FooCore --> BuildTool BarCore --> BuildTool FooApp1 --> FooCore FooApp2 --> FooCore BarPage --> BarCore

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.

gantt title Level 1: Typical Lerna or Workspace Runners dateFormat s axisFormat %S section Total prepare: active, total_prepare, 0, 30s build : active, total_build, after total_prepare, 50s test : active, total_test, after total_build, 25s section BuildTool prepare: bt_prepare, 0, 10s build : bt_build, after total_prepare, 10s test : bt_test, after total_build, 6s section FooCore prepare: fc_prepare, after bt_prepare, 10s build: fc_build, after bt_build, 15s test: fc_test, after total_build, 25s section FooApp1 prepare: fa1_prepare, after fc_prepare, 10s build: fa1_build, after fc_build, 25s test: fa1_test, after total_build, 15s section FooApp2 prepare: fa2_prepare, after fc_prepare, 10s build: fa2_build, after fc_build, 12s test: fa2_test, after total_build, 8s section BarCore prepare: bc_prepare, after bt_prepare, 10s build: bc_build, after bt_build, 10s test: bc_test, after total_build, 16s section BarPage prepare: bp_prepare, after bc_prepare, 10s build: bp_build, after bc_build, 25s test: bp_test, after total_build, 12s

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.

gantt title Level 2: Scoping dateFormat s axisFormat %S section Total Level 1: 0, 105s prepare: active, total_prepare, 0, 30s build : active, total_build, after total_prepare, 45s test : active, total_test, after total_build, 16s section BuildTool prepare: bt_prepare, 0, 10s build : bt_build, after total_prepare, 10s test : bt_test, after total_build, 6s section FooCore skipped: 0 section FooApp1 skipped: 0 section FooApp2 skipped: 0 section BarCore * prepare: bc_prepare, after bt_prepare, 10s build: bc_build, after bt_build, 10s test: bc_test, after total_build, 16s section BarPage prepare: bp_prepare, after bc_prepare, 10s build: bp_build, after bc_build, 25s test: bp_test, after total_build, 12s

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

gantt title Level 3: Caching dateFormat s axisFormat %S section Total Level 1: 0, 105s Level 2: 0, 91s prepare: active, total_prepare, 0, 30s build : active, total_build, after total_prepare, 37s test : active, total_test, after total_build, 12s section BuildTool prepare: bt_prepare, 0, 10s build : bt_build, after total_prepare, 10s test : bt_test, after total_build, 6s section FooCore skipped: 0 section FooApp1 skipped: 0 section FooApp2 skipped: 0 section BarCore prepare: bc_prepare, after bt_prepare, 10s build: crit, bc_build, after bt_build, 2s test: crit, bc_test, after total_build, 2s section BarPage * prepare: bp_prepare, after bc_prepare, 10s build: bp_build, after bc_build, 25s test: bp_test, after total_build, 12s

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.

gantt title Level 4: Pipelining dateFormat s axisFormat %S section Total Level 1: 0, 105s Level 2: 0, 91s Level 3: 0, 79s prepare: active, total_prepare, 0, 30s build : active, total_build, 10, 45s test : active, total_test, 20, 47s section BuildTool prepare: bt_prepare, 0, 10s build : bt_build, after bt_prepare, 10s test : bt_test, after bt_build, 6s section FooCore skipped: 0 section FooApp1 skipped: 0 section FooApp2 skipped: 0 section BarCore prepare: bc_prepare, after bt_prepare, 10s build: bc_build, after bt_build, 2s test: bc_test, after bc_build, 2s section BarPage * prepare: bp_prepare, after bc_prepare, 10s build: bp_build, after bp_prepare, 25s test: bp_test, after bp_build, 12s