Viblo Code
+1

Tuning Webpack production environment

Webpack Dev server helps a lot during development, but your production environment might be different and you might need to test those builds as well. And also There are things which are necessary to implement in production environments, which we are going to review in this article.

DLL builds

In the first article I described a process of separating packages into a separate chunk in order to load them quickly, but when your application grows larger, amount of dependencies also increases and vendor file might exceed reasonable size limits.

So it might be required to have more complex logic of separating code into packages.

DLL feature of Webpack can help with that. It is being implemented with 2 plugins: webpack.DllPlugin and webpack.DllReferencePlugin. First should be included into a separate DLL configuration while second in your main build config.

Let’s separate several packages into their own build

// build/templating.config.js
const isProd = process.env.NODE_ENV === 'production'

const { resolve } = require('path')
const { DllPlugin } = require('webpack')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

// Packages included in the build.
const templating = [
    'handlebars',
    'remarkable',
    'highlight.js',
]

const config = {
    name: 'templating',

    // Include source maps in development files
    devtool: isProd ? false : '#cheap-module-source-map',

    node: {
        fs: 'empty'
    },

    entry: {
        // This might contain multiple entries.
        templating,
    },

    output: {
		  path: resolve(__dirname, '..', 'dist'),
        filename: '[name].[hash].dll.js',
        publicPath: '/',
        // Library option defines the name of DLL which will
        // be used by Webpack in JS code
        library: '[name]_[hash]',
    },

    resolve: {
        extensions: ['*', '.js'],
        modules: [
            resolve(__dirname, '..', 'node_modules'),
        ],
        alias: {
            handlebars: 'handlebars/dist/handlebars.min.js',
        }
    },

    module: {
        // In general, this is not required if you only
        // include 3rd party libraries.
        rules: [
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            },
        ],
    },

    plugins: [
          // Here we define the plugin configuration.
          // The manifest file will be used in our main build.
          new DllPlugin({
              path: resolve(__dirname, '..', 'dist', '[name]-manifest.json'),
              name: '[name]_[hash]',
          }),
          new BundleAnalyzerPlugin({
              analyzerMode: isProd ? 'static' : 'disabled',
              generateStatsFile: isProd,
              openAnalyzer: false,
              reportFilename: 'templating.report.html',
              statsFilename: 'templating.stats.json',
          }),
    ],

    profile: isProd,

    performance: {
        hints: 'warning',
        maxEntrypointSize: 400000,
        maxAssetSize: 300000,
    },
}

module.exports = config

And after that we modify our main build (I omitted unchanged parts in this code listing. Refer to previous articles or source code on Github).

// build/base.config.js

// ...

const {
    // ...
    DllReferencePlugin,
    // ...
} = require('webpack')

// ...

let vendor = [
    'jquery',
    'bootstrap',
    'moment',
]

if (!isProd) {
    // Add templating to vendor in dev mode
    vendor = [
        ...vendor,
        ...require('./templating.config').entry.templating
    ]
}

const config = {
    name: 'base',
    dependencies: ['templating'],

    // ...
}

// ...

if (isProd) {
    config.plugins = [
        ...config.plugins,
        new DllReferencePlugin({
            // Reference to manifest created by DLL build.
            manifest: resolve(__dirname, '..', 'dist', 'templating-manifest.json'),
        }),
        // ...
    ]
}

module.exports = config

And the last change is to create a special build file which will include both our configurations.

// build/webpack.config.js

module.exports = [require('./base.config')]

if (process.env.NODE_ENV === 'production') {
    module.exports.push(require('./templating.config'))
}

And package.json changes in scripts:

"scripts": {
  "dev": "webpack-dev-server --hot --inline --config build/base.config.js",
  "build": "rimraf ./dist && NODE_ENV=production webpack --config build/webpack.config.js --hide-modules --progress",
  "watch": "NODE_ENV=production webpack --config build/webpack.config.js --hide-modules --progress --watch",
  "test": "echo \"Error: no test specified\" && exit 1"
}

Ok. The build will run fine now…. but… the templating file has not been loaded. This happened because Html plugin is not aware of another build existence.

There can be many ways to load the file, especially if you have custom server-side rendering or any other backend running.

In this example we will inject the file manually into the template.

First thing is that we need to inform our main build about templating file name. This can be done with AssetsPlugin

npm i -D assets-webpack-plugin
// build/AssetsPlugin.js
const AssetsPlugin = require('assets-webpack-plugin')

// We need this in order to have the same instance of plugin
// across the builds
module.exports = new AssetsPlugin()

And add the plugin both to base.config.js and templating.config.js

plugins: [
     // ...
     require('./AssetsPlugin')
     // ...
]

After this change, there will be a webpack-assets.json file in your project root after every build, which contains information about all entries

{
  "templating": {
    "js": "/templating.7bc7651c99315e4a21da.dll.js"
  },
  "app": {
    "js": "/app.7d39abd19a2473cd2364.js",
    "css":"/style.7d39abd19a2473cd2364.css"
  },
  "vendor": {
    "js": "/vendor.7d39abd19a2473cd2364.js"
  }
}

This file can now be loaded inside our html

new HtmlWebpackPlugin({
    title: 'SPA tutorial',
    template: resolve(__dirname, '..', 'src', 'html', 'index.ejs'),
    chunks: ['app', 'vendor'],
    readManifest: isProd ? () => {
        const assets = fs.readFileSync(resolve(__dirname, '..', 'webpack-assets.json'))
        return JSON.parse(assets)
    } : null,
}),
<!-- inside index.ejs -->
<% if (htmlWebpackPlugin.options.readManifest !== null) { %>
    <script type="text/javascript" src="<%= htmlWebpackPlugin.options.readManifest().templating.js %>"></script>
<% } %>

Local server

There are several options available to set up local server. In this article we focus on Caddy server configuration.

In order to speed up the process and avoid OS-specific configurations, we will use docker to run the servers.

Let’s create etc directory and a Caddy config file

Caddyfile

# etc/Caddyfile

http://spa.local { # You can replace the address with any other
    root /srv/dist
    gzip
    index index.html
}

Now the server can be run with following command:

docker run --rm --name spa_caddy -v $(pwd)/etc/Caddyfile:/etc/Caddyfile -v $(pwd):/srv -p 80:80 abiosoft/caddy

You can add this line to npm scripts for convenience.

CDN

Your application will most probably use cookies and sessions in order to manage user-specific data and that data will be passed with every request to your assets, which is not the best practice. It also will affect your page speed rank.

To avoid that, you can load your assets from a separate cookie-less domain, e. g. cdn.spa.local

# Caddyfile
http://cdn.spa.local {
    root /srv/dist

    gzip

    index goaway.png

    status 404 {
        /index.html
    }
}

http://spa.local {
    root /srv/dist

    gzip

    index index.html
}

Now you can add these values to .env file and use it in webpack config:

# .env file
DEBUG=true
WEBPACK_PUBLIC_PATH=http://cdn.spa.local/

And use it as publicPath in output section

publicPath: (isProd) ? process.env.WEBPACK_PUBLIC_PATH : '/',

Now all the assets in production will be loaded from CDN domain.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.