Tuesday, August 21, 2018

An adventure into optimizing SharePoint Framework runtime bundle sizes

image

(See https://www.techmikael.com/2018/08/an-even-better-bundle-optimization.html for an even better solution)

When I wrote the Modern Script Editor web part I went with using Office UI Fabric React (ouifr) for the editor UI. The web part bundle yields a zipped script file of 84KB when used on a page. Not that much really, and it will be cached in the browser. But most of the bundle size is due to the use of ouifr in the edit experience of the web part – not needed in view mode for a page.

Thus I’ve had a nagging feeling working on my mind for a while, and it occurred to me; why can’t I have the editor experience separate from the run-time experience?

And you can!! which I will explain how further in this post.

The result is that the updated web part now will download 4KB zipped on run-time instead of 84KB. A whopping 95% decrease. The editor experience will be another 141KB, which is more in total than before, but this only happens when you edit the page and web part – certainly a good trade off.

Bundle size revisited

When building SPFx components, any external library you add to your project will be included in the transpiled bundle, subsequently being downloaded and bootstrapped on the page you use the web part or extension on.

You can take advantage of asset bundling and the built-in CDN in SharePoint Online, or deploy the files to your own CDN, but the initial script size is still the same, and bootstrapping many large libraries will add to script running speed in your browser. This means, the less script you download and bootstrap on a page, the faster the page will run. Certainly something your end-users will appreciate.

Personally I opt for using asset bundling in my SPFx project, and use the CDN in SharePoint Online. This yields for the easiest setup for IT and for me as a developer. This post will focus on how to achieve dynamic loading of script when using asset bundling.

Breaking up the bundle

In order to break up a SPFx bundle where you dynamically load script resources you need to do the following:
  • Create a separate project which builds your script/component, or download the script from somewhere
  • Make sure your script is included in the SPFx .sppkg file
  • Load the script runtime when needed

Create a library project

For the script editor web part I started with create-react-library as the basis, fiddled around too many hours and ended up with https://github.com/wobba/sharepoint-modern-script-editor-propertypane with a working webpack file which handles typescript and sass files. I then moved the editor.tsx file from the script editor web part over to this new project. When building, this creates an isolated react component as a .js file.

Include library in the bundle

I went a step further and turned this into an npm package, which when added as a developer dependency will add the following code to gulpfile.js.

let copyDynamic = build.subTask('copy-dynamic-load-files', function (gulp, buildOptions, done) {
gulp.src('./node_modules/sharepoint-modern-script-editor-propertypane/bundles/editor-pop-up.min.js')
    .pipe(gulp.dest('./temp/deploy'))
    .pipe(gulp.dest('./dist'));

done();
});
build.rig.addPostBuildTask(copyDynamic);

What the code does is take the editor-pop-up.min.js file and copy it over to two folders in the SPFx structure.
  • /dist is for loading the script while debugging
  • /temp/deploy is where files are stored which are included in the .sppgk file
To sum it up, any file you copy into /temp/deploy will be included in the .sppkg bundle, and you can load them dynamically at will.

The above pattern can be used with any npm package containing a pre-bundles .js file. You add it as a developer dependency, and load the script dynamically instead of referencing it with require or import statements.

Dynamically load the library

SharePoint framework includes a class to help with loading script dynamically, SPComponentLoader.loadScript. If you deploy files to a custom CDN you know where to load the files from, but when included in the bundle itself you don’t really know the URL where the web part is loaded from. It could be the tenant app catalog, site app catalog or via SharePoint Online CDN.
To get hold of the script path I use a trick to include images in a bundle described in a post by Waldek Mastykarz as well as in an issue on sp-dev-docs. If you add an image to a folder in your solution and reference it with require, you will get the path to that image, and can simply strip off the filename.

private getScriptRoot(): string {
    const runtimePath: string = require('./1x1.png');
    const scriptRoot = runtimePath.substr(0, runtimePath.lastIndexOf("/"));
    return scriptRoot;
}

image

In the above image I created a 1x1 pixel png file which takes 156 bytes. A small price to pay to get the URL. I' will petition to get the script root as a native property.

Note: Pre-SPFx v1.5 you could get the base URL via the manifest object in the web part context. It’s still there, but not exposed via the type definition files, so makes it harder to work with.

Loading and instantiating the component in the case of the script editor web part looks like this:

const editorPopUp: any = await SPComponentLoader.loadScript(this.getScriptRoot() + '/editor-pop-up.min.js', { globalExportsName: "EditorPopUp" });
const element: React.ReactElement<IScriptEditorProps> = React.createElement(
    editorPopUp.default,
    {
        script: this.properties.script,
        title: this.properties.title,
        save: this.save
    }
);
ReactDom.render(element, this.domElement);

And that’s all there is to it :)

Summary

When you create more and more components in SPFx and add them on modern pages, total load and execution time increased as you add more and more third party dependencies in your projects. Having a critical eye as to what is needed in edit mode vs. runtime mode is one way to optimize the JavaScript footprint your component has on a page. Using libraries such as Office UI Fabric and moment can quickly add to the over all size, and they all require processing time on the page. One web part might not be an issue, but when you add five or ten instances of the same part, it might have an impact.

I recommend reading Optimize SharePoint Framework builds for production which explains how you can analyze what library adds size to your bundle.

Check out https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-script-editor and https://github.com/wobba/sharepoint-modern-script-editor-propertypane for the code used in this post.

Questions and Answers

Q: Why not load the external script/component using require, similar to the image bundling? The use of require is evaluated during packaging, which means any script referenced using require will be included in the original script bundle, not allowing a reduction in bundle size.
Q: Why not reference the script using the externals section of config.json instead?
While this would allow you to load the file separately it would be loaded on runtime with the web part or extension, thus not reducing the amount of script on the page.
Q: Can I use this method to load other files except .js files?
In theory you can use this method to add any file you want, and then load or reference them at will – that be images, text files, fonts, style sheets etc. For images using require, might be easier, but it’s all up to your needs.