Webpack is so hot right now! Webpack is great when it comes to module bundling and working with frameworks like Vue or React, but it is a bit more awkward when handling static assets (like CSS). You might be more used to handling your static assets with something like Gulp, and there are some pretty good reasons for that.
Still, the amount of JavaScript in our static projects is growing, so to compensate, let’s make use of Webpack, while remaining in Gulp. In this article, specifically, Gulp 4. We’ll use modern techniques to build an easily maintainable workflow, including the powerful and useful Hot Module Reloading (HMR).

You May Want To Start Here
This article isn’t quite for beginners. If you are new to Webpack or Gulp, perhaps start with these tutorials.
Gulp Tutorials
Webpack Tutorials
Demo
Check the demo repo on GitHub. The branch “hmr” shows how to set up Hot Module Reloading.
Prerequisites
Run the following to install necessary packages:
npm install babel-core \
babel-preset-es2015 \
browser-sync \
gulpjs/gulp#4.0 \
webpack \
webpack-dev-middleware \
webpack-hot-middleware -D
As of Node v7.9.0, ES6 modules are not supported, that is why we install Babel to make use of import statements and other cutting edge JS features in our tasks.
If you don’t need HMR, feel free to leave Hot Middleware out of the packages listed above. The Dev Middleware does not depend on it.
Starting Points
Let’s get started! Create a tasks
folder in your project root with three files: index.js
, webpack.js
and server.js
. We have less clutter in our project root since the index file acts like gulpfile.js
and the webpack file as webpack.config.js
.
The site
folder holds all your site’s assets:
╔ site
║ ╚═══ main.js
╠ tasks
║ ╠═══ index.js
║ ╠═══ server.js
║ ╚═══ webpack.js
╚ package.json
To tell Gulp where the tasks are located, we need to add flags in our `package.json`:
"scripts": {
"dev": "gulp --require babel-register --gulpfile tasks",
"build": "NODE_ENV=production gulp build --require babel-register --gulpfile tasks"
}
The babel-register
command processes the import statements and the --gulpfile
flag defines the path to gulpfile.js
or, in our case, index.js
. We only need to reference the tasks
folder because like in HTML the file named index marks the entry point.
Set up a basic Webpack config
In `webpack.js`:
import path from 'path'
import webpack from 'webpack'
let config = {
entry: './main.js',
output: {
filename: './bundle.js',
path: path.resolve(__dirname, '../site')
},
context: path.resolve(__dirname, '../site')
}
function scripts() {
return new Promise(resolve => webpack(config, (err, stats) => {
if (err) console.log('Webpack', err)
console.log(stats.toString({ /* stats options */ }))
resolve()
}))
}
module.exports = { config, scripts }
Notice how we don’t export the object directly like many tutorials show but put it into a variable first. This is necessary so we can use the configuration in the Gulp task scripts
below as well as in the server middleware in the next step.
Context
The config.context
setup is necessary to set all paths relative to our site
folder. Otherwise they would start from the tasks
folder which could lead to confusion down the road.
Separate config and task
If you have a very long Webpack config, you can also split it and the task into two files.
// webpack.js
export let config = { /* ... */ }
// scripts.js
import { config } from './webpack'
export function scripts() { /* ... */ }
Hot Module Reloading
Here’s how to make HMR work. Change the entry and plugins:
entry: {
main: [
'./main.js',
'webpack/hot/dev-server',
'webpack-hot-middleware/client'
]
},
/* ... */
plugins: [
new webpack.HotModuleReplacementPlugin()
]
Make sure to disable the extra entries and the HMR plugin for production. The package Webpack Merge helps setting up different environments for development and production.
BrowserSync
Now a BrowserSync task setup:
import gulp from 'gulp'
import Browser from 'browser-sync'
import webpack from 'webpack'
import webpackDevMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import { config as webpackConfig } from './webpack'
const browser = Browser.create()
const bundler = webpack(webpackConfig)
export function server() {
let config = {
server: 'site',
middleware: [
webpackDevMiddleware(bundler, { /* options */ }),
webpackHotMiddleware(bundler)
],
}
browser.init(config)
gulp.watch('site/*.js').on('change', () => browser.reload())
}
The Dev Middleware enables BrowserSync to process what was defined as entry in webpack.js
. To give it this information we import the config module. Hot Middlware on the other hand checks for changes in app components like `.vue` files for Vue.js to inject.
Since we cannot hot reload files like main.js
, we watch them and reload the window on change. Again, if you don’t need HMR, remove webpackHotMiddleware.
Import all Tasks
The `index.js` file includes all tasks:
import gulp from 'gulp'
import { scripts } from './webpack'
import { server } from './server'
export const dev = gulp.series( server )
export const build = gulp.series( scripts )
export default dev
The exported variables define what tasks to run under which command. The default export runs with gulp
.
If you separate development and production environments for Webpack, you might want to run a gulp build
task which makes use of production options. For that, we import the scripts
tasks on its own since we don’t need to start the server here.
During development, Webpack is run by BrowserSync so putting the scripts task in the dev command is not necessary.
Running Tasks
To start developing you cannot just run gulp
or gulp build
since it will look for a gulpfile.js
in the project root. We have to run the npm commands npm run dev
and npm run build
to make use of the defined flags.
Expanding
Now you can imagine how easy it is to expand and write more tasks. Export a task in one file and import it in `index.js`. Clean and easy to maintain!
To give you an idea of how to set up your project folder, here is my personal setup:
╔ build
╠ src
╠ tasks
║ ╠═══ config.js => project wide
║ ╠═══ icons.js => optimize/concat SVG
║ ╠═══ images.js => optimize images
║ ╠═══ index.js => run tasks
║ ╠═══ misc.js => copy, delete
║ ╠═══ server.js => start dev server
║ ╠═══ styles.js => CSS + preprocessor
║ ╚═══ webpack.js
╚ package.json
Again, why use both Webpack and Gulp?
Static File Handling
Gulp can handle static assets better than Webpack. The Copy Webpack Plugin can also copy files from your source to your build folder but when it comes to watching file deletion or changes like overriding an image, gulp.watch
is a safer bet.
Server Environment
Webpack also comes with a local server environment via Webpack Dev Server but using BrowserSync has some features you might not want to miss:
- CSS/HTML/image injection for non-app projects
- multiple device testing out of the box
- includes an admin panel for more control
- bandwidth throttling for speed and loading tests
Compilation Time
As seen in this post on GitHub Sass gets processed by node-sass much quicker than by Webpack’s combination of sass-loader, css-loader and extract-text-webpack-plugin.
Convenience
In Webpack, you have to import your CSS and SVG files for instance into JavaScript to process them which can be quite tricky and confusing sometimes. With Gulp, you don’t need to adjust your workflow.
Thanks author for the example and description.
This is great thing to combine Gulp and Webpack together to resolve disadvantages of each other. Some time ago, when I used Gulp only, I was looking for plugin to concatenate each javascript’s file together, but didn’t find better solution then integration Gulp + Browserify or Webpack bundlers. I have used Webpack earlier in my React SPA and I know how much powerful Webpack can be for JS.
That’s why I decided to get this way.
Here is my example of gulp 3.9, browsersync and webpack middleware stack:
https://github.com/wwwebman/gulp-webpack-starter
That’s not really true. If you don’t want to explicitly import (S)CSS in your JavaScript files just add your CSS to an entry point. You demoed having multiple files make up a webpack entry point, just add
'style!css!./path/to/file.css'
to the entry configuration. If you don’t want CSS inlined and injected by JavaScript webpack can make static css files for you too.Further, the webpack configuration here for hot loading is a lot more complex than required. You don’t need to add entry files for hot loading, just set
devServer.hot
totrue
.Again, I’m going to have to disagree. Static asset management in webpack is a lot more powerful than using gulp, especially when you understand loaders. Using the
html-loader
, for instance, will automatically use webpack loaders for referenced static files in the HTML (images, etc). If you don’t want to inline files, just usefile-loader
and then you get the added benefit of a failing build if webpack can’t find an asset, e.g., you deleted an asset and maybe forgot a reference. You lose that if you don’t let webpack manage your static assets.Not to be a Debbie Downer, but combining Webpack, Gulp, and Browsersync just makes everything much more complicated than it needs to be. I would personally just use webpack. If you need more features from Browsersync (like syncing user events between browsers, what it was originally designed for) then there’s a browsersync plugin available for webpack. If you prefer a more traditional setup then just use Gulp and Browsersync. Trying to combine everything together just creates a fragile setup that’s hard to update.
Hey Ken, I recently migrated from grunt and webpack 1 to NPM commands and webpack 2. And I have to admit I feel some of Pascal’s pain. It seems like my compile time is longer than before. Also, we run our development site out of our dist folder, but this causes issues when we change a .json file that contains our resource string as that file doesn’t get automatically copied to the dist folder. Do most projects avoid this by serving from their app folder? Or is there another plugin I’m missing. Concepts like this seem to be a little more straightforward with grunt/gulp.
I’m using Webpack every days and I love it but it’s still 100 times more complex to understand, setup and tweak than is Gulp or Grunt.
Also most of the Webpack boilerplate out there don’t handle static assets optimisation like images optimisation, sprite generation, etc. It seems that the community lost that in the switch between Grunt/Gulp and Webpack for some reason. I set them up with Webpack and it was a real pain to do so, so I understand why it can be useful to have Gulp in parallel for these.
Chris recently discussed the issue of dependencies (see comment on this page https://css-tricks.com/projects-need-react/), and I think it bears repeating.
In order to even start to use webpack, for example, you need to commit to a whole series of technologies. I don’t think the issue is what the list is (if I say “node.js” you may well reply that you like node.js), the issue is the fact that the list makes one commit to an entire, elaborate style of workflow — compiling resources, running local servers, package managers, etc. This all feels very much to be a computer-science driven way of thinking about development: emphasizing abstraction and streamlining and tools. But this way of thinking has proven to not be good for the web, where things need to be practical, seat-of-your-pants, and forgiving. HTML5, in my mind, was a backlash to this overengineering and a return to the more “blue collar” way of seeing the web: it’s an inefficient, always in motion, spaghetti, hack world, but it is all simple — text files and urls at base.
Simultaneous to this move towards increasing dependencies and compilation is this attempt to make WordPress become part of the “stack” — part of the infrastructure like apache, PHP, MySQL, etc. If you look at the code for this site, for example, you’ll see many JS files loading all over the place, often dynamically written — many of which, at first glance, seem devoted to variously “optimizing” the site. I see an SEO module, there always tons of google scripts, twitter scripts, FB scripts, etc.
If webpack et al are meant to somehow streamline delivery or make the organization of assets easier, then there seems to be some sort of disconnect somewhere. More and more often I see WP sites that load dozens of plugins that no one understands or keeps track of; I see developers blindly pasting “build” and “install” into their consoles, installing more and more alpha- or beta- server plugins, and leaving abandoned complex frameworks everywhere, deeply tied to their impossible-to-understand code. Cruft is building up more and more — and not the kind of cruft that HTML5 tried to solve by allowing messiness. Things are not getting streamlined or simplified. The proliferation of middleware web development tools is a big fat stinky mess.
Is Gulp 4.0 safe to use now? It’s been in beta for absolutely ages, to the point that I was wondering if all the devs had abandoned it in favour of WebPack.
Yes, it is safe to use.
It’s part of my workflow for over half a year now.
In the BrowserSync task set up I think you may have a typo. The code snippet says the file name is webpack.js but you have the following line:
import { config as webpackConfig } from './webpack'
This means you are also importing named exports from a local file named webpack.js
Is the name of BrowserSync task file different than webpack.js?
Shoot, you are right. Thanks!
The BrowserSync task would be in
server.js
, notwebpack.js
.I’ll contact an admin about that.