SPFx overclockers or how to significantly improve your SharePoint Framework build performance

Today's post will be about SharePoint Framework build performance. Especially about "serve" command, because it's the most frequently used command among developers. gulp serve is a kind of "watch" mode for your SharePoint Framework solution. As soon as you update a file, it will spin up the build process and will refresh your browser finally, so that you can see changes.

However, from here and there, I hear complaints about the poor performance of gulp serve command, especially if you have more than 10 web parts in a solution, or if your webparts are quite complicated (with lots of code and \ or additional heavy dependencies). Checkout Gulp webpack slow build and Long build times for SPFx projects with many components GitHub issues as well. I'm also not satisfied with the build performance in case of medium and of course big SharePoint Framework solutions. In a few recent weeks, I spent some time trying to go deeper and understanding all possible ways on how to improve performance for gulp serve command.

Read further and you will find a list of tricks, which reduce the amount of time to build a common SharePoint Framework solution. By build I mean serve or bundle (without --ship parameter) command, because they are very identical. The only difference is that serve is never-ending and has an additional step which refreshes your browser. In all other cases, they are the same, running tslint, typescript, sass, webpack, copy assets, etc. tasks. I will start with the easiest tricks, going to more complicated technics. I don't use any heavy hacks here. 

At the end of the post, you will find a detailed report on how any particular trick reduces build time on the example of SharePoint Starter Kit:

This is a solution designed for SharePoint Online which provides numerous web parts, extensions, and other components which you can use as an example and inspiration for your own customizations.

It contains 20+ webparts and quite slow when you use gulp serve command. Which makes it a good candidate for improvements.

Disclaimer

All hints listed here were tested with SharePoint Framework 1.9.1. Maybe in future some of them will not work or Microsoft will improve the build process, please keep that in mind. 

Ok, so here is my list of tricks to improve the speed of your SharePoint Framework serve build:

1. Disable autosave feature in VSCode. 

If you have autosave feature with "afterDelay" setting, you might be in trouble. Maybe it doesn't affect serve command explicitly, but I found it very annoying when the build runs every time you modify one file and not even going to see what happens in a browser. It heavily loads CPU and affects performance in general. 

So just disable it by going to workspace settings and either disable it or set it to be "onFocusChange" or "onWindowsChange". You can do that by modifying .vscode/settings.json file: 

2. Disable tslint & TypeScript task. 

TSLint task consumes a few precious seconds. Do we need it for just serve? I think no, additionally, vscode is smart enough to show tslint errors live. 

How to disable it? You should modify gulpfile.js in your solution. First of all, I recommend you to have a flag, which indicates that you want to run serve or bundle process in "performance-optimized mode". For example, gulp serve --fast enables performance optimization, while gulp serve spins up a regular serve process without optimizations. Doing that way you will be able to compare them in case of any unexpected errors or behaviors.

For convenience install yargs module to read command line parameters: 

npm install yargs --save-dev

Now in your gulpfile.js (before build.initialize(gulp); line):

const argv = require('yargs').argv;

// apply performance fixes only on 'gulp serve --fast' or 'gulp bundle --fast'
const isFast = argv['fast'];

if (isFast) {
  applyPerformanceFixes();
}

function applyPerformanceFixes() {
  // disable tslint task
  build.tslintCmd.enabled = false;
}

TypeScript task is slow, so just disable it. How to transpile .ts files in that case? With custom TypeScript incremental watch process.

The issue with TypeScript task inside SharePoint Framework build pipeline is that it gets all TypeScript files and transpiles them every time you modify a single file in the src folder. A better (and faster) approach is to use incremental TypeScript build in watch mode. 

How to do that? Very simple. Disable TypeScript task (inside applyPerformanceFixes function):

build.tscCmd.enabled = false;

But that's not all. My original solution contained an issue with clean "serve" command like Elio correctly pointed out in comments. So I updated this step a bit to fix those issues as well. Thank you, Elio for a nice catch!

By the way, Elio also posted an article about SharePoint Framework performance impromenets. However, he uses completely different approach - Windows Subsystem for Linux (WSL 2). Check out his post - Speed up SharePoint Framework builds with WSL 2. If you combine both approaches, you might gain incredible speed on Windows machines!

To fix sass and localization files issue, we should:

  1. Run sass task prior to our serve command. Simply add a new task in your gulpfile.js

gulp.task('sass', () => {
  const config = build.getConfig();
  return build.sass.execute(config);
})

  2. Add custom build step which copies all localization files to output lib folder:

function applyPerformanceFixes() {
  ....
  // disable typescript task (execute `npm run tsc:watch` in a separate cmd to have typescript support)
  build.tscCmd.enabled = false;
  //add custom copy task, which copies localization files
  addCopyTask();
  // optional, add only if typescript incremental build ends after webpack is starting
  //addWaitSubTask();

  ....
}

function addCopyTask() {
  build.rig.addPostTypescriptTask(build.subTask('custom-copy', function (gulp, buildOptions, done) {
    gulp.src(['src/**/*.js', 'src/**/*.json'])
      .pipe(gulp.dest('lib')).on('end', done);
  }));
}

We're going to run TypeScript compilation and serve at the same time, thus let's add concurrently module to help us:

npm install concurrently --save-dev

Additionally, you should install TypeScript of the same version as your SharePoint Framework solution, for example for 1.9.1 you should install:

npm install typescript@2.9.2 --save-dev

in your package.json file add a new npm script, which executes serve with incremental TypeScript build:

"serve:fast": "gulp sass && concurrently \"tsc -p tsconfig.json -w --preserveWatchOutput true\" \"gulp serve --fast-mode\"",

It executes sass task for us (so that all our <Component>.module.scss.ts files are created), then it runs TypeScript compilation in watch mode and serve command in parallel. How to run fast serve then? Just run in a console:

npm run serve:fast

A few notes on concurrency. It's very important that incremental TypeScript ends before webpack task starts in gulp serve command. I've made lots of experiments and it worked just fine for me, because incremental TypeScript is fast, from the other side gulp serve command spends 1-2 seconds of preparations before going to webpack task, which is enough for incremental TypeScript task to complete its work. If in some cases it's not enough, you can add an empty subtask to your gulpfile.js, which waits 1-2 seconds before webpack task:

const wait = build.subTask('wait', function (gulp, buildOptions, done) {
    setTimeout(done, 1000);
  });

  build.rig.addPreBuildTask(wait);

But again, it's optional and I didn't have a need to add it.

Now it's time to have some webpack fun!

3. Add include rule for webpack's source map loader.  

Gulp serve spends a lot of time inside the webpack task. Let's try to understand why it's happening and how to fix it. There is a very handy webpack plugin called Speed Measure Plugin. It measures the performance of individual tasks inside the webpack pipeline and outputs the report. 

This is what is shows for SP Starter Kit:

Total time spent for webpack build - 36 seconds. 10 sec spent on source-map-loader. That's not good. What is source map loader? It ensures that all source maps correctly loaded for your sources. From the output, we see that it processed 1621 modules. SP Starter Kit has fewer source files. Probably this loader tries to find source maps for dependencies as well, but we don't need them, only for our own sources. 

So let's add include rule for source-map-loader, so that it looks only inside our lib folder (inside applyPerformanceFixes function):

const path = require('path');

build.configureWebpack.mergeConfig({
    additionalConfiguration: (generatedConfiguration) => {
      includeRuleForSourceMapLoader(generatedConfiguration.module.rules);
      return generatedConfiguration;
    }
  });

// sets include rule for source-map-loader to load source maps only for your sources, i.e. files from src/ folder
function includeRuleForSourceMapLoader(rules) {
  for (const rule of rules) {
    if (rule.use && typeof rule.use === 'string' && rule.use.indexOf('source-map-loader') !== -1) {
      rule.include = [
        path.resolve(__dirname, 'lib')
      ]
    }
  }
}

This is how result webpack configuration will look like:

4. Disable minification for css-loader. 

If we go back to the output from the speed measure plugin, we will see that css-loader took approximately 4 sec. In the original webpack configuration SharePoint Framework sets minimize=true for css-loader. We definitely don't need minimized css for serve command, because css minification is a time-consuming process. So let's fix it:

build.configureWebpack.mergeConfig({
    additionalConfiguration: (generatedConfiguration) => {
      disableMinimizeForCss(generatedConfiguration.module.rules);
      return generatedConfiguration;
    }
  });

function disableMinimizeForCss(rules) {
  for (const rule of rules) {
    if (rule.use
      && rule.use instanceof Array
      && rule.use.length == 2
      && rule.use[1].loader
      && rule.use[1].loader.indexOf('css-loader') !== -1) {
      rule.use[1].options.minimize = false;
    }
  }
}

The result:

5. Use hard-source-webpack-plugin.

HardSourceWebpackPlugin is a plugin for webpack to provide an intermediate caching step for modules. In order to see results, you'll need to run webpack twice with this plugin: the first build will take the normal amount of time. The second build will be significantly faster.

Install it first:

npm install hard-source-webpack-plugin --save-dev

Add to the list of plugins for webpack:

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

build.configureWebpack.mergeConfig({
    additionalConfiguration: (generatedConfiguration) => {
      generatedConfiguration.plugins.push(new HardSourceWebpackPlugin());
      return generatedConfiguration;
    }
  });

6. Bundle only webparts you need for development (aka active component).

This one a little tricky, but it might give a lot of additional performance boost. The idea is that you don't need all your webparts to be bundled every time you develop. It's especially true for SP Starter Kit because it contains 20+ web parts. Usually, you work with 1-3 web parts at a time. So why make heavy build every time, if we don't use all of the web parts during development? 

The easiest way is just going and deleting all not needed web parts from config/config.json file. It works, but config.json is a source-controlled file, you have to restore it every time you're going to commit your code. There is a better way to fix it without modifying config.json file. 

Create a new file config/active-components.json.Specify an array of components you're going to use for development. For example, just links web part:

["links-web-part"]

Update your webpack build to have only one "real" entry point for the links web part. For other web parts specify entry point to be index.js (which is just empty file).

build.configureWebpack.mergeConfig({
         useActiveComponent(generatedConfiguration.entry);
      return generatedConfiguration;
    }
  });

// enables only webparts \ bundles listed in active-components.json file
// all others will be disabled
function useActiveComponent(entry) {
  const components = require('./config/active-components.json');
  if (!components || components.length === 0) return;

  const indexPath = path.resolve(__dirname, 'lib/index.js');

  for (const entryKey in entry) {
    if (components.indexOf(entryKey) === -1) {
      entry[entryKey] = indexPath;
    }
  }

}

Checkout resulting webpack config:

Links web part points to correct entry point, all others point to index.js. Index.js is empty, so webpack doesn't waste resources on bundling other components, only links web part. 

If you try to add a web part other than link web part, you will receive an error: 

To fix it simply add "followed-sites-web-part" to the list in the active-components.json file and re-run the process. You can ignore active-components.json in your source control, so every developer has their own version of "active" web parts. 

If you're still here, you're a hero! Let's proceed to the most interesting part - performance tests!

Performance tests

As said, I used SP Starter Kit as a solution to run tests. I measured the average time of gulp bundle process (I use gulp bundle because it's an equivalent to gulp serve, it outputs running time and it ends, unlike gulp serve). Gulp bundle without modifications takes approximately 60 seconds, let's consider it like our 100% measure. Then I run gulp bundle with each individual improvements to see how it affects general performance.

My setup: 

- Virtual machine with Windows 10

- 12 GB memory on VM, Samsung SSD, Intel Core i7 3.5 GHz. 

You can see the results in the table below:

  Time, sec Improvement, times faster
SP Starter Kit without optimizations 60s 1
     
2. Disabled tslint & TypeScript 50s 1.2x
3. source-map-loader optimizations 42s 1.42x
4. css-loader optimizations 57s 1.05x
5. hard-source-loader webpack plugin 55s 1.09x
All the above 24s 2.5x
     
6. The active component (only links web part) without other improvements 30s 2.0x
The active component + all others (1-5 improvements) 14s 4.2x

Check out how two very simple webpack tricks 3. source-map-loader optimizations and 4. css-loader optimizations affect webpack performance (measured with speed-measure webpack plugin). 

Two simple tricks made webpack build 2.5x times faster!

I also measured the same using just regular OOB Hello World react web part:

  Time, sec Improvement, times faster
Out of the box Hello World web part 13s 1
All improvements (1-5)  8s 1.6x

Conclusion

If you want to improve the speed of your SharePoint Framework build significantly, use 6. The active component together with 3. source-map-loader optimizations. It will give you (of course depending on your machine) 2x - 2.5x faster builds. If you want even more speed, use 2. Disabled tslint & TypeScript as well. For maximum performance, apply all 1-6 improvements. You can do experiments on your side and see what works for you better. 

If you want to try to run my samples from your environment, please download my fork of SP Starter Kit, performance branch. It contains all the modifications I described in this post (spfx project is in the solution folder). 

A few more words

The problem is getting more and more annoying. With such great numbers in adoption regarding SharePoint Framework, Microsoft, please provide more convenience for developers as well. Serve build should be incremental, TypeScript task should use watch mode, webpack task should use watch, webpack config should be optimized as much as possible for performance, other time-consuming tasks should be optimized for performance as well. You will help the community a lot with such improvements.