How to configure Webpack 4 with VueJS : a complete guide

Samuel Teboul
Vue.js Developers
Published in
9 min readMar 6, 2019
webpack & Vue logos

I have just started a new job at a young startup that develops its front-end part using VueJS. As a developer with an Angular background, I started very skeptical. However, I have grown to appreciate VueJS for its easy understanding and use. For the past 2 months, I have worked on many parts of the application that have enabled me to greatly develop my VueJS knowledge. One of my latest responsability has been to upgrade webpack in order to optimize the company’s dashboard.

In this article, I will describe a step-by-step guide on how to configure webpack to optimize any VueJS application.

Install project dependencies

Create your package.json file and start by installing Vue and webpack dependencies.

"dependencies": {
"@babel/polyfill": "~7.2",
"vue": "~2.6",
"vue-router": "~3.0"
},
"devDependencies": {
"@babel/core": "~7.2",
"@babel/plugin-proposal-class-properties": "~7.3",
"@babel/plugin-proposal-decorators": "~7.3",
"@babel/plugin-proposal-json-strings": "~7.2",
"@babel/plugin-syntax-dynamic-import": "~7.2",
"@babel/plugin-syntax-import-meta": "~7.2",
"@babel/preset-env": "~7.3",
"babel-loader": "~8.0",
"compression-webpack-plugin": "~2.0",
"cross-env": "~5.2",
"css-loader": "~0.28",
"friendly-errors-webpack-plugin": "~1.7",
"html-webpack-plugin": "~3.2",
"mini-css-extract-plugin": "~0.5",
"node-sass": "~4.11",
"optimize-css-assets-webpack-plugin": "~3.2",
"sass-loader": "~7.1",
"uglifyjs-webpack-plugin": "~1.2",
"vue-loader": "~15.6",
"vue-style-loader": "~4.1",
"vue-template-compiler": "~2.6",
"webpack": "~4.29",
"webpack-bundle-analyzer": "~3.0",
"webpack-cli": "~3.2",
"webpack-dev-server": "~3.1",
"webpack-hot-middleware": "~2.24",
"webpack-merge": "~4.2"
}
webpack & Babel logos

What is Babel? According to Babel official website:

Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. Here are the main things Babel can do for you:

- Transform syntax

- Polyfill features that are missing in your target environment (through @babel/polyfill)

- Source code transformations (codemods)

- And more!

Babel is a compiler (source code => output code). Like many other compilers it runs in 3 stages: parsing, transforming, and printing.

Now, out of the box Babel doesn’t do anything. It basically acts like const babel = code => code; by parsing the code and then generating the same code back out again. You will need to add plugins for Babel to do anything.

Instead of individual plugins, you can also enable a set of plugins in a preset.

.babelrc configuration

{
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8", "ie >= 11"]
}
}
]
],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
]
}
  • @babel/preset-env allows you to use the latest javascript version without worrying which syntax transforms you will eventually need for your target environments.
  • plugin-syntax-dynamic-import is essential to be able to use lazy loading. Otherwise webpack will not compile this syntax const AppHome= () => import("@/components/AppHome");

The base: common webpack configuration

This section will define settings that are shared across all environments in your application: entry files, plugins and loaders.

Entry — An entry point indicates which module webpack should use to begin building out its internal dependency graph. Webpack will figure out which other modules and libraries that entry point depends on (directly and indirectly). Below are 2 different entry points : polyfill and main

entry: {
polyfill: '@babel/polyfill',
main: helpers.root('src', 'main'),
}

Resolve — These options change how modules are resolved.

resolve: {
extensions: [ '.js', '.vue' ],
alias: {
'vue$': isDev ? 'vue/dist/vue.runtime.js' : 'vue/dist/vue.runtime.min.js',
'@': helpers.root('src')
}
}
  • extensions: enable users to avoid writing the extension when importing

Instead of writing:

import AppHome from "../../components/AppHome.vue";

You can write:

import AppHome from "@/components/AppHome";
  • alias: allows toimport or require modules more easily

Instead of using relative paths when importing…

import AppHome from "../../components/AppHome";

…you can use the alias that was just created

import AppHome from "@/components/AppHome";

In the dist/ folder of the vue package, you will find many different builds of Vue.js. Here’s an overview of the differences between them.

Explanation of different builds

In our case, vue is mapped to vue.runtime.js . runtime references to the code that is responsible for creating Vue instances, rendering and patching virtual DOM, etc... Basically, everything minus the compiler. It is essential to understand this concept because we always want to send less KB as possible to the browser.

To make the application properly work without the compiler, make sure to never declare a component with string templates…

// this requires the compiler
new Vue({
template: '<div>{{ hi }}</div>'
})

…and use a render function:

// this doesn't require the compiler
new Vue({
render (h) {
return h('div', this.hi)
}
})

Loaders — webpack enables use of loaders to preprocess files.

module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
include: [ helpers.root('src') ]
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [ helpers.root('src') ]
},
{
test: /\.css$/,
use: [
isDev ? 'vue-style-loader' : MiniCSSExtractPlugin.loader,
{ loader: 'css-loader', options: { sourceMap: isDev } },
]
},
{
test: /\.scss$/,
use: [
isDev ? 'vue-style-loader' : MiniCSSExtractPlugin.loader,
{ loader: 'css-loader', options: { sourceMap: isDev } },
{ loader: 'sass-loader', options: { sourceMap: isDev } }
]
},
{
test: /\.sass$/,
use: [
isDev ? 'vue-style-loader' : MiniCSSExtractPlugin.loader,
{ loader: 'css-loader', options: { sourceMap: isDev } },
{ loader: 'sass-loader', options: { sourceMap: isDev } }
]
}
]
}

Some webpack loaders are pretty standard:

  • .html files are loaded with html-loader
  • .vue files are loaded with vue-loader
  • .js files are loaded with babel-loader

The way styles are loaded is different depending if you are working in development or production:

  • .css .scss and .sass files are loaded with vue-style-loader in development mode and MiniCSSExtractPlugin.loader in production mode. One tricky point is that sass files are loaded by using two loaders: sass-loader and css-loader.
  • sourceMap is set to true only during development. This is very important to make debugging easier later on.

Plugins — The plugins option is used to customize the webpack build process in a variety of ways.

plugins: [
new VueLoaderPlugin(),
new HtmlPlugin({
template: 'index.html',
chunksSortMode: 'dependency'
})
]
  • VueLoaderPlugin() is required! This plugin is responsible for cloning any rules you have defined and apply them to the corresponding language blocks in .vue files. For example, if you have a rule matching /\.js$/, it will be applied to <script> blocks in .vue files.
  • HtmlWebpackPlugin() generates an HTML5 file that includes all webpack bundles in the body using script tags. chunksSortMode: "dependency” needs to be added as an option to control how chunks should be sorted when they are included to the HTML.

Development configuration

mode: "development"

In webpack 4, chosen mode instructs webpack to use its built-in optimizations accordingly.

devtool: 'cheap-module-eval-source-map'

This option controls if and how source maps are generated. By using cheap-module-eval-source-map, source maps from loaders are processed for better results. However, loader source maps are simplified to a single mapping per line.

output: {
path: helpers.root('dist'),
publicPath: '/',
filename: 'js/[name].bundle.js',
chunkFilename: 'js/[id].chunk.js'
}

The output key contains a set of options instructing webpack on how and where it should output bundles, assets or anything else you want to bundle or load with webpack. In our case, webpack is configured to output our bundles to the distfolder.

optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all'
}
}

splitChunks finds modules which are shared between chunks and splits them into separate ones. It enables you to reduce duplication or to separate vendor modules from application modules. chunks: 'all' is particularly powerful because it means that chunks can be shared even between async and non-async chunks.

Tobias Koppers tweet explaining runtimeChunk

This tweet from Tobias Koppers - webpack author, confirms that runtimeChunk: 'single' is clearly needed in our project.

The optimization key has many others options that are set by default depending on webpack configuration mode (development/production). You can read more about it here.

plugins: [
new webpack.EnvironmentPlugin(environment),
new webpack.HotModuleReplacementPlugin(),
new FriendlyErrorsPlugin()
]

EnvironmentPlugin() allows you to create global constants which can be configured at compile time. This can be useful for allowing different behaviors between development and production builds.

devServer: {
compress: true,
historyApiFallback: true,
hot: true,
open: true,
overlay: true,
port: 8000,
stats: {
normal: true
}
}

The above is weback devServer configuration that allows quick application development. You can read more about it here.

Production configuration

minimizer: [
new OptimizeCSSAssetsPlugin({
cssProcessorPluginOptions: {
preset: [
'default',
{ discardComments: { removeAll: true } }
],
}
}),
new UglifyJSPlugin({
cache: true,
parallel: true,
sourceMap: !isProd
})
]
  • UglifyJsPlugin() uses uglify-js to minify your javascript files. cacheand parallel properties are set to true in order to enable file caching and to use multi-process parallel running. This improves the build speed. More options are available and you can read more about it here.
  • OptimizeCSSAssetsPlugin() will search for CSS assets during the webpack build and will optimize and minimize them. All comments will be removed from our minified CSS and no messages will be printed to the console. This plugin should be used only on production builds without style-loader in the loaders chain (as we did in the common webpack configuration), especially if you want to have HMR in development.
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name (module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
return `npm.${packageName.replace('@', '')}`;
}
},
styles: {
test: /\.css$/,
name: 'styles',
chunks: 'all',
enforce: true
}
}
}

This article helped me a lot to understand how to configure webpack in production and how to split chunks the correct way. The author explains it as follow:

The docs will do a good job of explaining most things in here, but I’ll explain a bit about the groovy parts, because they took me so damn long to get right.

Webpack has some clever defaults that aren’t so clever, like a maximum of 3 files when splitting the output files, and a minimum file size of 30 KB (all smaller files would be joined together). So I have overridden these.

cacheGroups is where we define rules for how Webpack should group chunks into output files. I have one here called ‘vendor’ that will be used for any module being loaded from node_modules. Normally, you would just define a name for the output file as a string. But I’m defining name as a function (which will be called for every parsed file). I’m then returning the name of the package from the path of the module. As a result, we’ll get one file for each package, e.g. npm.react-dom.899sadfhj4.js.

NPM package names must be URL-safe in order to be published, so we don’t need to encodeURI that packageName. BUT, I had trouble with a .NET server not serving files with an @ in the name (from a scoped package), so I’ve replaced that in this snippet.

This whole setup is great because it’s set-and-forget. No maintenance required — I didn’t need to refer to any packages by name.

plugins: [
new webpack.EnvironmentPlugin(environment),
new MiniCSSExtractPlugin({
filename: 'css/[name].[hash].css',
chunkFilename: 'css/[id].[hash].css'
}),
new CompressionPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\\.(js|css)$'),
threshold: 10240,
minRatio: 0.8
}),
new webpack.HashedModuleIdsPlugin()
]
  • MiniCSSExtractPlugin() is required because webpack uses MiniCSSExtractPlugin.loader to load styles in production.
  • CompressionPlugin() prepares compressed versions of assets to serve them with Content-Encoding.
  • HashedModuleIdsPlugin() is responsible of generating hashes based on module relative path. A four character string is then generated as module id. This plugin is suggested to be used only in production.

Adding scripts

Add the following scripts to your package.json file :

"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --progress",
"staging": "cross-env NODE_ENV=staging webpack",
"build": "cross-env NODE_ENV=production webpack"
},

Now, you can run npm run dev to compile the VueJS application and serve it to the browser via webpack-dev-server.

Shia Labeouf giving tons of clap clap!

I hope you enjoyed this article! If you have any questions/suggestions, let me know in the comments below.

You can find the project files on my GitHub:

--

--

Samuel Teboul
Vue.js Developers

Senior Software Engineer | JavaScript | TypeScript | Angular | VueJS | NodeJS | RxJS