In an era where Continuous Integration/Continuous Deployment (CI/CD) is the norm, improving build times becomes a necessity for a successful DevOps strategy. One of the essential factors that lead to improved build times is eliminating the waste. When building an Angular application this means avoiding downloading the same npm packages after each build. Not only is it a time consuming process but it also introduces several unnecessary API calls.
This problem historically existed when using hosted build agents since each time a build is triggered a new VM is allocated and thus any previously cached npm packages are lost. One way to get around this is to use a self-hosted build agent. Whereas this is an effective technique it may not always be a feasible solution. Fortunately, Azure DevOps now offers a new build task called cache task. This task works like any other task and is added to the
steps section of a job. When a cache step is encountered during a run, the task will restore the cache based on the provided inputs. If no cache is found, the step completes and the next step in the job is run. After all steps in the job have run and assuming a successful job status, a special “save cache” step is run for each “restore cache” step that was not skipped. This step is responsible for saving the cache.
To demonstrate the cache task in action I went ahead and created a simple build pipeline that builds an Angular application. As shown below the build pipeline is comprised of three different tasks. The first task restores the npm packages, the second task builds the Angular application, and the third task publishes the artifacts to be used by the release pipeline. As shown below, the npm installation task takes 1m 19s to complete. This is a significant amount of time for something that rarely changes between build runs.
In order to avoid downloading the same npm packages after each build I introduced the cache task to my build pipeline as shown below.
key should be set to the identifier for the cache you want to restore or save. Keys are composed of a combination of string values, file paths, or file patterns, where each segment is separated by a
| character. I used the file pattern which is a comma-separated list of glob-style wildcard pattern that must match at least one file. In this example the key will be a hash of the package-lock.json file which lives in the root level.
**/package-lock.json, !**/node_modules/**/package-lock.json, !**/.*/**/package-lock.json
In the future, any modifications to the package-lock.json file will invalidate the cache which would prompt restoring the npm packages again.
path should be set to the directory to populate the cache from (on save) and to store files in (on restore). It can be absolute or relative. Relative paths are resolved against
$(System.DefaultWorkingDirectory). I set the path to the node_modules folder since it includes the downloaded npm packages that I am trying to cache.
If you look closely at the build run below you will notice that the first time the build is run there is a cache miss as the cache doesn’t exist yet. The cache is then resolved to the key which is the hash code of the package-lock.json file.
Note: At this point nothing has been cached yet. Once the npm installation step runs there is a post-job:cache npm packages step that gets added by the cache task that caches the npm packages.
Once the packages are cached, all subsequent runs will use the cached node_modules. As you can see below the npm package installation finished in 30s instead of the original 1m 19s.
Optimistic Cache Restoration
The cache task provides a “Cache hit variable” that can be tapped into in later tasks in the pipeline. As shown below I defined the variable “CachRestored” which I utilize to optionally skip the npm install step.
I had to revisit the npm install task to conditionally run it when there is a cache miss in the cache task. I achieve that by tapping into the “CacheRestored” which is only set to true upon a cache hit.
As you can see below the npm install task was skipped when there was a cache hit. This provided an additional performance boost by reducing the build time by an additional 29s (dropped from 30s to 1s).
As demonstrated above I was able to drop the build time from 5m 10s to 2m 34s which is roughly a 50% reduction in build time. This is a significant saving especially when you factor in the fact that you will run the build several times a day. This can quickly add up to hours of saved build time each day.
In a future blog post I will look into achieving similar build time savings for containerized Angular applications. Specifically, I will try to achieve caching for different docker layers. Stay tuned!