Increasing Umbraco Performance with webpack

Heads Up!

This article is several years old now, and much has happened since then, so please keep that in mind while reading it.

While web project standards tend to grow more and more complex, one of the most important challenges for developers – and requests by clients for that matter – is project performance. The site, no matter how complex the data it handles for each visit, has to run smoothly like a sprinter without losing any of its stamina with months and years going by.

 

A basic strategy to achieve this is by bundling and minifying your .js and .css files. At byte5, we have used all sorts of tools for this task over the past years: Microsoft's Web Optimization Framework for instance or, more recently, task runner tools such as gulp and grunt.

 

Over the last months, however, webpack has become our new favourite „weapon of choice“. The tool is a module bundler, not a task runner like gulp or grunt. Due to webpack's versatility, however, you can often go without the combination of various tools and use webpack for any minifying task you want to carry out. It doesn't only bundle Javascript and CSS but also solves a number of other challenges: compiling SCSS to CSS, compiling Typescript, optimising images, and a lot more.

 

What some might call a disadvantage with webpack is the fact that it's initially not that easy to use, since the configuration can get a little tricky at first. In this article, I'll show you a step-by-step approach to configure the tool for its most important tasks – a basis that you also might like to use for future Umbraco projects.

 

Step 1: Installation and configuration

 

In order to get webpack running in your project, you first need to install Node.js. On the official Node.js website you can easily download the latest version. Another useful tool for this step is webpack Task Runner for Visual Studio. The extension tool allows you to use webpack directly in your Visual Studio project.

 

Next thing we need is a package.json file in our Umbraco project's root directory, additionally to the packages.config file that we've already got. The file is required by Node.js package manager (npm) that we will shortly use to install webpack in our project.

 

You can use Visual Studio or the command line to create the file by executing the following command:

 

npm init -f

 

This creates a standard package.json file for our project which should look something like this:

 

{
  "name": "UmbracoWebpackExample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

 

Now we can easily install webpack with the following command:

npm install --save-dev webpack

 

If the installation was successful, you should see the folder node_modules in your root directory, which contains all the packages added by npm.

 

Step 2: The First Bundle

The next file we have to create in the root directory is webpack.config.js. If you've already added webpack Task Runner to your Visual Studio, you can easily use the pre-installed template for webpack.config files.

 

If you want to use webpack for its core task – bundling .js files –, a simple config file could look something like this:

const path = require('path');

module.exports = {
    entry: {
        app: [
            './scripts/script1.js',
            './scripts/script2.js'
        ]
    },
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    }
};

 

entry: This defines the entries for all bundles. In our example, there is only one bundle called app, which consists of two files (script1.js and script2.js). You can, however, define as many bundles and files as you need.

 

output: This defines where the files that webpack creates are supposed to be put. In our example, we want to use the “dist” folder using the name scheme [name].js. In this case, name stands for the name of the bundle (app).

 

Now we can execute webpack (easily by using the webpack Task Runner in Visual Studio) and get the file app.js in the “dist” folder that was automatically created by the tool.

 

Step 3: Minifying

If you are planning to use the bundle you just created live in production, it makes sense to minify the script after you've bundled it. In order to take care of that, webpack needs the plugin UglifyJs, which can be installed via npm:

npm install --save-dev uglifyjs-webpack-plugin

Now we've got to add UglifyJs to the plugins in our webpack.config and our bundle is getting minified:

const path = require('path');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
    entry: {
        app: [
            './scripts/script1.js',
            './scripts/script2.js'
        ]
    },
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    },
    plugins: [
        new UglifyJsPlugin()
    ]
};

 

You can easily configure the plugin for various applications. For instance, you can include or exclude certain files or configure compatibility with everyone's favourite browser: IE8, to which the standard UglifyJs minified files are not compatible. Yeah, right? I've got no explanation for that either!

 

Step 4: Dev and Prod Build

At this point our bundle is always automatically minified, which sounds quite nice but will prove to be unnecessary and irritating while working on the project. We would need a way to make our minifier recognise whether we are in development or production mode, so that the bundle only gets minified for live configuration.

 

A solution to this problem is to define the mode by an environment variable when executing webpack. Then you can access the variable in webpack.config and depending on the value execute a number of configurations.

 

const path = require('path');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

const isDev = process.env.NODE_ENV === 'development';

module.exports = {
    entry: {
        app: [
            './scripts/script1.js',
            './scripts/script2.js'
        ]
    },
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    },
    plugins: [
        // Plugins which should always be executed
    ].concat(isDev ?
        [
            // Only exectued in dev environment
        ] :
        [
            // Only exectued in prod environment
            new UglifyJsPlugin()
        ])
};

 

In line 4, we check whether the active environment variable has the value “development”, so that only certain plugins will be executed. In our case, the plugin UglifyJs will only be executed in live mode. For the developer mode, no plugins have been activated.

 

If you're using the plugin webpack Task Runner for Visual Studio, you've got the advantage that both modes are already pre-configured. In this manner, switching between modes is executed by one mouse click.

Step 5: ES6 and Babel

Javascript has developed a lot over the past years and new versions such as ES6 offer lots of new functions that improve development tasks with Js. The only problem behind ES6 is, however, that not all browsers support the new features. That’s why there are tools like Babel, which compiles modern Javascript with ES6 functionalities into ES5 compatible Javascript. Babel makes sure everything runs on older browser versions as well as brand-new ones.

 

babel-loader for webpack allows us to easily integrate this step right when creating our bundle. But first, we have to install babel-loader via npm.

npm install babel-loader babel-core babel-preset-env --save-dev

Then we just have to adapt our webpack.config. As a result, ES6 Javascript is compiled into fully ES5 compatible Javascript.

 

const path = require('path');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

const isDev = process.env.NODE_ENV === 'development';

module.exports = {
    entry: {
        app: [
            './scripts/script1.js',
            './scripts/script2.js'
        ]
    },
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: [/node_modules/],
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['babel-preset-env']
                    }
                }
            }
        ]
    },
    plugins: [
        // Plugins which should always be executed
    ].concat(isDev ?
        [
            // Only exectued in dev environment
        ] :
        [
            // Only exectued in prod environment
            new UglifyJsPlugin()
        ])
};

 

A new addition here is the module object where we can define rules that influence what kind of files the various loaders apply to. In our case, we want to make sure babel-loader is applied on all Javascript files that are located outside of the node_modules folder.

Step 6: Compiling SCSS/SASS

Another way in which webpack makes our everyday work life easier is when it comes to compiling SASS to normal CSS. For webpack to fulfil this task, we need to install a few extensions via npm.

npm install sass-loader node-sass extract-text-webpack-plugin css-loader --save-dev

When all necessary plugins have been installed, we only adapt our webpack.config.

const path = require('path');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

const ExtractTextPlugin = require("extract-text-webpack-plugin");
const extractSass = new ExtractTextPlugin({
    filename: "styles.css"
});

const isDev = process.env.NODE_ENV === 'development';

module.exports = {
    entry: {
        app: [
            './scripts/script1.js',
            './scripts/script2.js',
            './Scss/main.scss'
        ]
    },
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: [/node_modules/],
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['babel-preset-env']
                    }
                }
            },
            {
                test: /\.scss$/,
                use: extractSass.extract({
                    use: [{
                        loader: 'css-loader'
                    }, {
                        loader: 'sass-loader'
                    }]
                })
            }
        ]
    },
    plugins: [
        // Plugins which should always be executed
        extractSass
    ].concat(isDev ?
        [
            // Only exectued in dev environment
        ] :
        [
            // Only exectued in prod environment
            new UglifyJsPlugin()
        ])
};

 

First, we configure the ExtractTextPlugin. The plugin will later write the compiled SCSS into a new CSS file (styles.css). Additionally, a new rule for the module object was added, which controls that all .scss files will be compiled by the sass-loader and css-loader. Then they will be added to the new file by the ExtractTextPlugin. As a final step, we add the ExtractTextPlugin to the plugins object.

 

If everything is configured correctly, the new file should be located in the “dist” folder right after you've executed webpack once again.

 

Step 7: CI/CD Pipeline

And another final tip: If you've configured a CI/CD pipeline for your Umbraco project, it's certainly a great idea to run webpack and create your bundles automatically when building your project. An easier way to do this is expanding the .csproj of your Umbraco project.

<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="BeforeBuild" Condition="'$(Configuration)' != 'debug'">
    <Exec Command="npm install" />
    <Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />
  </Target>
</Project>

Therefore, you just have to add the target element before the closing </Project> tag. In this way, we add our tasks to the BeforeBuild event of the project. Now every time a project is created the two Exec commands initiate the installation of the npm packages as well as the execution of webpack.

 

Since you probably don't want webpack to be added to every build – even locally in our dev environment – you add an additional condition to the target element. In this way, the commands will only be triggered, if the build configuration is not set to “Debug”. In order to make this work for automatic builds on your build server, you go back to step one and install Node on the server.

Dennis Zille