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 ...
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, }),
<% 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.