Mariusz Rajczakowski
Software Engineer
16 min read | 3 months ago

Speed up your development with HMR and webpack!

Prerequisites

Prior to this tutorial you should be familiar with webpack setup. If you don't understand webpack basics head over to this article first.

What is HMR?

Hot Module Replacement (HMR) is one of the most useful webpack’s feature that allows modules to be updated while an application is running, without a full refresh.

Remember times when you were pressing F5 to refresh page and see changes? With HMR you can skip that step!

Speeding up development

Using HMR can help you with:

  • not losing application state which is normally lost during full reload
  • saving time for refreshing the page
  • tweaking your styles on the fly

How does it work?

  1. It starts very early, when you compile your assets, webpack adds a small HMR runtime to the bundle, which runs inside the app.
  2. When webpack finished the build, it still watches your source code for changes.
  3. If the source code would change, it rebuilds only changed module(s)
  4. There are two ways of updating the module in the browser (depending on settings): Webpack signals to the HMR runtime or HMR runtime will poll webpack for changes.
  5. Either way the changes are being sent to the browser, then HMR runtime tries to apply the hot update.
  6. First it tries to find out whether the updated module can self-accept. If not, then it bubbles up to the module which required the updated module, and tries the same.
  7. It will do this recursively until reach entry point of an app. When it does, without hot accept, it fails and you see full page refresh

Different viewpoint of changes

Compiler level

Apart from normal assets, compiler emits two additional files:

  • Manifest (JSON) - contains a new compilation hash and list of updated chunks
  • Updated chunks (JS) - each of these chunk contains new code or a flag indication the the module has been removed

Application level

Application asks HMR runtime to check for updates or webpack-dev-server notifies the app (via websocket by sending invalidating notification)

The HMR runtime downloads updated modules and notifies the app

Then app ask runtime to apply those updates

The HMR runtime applies the updates.

Module level

HMR is an opt-in feature - it only affect those modules containing HMR code.

You can specify handlers which are called when a dependency or module itself is updated.

If a module has no HMR code that the update bubbles up until reaches entry point and full page refresh occurs.

It is not mandatory to have HMR code in every module, moreover having it in a module which wraps up the tree of modules, and when the dependency got updated it reloads entire tree with all descendants.

HMR runtime

HMR runtime is an additional code emitted to the build to track module parents and children.

It does support two methods: check and apply

Check sends HTTP request to get manifest with list of updated chunks, if it fails there is no update available.

When it succeed, it will compare the list of currently loaded chunks with the list of updated chunks.

For each affected chunk, an update is downloaded. When it does, the runtime switches to ready state and the changes can be applied.

Apply method marks all updated modules as invalid. There needs to be a specified handler for a module update either in a module itself or in the parent module.

The process continues recursively in the tree hierarchy until reaches an entry point without finding an update handler, when it fails and fully reload page

How to set up HMR in your project?

There are 3 ways of configuring you project with HMR:

  1. Webpack-dev-server CLI - you are running you web-server from command-line, you can use either inline (probably just for testing) or webpack.config.js approach (real apps development)
  2. Webpack-hot-middleware + webpack-dev-middleware - if you running your own express server your can enable HMR by adding those middlewares instead of using webpack-dev-server
  3. Webpack-dev-server API - you are running you webpack-dev-server from a task runner like gulp or grunt

Disclaimer

If you want to introduce HMR into you flow, you can't just use webpack CLI/API, HMR requires a server to work (either webpack-dev-server or express), then you will have to choose one of the solutions as specified above.

Let me help you with finding the right one for you.

Enabling HMR

Ad 1. Webpack-dev-server CLI

Inline approach

//create a new dir and cd into it
mkdir hmr-cli-test
cd hmr-cli-test

//install webpack-dev-server
//globally
npm install webpack-dev-server -g
//or locally
npm install webpack-dev-server --save-dev

//install other dependencies
npm install webpack css-loader style-loader --save-dev

//initialize some files:
touch app.css app.js index.html

//then add styling to app.css
body {
  background: blue;
}

//edit app.js and add
require('./app.css');
console.log('loaded');

if(module.hot){
  module.hot.accept();
 //all changes to this file and app.css will cause module update without full page refresh
}

Paste the following lines to index.html:

<html>
<head>
  <title>HMR webpack-dev-server CLI</title>
</head>
<body>
  <h1>HMR webpack-dev-server CLI</h1>
  <script src='bundle.js'></script>
</body>
</html>
//then run in your terminal
//if webpack-dev-server installed globally
webpack-dev-server ./app.js --hot --inline --module-bind 'css=style-loader\!css-loader'
//if installed locally
node_modules/.bin/webpack-dev-server ./app.js --hot --inline --module-bind 'css=style-loader\!css-loader'

When you go to your browser browser and open at http://localhost:8080 you should blue background and when you open your dev inspector you should see console.log message - 'loaded'

Webpack.config.js + hot option

You can also you HMR and specify all the settings in you config

mkdir hmr-cli-with-config
cd hmr-cli-with-config

//install webpack-dev-server and dependencies
npm install webpack webpack-dev-server css-loader style-loader --save-dev

mkdir src dist
touch src/app.js src/app.css dist/index.html

//then fill app.js, app.css and index.html with the same content as an inline example above

Create a new file called weback.config.js and add:

//webpack.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: [
      // 'webpack-dev-server/client?http://localhost:8080',
      // 'webpack/hot/dev-server',
      './src/app.js'
    ],
    devServer: {
      contentBase: './dist', //specifies where your index.html and non webpack files lives

      //you would either add 'webpack-dev-server/client?http://localhost:8080'
      // to the entry or add flag -> hot:true
      hot: true,

      //you would either add 'webpack/hot/dev-server' to the entry or
      //add flag -> inline:true
      inline: true //it does hmr in the browser
    },
   module: {
      rules: [
        {
           test: /\.css$/,
           use: ['style-loader', 'css-loader']
           //all the css modules will be passed through css-loader and then injected to html via
        style-loader
         }
      ]
   },
    plugins: [
      new webpack.HotModuleReplacementPlugin()
      //generates hot update chunks
    ],
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

If you go to the terminal and then run:

webpack-dev-server
//then you should see the same output like in the inline example above
//however it's more managable, when you have a real project

Ad 2. Webpack-hot-middleware + express

If you have a project using express as a server, instead of using webpack-dev-server you might choose this solution.

mkdir hmr-with-express
cd hmr-with-express

//install dependencies
npm install express --save

npm install style-loader css-loader webpack webpack-hot-middleware webpack-dev-middleware --save-dev

//add folders and files
mkdir src dist
touch webpack.config.js src/app.js src/app.css dist/index.html server.js

//then add styling to src/app.css
body {
  background: blue;
}

//edit src/app.js and add
require('./app.css');
console.log('loaded');

if(module.hot){
  module.hot.accept();
 //all changes to this file and app.css will cause module update without full page refresh
}

Paste the following lines to index.html:

<html>
<head>
  <title>HMR + express</title>
</head>
<body>
  <h1>HMR + express </h1>
  <script src='bundle.js'></script>
</body>
</html>

Paste those line to your webpack.config.js:

//webpack.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: [
    'webpack-hot-middleware/client?path=/webpack_hmr&timeout=10000',
    './src/app.js',
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  module: {
      rules: [
        {
           test: /\.css$/,
           use: ['style-loader', 'css-loader']
         }
      ]
   }
};

Edit server.js file and add:

const express = require('express');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const webpack = require('webpack');
const config = require('./webpack.config.js');
const path = require('path');
const app = express();

const compiler = webpack(config);

//webpack-dev-middleware
//serves the files emitted from webpack
//over a connect server - does it in memory
app.use(webpackDevMiddleware(compiler, {
  stats: { colors: true }
}));

//webpack hot middleware
//does hmr
app.use(webpackHotMiddleware(compiler, {
  //the path which middleware serves event stream on
  //must match the webpack.config.js hearbeat entry
  path: '/webpack_hmr',

  //this settings set how ofthen send a hearbeat to the client to keep current connection alive
  //should be half of client entry's timeout
  heartbeat: 5000 //every 5 seconds
}));

//serving all static files from dist folder
app.use(express.static(`${__dirname}/dist`));

app.listen(3000, ()=>{
  console.log(`HMR express server listening on http://localhost:3000`);
});

Ad 3. Webpack-dev-server node.js API

You can use this way of running webpack-dev-server when you are using some task runners like i.e. gulp

mkdir hmr-with-api
cd hmr-with-api

//install dependencies
npm install gulp gulp-util webpack webpack-dev-server css-loader style-loader  --save-dev

//make folders and files
mkdir src dist
touch webpack.config.js src/app.js src/app.css dist/index.html gulpfile.js

//then add styling to src/app.css
body {
  background: blue;
}

//edit src/app.js and add
require('./app.css');

if(module.hot){
  module.hot.accept();
 //all changes to this file and app.css will cause module update without full page refresh
}

Paste the following lines to dist/index.html:

<html>
<head>
  <title>HMR + webpack-dev-server API</title>
</head>
<body>
  <h1>HMR + webpack-dev-server API </h1>
  <script src='bundle.js'></script>
</body>
</html>

Paste those line to your webpack.config.js:

const path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: [
      './src/app.js'
    ],
    devServer: {
      contentBase: path.join(__dirname, 'dist'),
      hot: true,
      inline: true
    },
   module: {
      rules: [
        {
           test: /\.css$/,
           use: ['style-loader', 'css-loader']
         }
      ]
   },
    plugins: [
      new webpack.HotModuleReplacementPlugin()
    ],
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
};

Then edit your gulpfile and add:

const gulp = require('gulp');
const gulpUtil = require('gulp-util');
const webpack = require('webpack');
const webpackDevServer = require('webpack-dev-server');
const webpackConfig = require('./webpack.config.js');
const path = require('path');

const port = 8080;

gulp.task('default', ['webpack-dev-server']);

gulp.task('webpack-dev-server', (callback) => {

  const compiler = webpack(webpackConfig);

  const server = new webpackDevServer(compiler,{
    contentBase: webpackConfig.devServer.contentBase,
    hot: true,
    stats: {
      colors: true,
    },
  });

  server.listen(port, 'localhost', (err) => {
  if(err) throw new gulpUtil.PluginError('webpack-dev-server', err);
     gulpUtil.log('[webpack-dev-server]', `http://localhost:${port}`);
  });
});

//then run your gulp and you should be able to
//go to localhost:8080 and hmr should just works!

Start using HMR and be faster than ever before!

HMR speeds up development process - no doubt, but you might be wondering which solution to choose when adding this to your project. This is how you choose the right way:

  • If you have your project and using gulp or grunt to perform your tasks: use webpack-dev-server API
  • If you using express as your server of choice - you should use webpack-hot-middleware + webpack-dev-middleware to implement HMR
  • If you have normal project an the above was not a fit for you, then use webpack-dev-server CLI with webpack.config.js
  • If you just testing stuff, and want to play around a little bit use webpack-dev-serve CLI with specifying all settings in command line

Enjoy your faster development with HMR feature!

References
  1. https://www.andrewhfarmer.com/3-ways-webpack-hmr
  2. https://webpack.js.org/concepts/hot-module-replacement/

Share:



Warning! This site uses cookies
By continuing to browse the site, you are agreeing to our use of cookies. Read our privacy policy