Optimizing Office web apps and add-ins for release with Gulp

Minified contents of an Office Add-in generated using the Yeoman Office Generator
When building Office web applications and add-ins there comes the time where they are ready for release. What's good for development however, isn't necessarily leading to best performance. Luckily using Gulp you can pretty easily improve your solution's performance.

When developing solutions, separating the different components simplifies management and makes it easier for the different developers to work on the different parts of the application. Because the different pieces are kept in separate files, there is less risk of merge conflicts and in case of errors, it's easier to locate where the particular piece of code is located.

Depending on your application it might consists of a couple to a few dozen files varying from external libraries to the different pieces of the application. When starting your application, often it needs to load the majority of these files first. If these files are relatively small and spread across different hosts, it will likely take longer for your application to start.

Using a Gulp task you can optimize your Office web application or add-in by:

  • prebuilding the Angular template cache
  • merging and minifying CSS and JavaScript files (both specific to your application and from external libraries)
  • minifying HTML files
  • setting far future expire header for static resources

The great news is, that using Gulp you can easily setup all of the above without any impact on your development process. So how would you do it?

Let's start by creating a new Office Add-in using the Yeoman Office Generator. In this example we will build an ADAL JS Taskpane Add-in.

Taskpane Office Add-in generated using the Yeoman Office Generator

Office add-ins generated using the Yeoman Office Generator v0.6.3 have a dist Gulp task that minifies add-ins CSS and JavaScript files. If we run this task, we will see the different assets copied to the dist folder: even though the CSS and JavaScript files are minified they are still separate files that the application needs to download one by one. Not to mention the bower dependencies referenced in the index.html file.

Contents of the release version of an Office Add-in generated using the Yeoman Office Generator

What if our add-in could look like this after running the dist task:

Optimized contents of the release version of an Office Add-in generated using the Yeoman Office Generator

The source code of a working sample using the optimized Gulp dist task is available on GitHub at https://github.com/waldekmastykarz/sample-yooffice-bundles. The commits in the repository correspond to the chapters in this article.

Wiring bower dependencies in index.html

In the first step we will configure automatically wiring up bower dependencies in the index.html file. Although this step on its own doesn't optimize the performance of our add-in it's an important step in the optimization process that we will use later in the process.

We do this using the gulp-wiredep plugin which we need to install first using:

$ npm i gulp-wiredep --save-dev

Next, let's replace the references to external libraries with their local equivalents installed using bower:

<!DOCTYPE html>  
<html>  
<head>  
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title></title>
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" />

  <!-- bower:css -->
  <link rel="stylesheet" href="bower_components/office-ui-fabric/dist/css/fabric.css" />
  <link rel="stylesheet" href="bower_components/office-ui-fabric/dist/css/fabric.components.css" />
  <link href="bower_components/microsoft.office.js/content/officethemes.css" rel="stylesheet" type="text/css" />
  <!-- endbower -->
  <link href="content/Office.css" rel="stylesheet" type="text/css"/>
  <link href="content/app.css" rel="stylesheet" type="text/css"/>
  <!-- bower:js -->
  <script src="bower_components/jquery/dist/jquery.js"></script>
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
  <script src="bower_components/adal-angular/lib/adal.js"></script>
  <script src="bower_components/adal-angular/lib/adal-angular.js"></script>
  <script src="bower_components/office-ui-fabric/dist/js/jquery.fabric.js"></script>
  <!-- endbower -->
  <script src="//appsforoffice.microsoft.com/lib/1/hosted/Office.js"></script>
</head>  
<body>

  <div id="container">
    <div data-ng-view></div>
  </div>

  <script src="app/app.module.js"></script>
  <script src="app/app.config.js"></script>
  <script src="app/app.adalconfig.js"></script>
  <script src="app/app.routes.js"></script>
  <script src="app/services/data.service.js"></script>
  <script src="app/home/home.controller.js"></script>

</body>  
</html>  

https://github.com/waldekmastykarz/sample-yooffice-bundles/blob/d6cb9b9b91f99dc2d9243917eef0e509b2a3187c/index.html

Although it's not required to specify static links to all resources between wiredep's comments, if we do that we will be able to run both optimized and unoptimized versions of our add-in.

If you look closer at the sample code you’ll notice that on line 25 we still reference the hosted version of the Office.js script and that it’s located outside of wiredep’s comments. The reason for that is, that the Office store publication guidelines require add-ins to reference the hosted version of the library rather than its local copy.

Then, let's extend the existing dist task to use wiredep:

gulp.task('dist-wiredependencies', function() {  
    gulp.src('./index.html')
        .pipe($.wiredep({
            exclude: 'bower_components/microsoft.office.js/scripts/office/1/office.js'
        }))
        .pipe(gulp.dest(config.release));
});

/**
 * Creates a release version of the project
 */
gulp.task('dist', function () {  
  runSequence(
    ['dist-remove'],
    ['dist-wiredependencies'],
    ['dist-copy-files'],
    ['dist-minify']
    );
});

https://github.com/waldekmastykarz/sample-yooffice-bundles/blob/d6cb9b9b91f99dc2d9243917eef0e509b2a3187c/gulpfile.js

Also here we need to exclude the office.js file from being processed by wiredep (line 4).

If we run the dist task, we would get output nearly identical to the default dist task output. While not much has changed yet, we are now able to get references to all dependencies in the dist task. In the next step we will use these references and merge them with add-in's assets.

Combining external dependencies with add-in's files with useref

In the previous step we used the wiredep plugin to automatically wire bower dependencies with the application. In this step we will use the useref plugin to merge bower dependencies with add-in's files to minimize the number of files to be downloaded.

Let's start by installing the gulp-useref plugin:

$ npm i gulp-useref --save-dev

Next, let's wrap references to add-in's files with useref's tags:

<!DOCTYPE html>  
<html>  
<head>  
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title></title>
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" />

  <!-- build:css content/styles.css -->
  <!-- bower:css -->
  <link rel="stylesheet" href="bower_components/office-ui-fabric/dist/css/fabric.css" />
  <link rel="stylesheet" href="bower_components/office-ui-fabric/dist/css/fabric.components.css" />
  <link href="bower_components/microsoft.office.js/content/officethemes.css" rel="stylesheet" type="text/css" />
  <!-- endbower -->
  <link href="content/Office.css" rel="stylesheet" type="text/css"/>
  <link href="content/app.css" rel="stylesheet" type="text/css"/>
  <!-- endbuild -->

  <script src="//appsforoffice.microsoft.com/lib/1/hosted/Office.js"></script>
  <!-- build:js scripts/app.js -->
  <!-- bower:js -->
  <script src="bower_components/jquery/dist/jquery.js"></script>
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
  <script src="bower_components/adal-angular/lib/adal.js"></script>
  <script src="bower_components/adal-angular/lib/adal-angular.js"></script>
  <script src="bower_components/office-ui-fabric/dist/js/jquery.fabric.js"></script>
  <!-- endbower -->
  <script src="app/app.module.js"></script>
  <script src="app/app.config.js"></script>
  <script src="app/app.adalconfig.js"></script>
  <script src="app/app.routes.js"></script>
  <script src="app/services/data.service.js"></script>
  <script src="app/home/home.controller.js"></script>
  <!-- endbuild -->
</head>  
<body>

  <div id="container">
    <div data-ng-view></div>
  </div>

</body>  
</html>  

https://github.com/waldekmastykarz/sample-yooffice-bundles/blob/4ef3e68bea09ee9d1102e63cdec721b6b81d2ebb/index.html

Please note how the useref tags wrap wiredep's tags. This is because we want external dependencies to be merged with internal resources. If you would prefer to keep both types of resources separate, you would have to place useref's comments outside of wiredep's comments.

Next, let's extend the dist-wiredependencies task to include useref:

gulp.task('dist-copy-files', function() {  
  return gulp.src([
    './app/**/*.html',
    './images/**/*',
    './manifest-*.xml',
    './package.json'
  ], { base: './' }).pipe(gulp.dest(config.release));
});

gulp.task('dist-wiredependencies', function() {  
    gulp.src('./index.html')
        .pipe($.wiredep({
            exclude: 'bower_components/microsoft.office.js/scripts/office/1/office.js'
        }))
        .pipe($.useref())
        .pipe(gulp.dest(config.release));
});

/**
 * Creates a release version of the project
 */
gulp.task('dist', function () {  
  runSequence(
    ['dist-remove'],
    ['dist-wiredependencies'],
    ['dist-copy-files']
    );
});

Because CSS and JavaScript files are now combined using useref we no longer need to copy the complete bower_components folder to the dist folder and which has been removed from the dist-copy-files task.

If we run the dist task now, we will see the different CSS and JavaScript files combined together:

Contents of the release version of an Office Add-in optimized using wiredep and useref

Preloading Angular templates in the template cache

Looking at our add-in there is one more type of files that isn't currently included in the generated bundles: Angular templates.

Angular uses templates to display information on the screen. The first time a template is requested, it's downloaded from the web server and added to the template cache. If your application always uses a number of templates you can lower the number of requests by prebuilding the template cache. This can be done as a part of your Gulp build task using the gulp-angular-templatecache plugin.

Let's start by installing the plugin:

$ npm i gulp-angular-templatecache --save-dev

Next, let's create a separate task that will add all templates to the template cache:

gulp.task('dist-templatecache', function() {  
    return gulp.src('./app/**/*.html')
        .pipe($.angularTemplatecache(
            'templates.js',
            {
                module: 'officeAddin',
                root: 'app/',
                standAlone: false
            }
        ))
        .pipe(gulp.dest(config.tmp));
});

https://github.com/waldekmastykarz/sample-yooffice-bundles/blob/a589d46b285f63f41d30e24edce1ad16d95b881a/gulpfile.js#L208-L219

The gulp-angular-templatecache plugin creates a separate JavaScript file that adds templates to the template cache and which should be referenced by loaded together with the Angular application. One way to do this is using the gulp-inject plugin. This plugin takes a list of files and injects their references to .html files as specified by placeholders.

Let's start by installing the plugin:

$ npm i gulp-inject --save-dev

Next, let's extend the dist-wiredependencies task to use the newly added dist-templatecache task and take the generated templates file and associate it with an inject placeholder:

gulp.task('dist-wiredependencies', ['dist-templatecache'], function() {  
    gulp.src('./index.html')
        .pipe($.wiredep({
            exclude: 'bower_components/microsoft.office.js/scripts/office/1/office.js'
        }))
        .pipe($.inject(gulp.src('./tmp/templates.js', {read:false}), {name:'templates'}))
        .pipe($.useref())
        .pipe(gulp.dest(config.release));
});

https://github.com/waldekmastykarz/sample-yooffice-bundles/blob/a589d46b285f63f41d30e24edce1ad16d95b881a/gulpfile.js#L221-L229

Finally, let's add the placeholder so that the generated templates file will be referenced in index.html.

<!DOCTYPE html>  
<html>  
<head>  
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title></title>
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" />

  <!-- build:css content/styles.css -->
  <!-- bower:css -->
  <link rel="stylesheet" href="bower_components/office-ui-fabric/dist/css/fabric.css" />
  <link rel="stylesheet" href="bower_components/office-ui-fabric/dist/css/fabric.components.css" />
  <link href="bower_components/microsoft.office.js/content/officethemes.css" rel="stylesheet" type="text/css" />
  <!-- endbower -->
  <link href="content/Office.css" rel="stylesheet" type="text/css"/>
  <link href="content/app.css" rel="stylesheet" type="text/css"/>
  <!-- endbuild -->

  <script src="//appsforoffice.microsoft.com/lib/1/hosted/Office.js"></script>
  <!-- build:js scripts/app.js -->
  <!-- bower:js -->
  <script src="bower_components/jquery/dist/jquery.js"></script>
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
  <script src="bower_components/adal-angular/lib/adal.js"></script>
  <script src="bower_components/adal-angular/lib/adal-angular.js"></script>
  <script src="bower_components/office-ui-fabric/dist/js/jquery.fabric.js"></script>
  <!-- endbower -->
  <script src="app/app.module.js"></script>
  <script src="app/app.config.js"></script>
  <script src="app/app.adalconfig.js"></script>
  <script src="app/app.routes.js"></script>
  <script src="app/services/data.service.js"></script>
  <script src="app/home/home.controller.js"></script>
  <!-- templates:js -->
  <!-- endinject -->
  <!-- endbuild -->
</head>  
<body>

  <div id="container">
    <div data-ng-view></div>
  </div>

</body>  
</html>  

https://github.com/waldekmastykarz/sample-yooffice-bundles/blob/a589d46b285f63f41d30e24edce1ad16d95b881a/index.html#L36-L37

Because the templates placeholder is included within the useref tag, it will be combined with other scripts.

If we run the dist task now, we see how the contents of the home template are included in the combined script file.

Angular templates precached and merged with other scripts belonging to the Office Add-in

These files are however still pretty large and can be further optimized through minification.

Minifying files

Our Office add-in uses three types of files that can be minified: HTML, CSS and JavaScript files. Different Gulp plugins can be used to minify these files.

Minifying HTML files

HTML files can be minified using the gulp-htmlmin plugin. In our add-in there are two HTML files that need to be minified: index.html and the home.html Angular template file.

Let's start by installing the plugin:

$ npm i gulp-htmlmin --save-dev

Next, let's call the plugin from our dist task. First we add it to the dist-templatecache task:

gulp.task('dist-templatecache', function() {  
    return gulp.src('./app/**/*.html')
        .pipe($.htmlmin({
            collapseWhitespace: true
         }))
        .pipe($.angularTemplatecache(
            'templates.js',
            {
                module: 'officeAddin',
                root: 'app/',
                standAlone: false
            }
        ))
        .pipe(gulp.dest(config.tmp));
});

https://github.com/waldekmastykarz/sample-yooffice-bundles/blob/9e9912b97e2882f03f1c2987c0acaf2550e1aff7/gulpfile.js#L210-L212

This will minify Angular templates before adding them to the template cache.

Next, let's minify the index.html file. Here it's essential that we call the htmlmin plugin only for HTML files to prevent the plugin for trying to process other types of files. We do this using the gulp-if plugin that allows us to execute additional pipeling processing instruction based on a condition.

Let's install the plugin:

$ npm i gulp-if --save-dev

and extend the dist-wiredependencies task:

gulp.task('dist-wiredependencies', ['dist-templatecache'], function() {  
    gulp.src('./index.html')
        .pipe($.wiredep({
            exclude: 'bower_components/microsoft.office.js/scripts/office/1/office.js'
        }))
        .pipe($.inject(gulp.src('./tmp/templates.js', {read:false}), {name:'templates'}))
        .pipe($.useref())
        .pipe($.if('*.html', $.htmlmin({collapseWhitespace: true})))
        .pipe(gulp.dest(config.release));
});

If we run the dist task now, we can see the minified contents of the index.html file and the Angular templates stored in the template cache.

Minified HTML of the Office Add-in generated using the Yeoman Office Generator

Minifying CSS stylesheets

CSS stylesheets can be minified using the gulp-cssnano plugin. Similar to minifying HTML files, the cssnano plugin should only be called when processing CSS files.

Let's start by installing the plugin:

$ npm i gulp-cssnano --save-dev

Next, let's extend the dist-wiredependencies task:

gulp.task('dist-wiredependencies', ['dist-templatecache'], function() {  
    gulp.src('./index.html')
        .pipe($.wiredep({
            exclude: 'bower_components/microsoft.office.js/scripts/office/1/office.js'
        }))
        .pipe($.inject(gulp.src('./tmp/templates.js', {read:false}), {name:'templates'}))
        .pipe($.useref())
        .pipe($.if('*.html', $.htmlmin({collapseWhitespace: true})))
        .pipe($.if('*.css', $.cssnano()))
        .pipe(gulp.dest(config.release));
});

https://github.com/waldekmastykarz/sample-yooffice-bundles/blob/f1897960483a072687140b919874aeaf9dcee09c/gulpfile.js#L232

If we run the dist task now, we can see the minified contents of the combined CSS file.

Minified CSS of an Office Add-in generated using the Yeoman Office Generator

Minifying JavaScript files

JavaScript files can be minified using the gulp-uglify plugin which should be executed only for JavaScript files.

Let's install the plugin:

$ npm i gulp-uglify --save-dev

Then, let's extend the dist-wiredependencies task:

gulp.task('dist-wiredependencies', ['dist-templatecache'], function() {  
    gulp.src('./index.html')
        .pipe($.wiredep({
            exclude: 'bower_components/microsoft.office.js/scripts/office/1/office.js'
        }))
        .pipe($.inject(gulp.src('./tmp/templates.js', {read:false}), {name:'templates'}))
        .pipe($.useref())
        .pipe($.if('*.html', $.htmlmin({collapseWhitespace: true})))
        .pipe($.if('*.css', $.cssnano()))
        .pipe($.if('*.js', $.uglify()))
        .pipe(gulp.dest(config.release));
});

If we run the dist task now, we will see the minified contents of the combined JavaScript file.

Minified content of JavaScript files of an Office Add-in generated using the Yeoman Office Generator

Optimizing files for client-side caching

After releasing the application, a good practice to improve its performance is to allow clients to cache its files. Ideally clients shouldn't request files unless they have been changed. A common technique to achieve this, is to configure the web server to set far future cache expiration header on static files.

The consequence of this technique is that clients will always load application's files from the local cache unless that cache has been invalidated. So if you want to push an update to your application, you want to have a mechanism in place that will allow you to do so without having to contact your users and ask them to clear their cache.

One way to support pushing updates to clients when using the far future cache expiration date is to include a token in the file URL that identifies a specific release. This could be the release date, release version or some other arbitrary token related to a specific release of your application.

Following is how you could use the version number stored in the package.json file for this purpose.

Let's start by changing the names of the files generated by useref defined in the index.html file:

<!DOCTYPE html>  
<html>  
<head>  
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title></title>
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" />

  <!-- build:css content/styles.@packageVersion@.css -->
  <!-- bower:css -->
  <link rel="stylesheet" href="bower_components/office-ui-fabric/dist/css/fabric.css" />
  <link rel="stylesheet" href="bower_components/office-ui-fabric/dist/css/fabric.components.css" />
  <link href="bower_components/microsoft.office.js/content/officethemes.css" rel="stylesheet" type="text/css" />
  <!-- endbower -->
  <link href="content/Office.css" rel="stylesheet" type="text/css"/>
  <link href="content/app.css" rel="stylesheet" type="text/css"/>
  <!-- endbuild -->

  <script src="//appsforoffice.microsoft.com/lib/1/hosted/Office.js"></script>
  <!-- build:js scripts/app.@packageVersion@.js -->
  <!-- bower:js -->
  <script src="bower_components/jquery/dist/jquery.js"></script>
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
  <script src="bower_components/adal-angular/lib/adal.js"></script>
  <script src="bower_components/adal-angular/lib/adal-angular.js"></script>
  <script src="bower_components/office-ui-fabric/dist/js/jquery.fabric.js"></script>
  <!-- endbower -->
  <script src="app/app.module.js"></script>
  <script src="app/app.config.js"></script>
  <script src="app/app.adalconfig.js"></script>
  <script src="app/app.routes.js"></script>
  <script src="app/services/data.service.js"></script>
  <script src="app/home/home.controller.js"></script>
  <!-- templates:js -->
  <!-- endinject -->
  <!-- endbuild -->
</head>  
<body>

  <div id="container">
    <div data-ng-view></div>
  </div>

</body>  
</html>  

https://github.com/waldekmastykarz/sample-yooffice-bundles/blob/329abb774e875b6c03d553df8640594a34fdcc1d/index.html

We introduce the @packageVersion@ token in the file name which we will replace with the version number stored in the package.json file.

Next, let's extend the dist-wiredependencies task and retrieve the version number from the package.json file:

var packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));  

https://github.com/waldekmastykarz/sample-yooffice-bundles/blob/329abb774e875b6c03d553df8640594a34fdcc1d/gulpfile.js#L225

Finally we need to replace the @packageVersion@ token in the contents of the index.html file with the version number retrieved from the package.json file. One way to do this is to use the gulp-replace plugin:

gulp.task('dist-wiredependencies', ['dist-templatecache'], function() {  
    var packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));

    gulp.src('./index.html')
        .pipe($.replace([email protected]@', packageJson.version))
        .pipe($.wiredep({
            exclude: 'bower_components/microsoft.office.js/scripts/office/1/office.js'
        }))
        .pipe($.inject(gulp.src('./tmp/templates.js', {read:false}), {name:'templates'}))
        .pipe($.useref())
        .pipe($.if('*.html', $.htmlmin({collapseWhitespace: true})))
        .pipe($.if('*.css', $.cssnano()))
        .pipe($.if('*.js', $.uglify()))
        .pipe(gulp.dest(config.release));
});

https://github.com/waldekmastykarz/sample-yooffice-bundles/blob/329abb774e875b6c03d553df8640594a34fdcc1d/gulpfile.js#L228

If we run the dist task now, we will see how the combined CSS and JavaScript files contain the package version number as defined in the package.json file. Having set the far future cache expiration date on the web servers these files would be cached on the client until the cache would be cleared or until the index.html file would be updated with a reference to a new version of these files.

Bonus: test optimized version of the add-in

By default Office add-ins generated using the Yeoman Office Generator contain the serve-static task which starts a local web server serving the unoptimized versions of the add-in's files.

If you would like to test the optimized version of your add-in, you could add the following serve-dist task to your gulpfile.js file:

gulp.task('serve-dist', ['dist'], function () {  
  gulp.src('./dist')
    .pipe(webserver({
      https: true,
      port: '8443',
      host: 'localhost',
      directoryListing: true,
      fallback: 'index.html'
    }));
});

https://github.com/waldekmastykarz/sample-yooffice-bundles/blob/b2a46be3a07f50882dd9c0cecea14aff476fcd13/gulpfile.js#L240-L249

Running this is very similar to running the serve-static task with the only difference that it serves the optimized version of the add-in from the dist folder rather than the raw files used for development.

Summary

When building Office web applications and add-ins there comes the time where they are ready for release. Using Gulp you can relatively easily build a task that will optimize the different assets for performance and have your application load and work faster.

Big thanks to Stefan Bauer for helping me out throughout the different stages of the process. Stefan is currently working on implementing a similar process as a part of the Yeoman Office Generator and I'm looking forward to see his take on this idea.

Comments

comments powered by Disqus