Configuring Grunt Watch with Live Reload for Automatic Browser Refresh
Set up Grunt's watch task with LiveReload to auto-refresh the browser on source changes. This guide covers a working Gruntfile, the required middleware, and common pitfalls like infinite loops and missing script injection.
09 May 2026, 03:59 UTC

The useful answer
Grunt's grunt-contrib-watch can automatically reload the browser when source files change, but it requires three pieces working together: a watch target that matches your source files (not the output), a task that rebuilds those files, and a LiveReload script injected into the served HTML so the browser receives the WebSocket reload signal.
How the mechanism works
grunt-contrib-watch uses chokidar to monitor file system events against minimatch glob patterns. When a matched file changes, it runs the associated tasks (e.g., Sass compilation). If the target's options.livereload is truthy, the watch task starts a WebSocket server on port 35729 (by default) and broadcasts a reload message to any connected clients. The browser must have a small client script that opens that WebSocket connection. That script is injected either by the connect-livereload middleware in a grunt-contrib-connect server or by manually adding <script src="//localhost:35729/livereload.js"></script> to your HTML during development.
Worked configuration example
The following Gruntfile.js sets up a minimal project that compiles SCSS to CSS and serves the project with LiveReload. It assumes you have installed the packages listed in the verification steps.
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
sass: {
dev: {
files: {
'dist/css/style.css': 'src/scss/style.scss'
}
}
},
connect: {
server: {
options: {
port: 9000,
base: '.',
middleware: function(connect, options) {
// Inject livereload script into HTML responses
var livereload = require('connect-livereload');
return [
livereload(),
connect.static(options.base)
];
}
}
}
},
watch: {
css: {
files: ['src/scss/**/*.scss'],
tasks: ['sass:dev'],
options: {
livereload: true,
spawn: false
}
},
html: {
files: ['**/*.html'],
options: {
livereload: true
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-sass');
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.registerTask('default', ['connect:server', 'watch']);
};
Key points in the example
- Source patterns, not output:
files: ['src/scss/**/*.scss']watches the source directory. Watchingdist/**/*.csswould trigger an infinite loop because the Sass task writes there. - Task per target: The
csstarget runssass:dev; thehtmltarget has no tasks because HTML changes only need a browser reload. - Livereload option: Both targets set
livereload: true. The watch task starts a single WebSocket server shared across targets. - Spawn false:
spawn: falseruns the Sass task in the same Node process, which is faster but shares state. Usespawn: true(default) for isolation if tasks leak memory or rely on global state. - Middleware order:
connect-livereloadis placed beforeconnect.staticso the script is injected into HTML before it is sent to the browser.
Limits and common mistakes
Infinite rebuild loops
Watching the destination directory (e.g., dist/**/*.css) causes the watcher to see its own output as a change, triggering another build. Always watch source directories.
Missing script injection
If you run grunt watch without a Connect server (or without the middleware), the browser never receives the livereload.js script. Either add the middleware as shown, or manually include the script tag in your HTML during development and remove it for production.
Port conflicts
Only one LiveReload server can bind to port 35729. If you run multiple Grunt instances, change the port per instance: options: { livereload: 35730 }.
Windows file system events
On Windows, rapid saves may be missed. Add options: { interval: 500 } to fall back to polling every 500 ms.
Large file trees
Projects with 10,000+ watched files consume significant CPU. Narrow patterns (e.g., src/scss/**/*.scss instead of **/*.scss) and consider grunt-newer to run tasks only on actually changed files.
Spawn trade-offs
spawn: false speeds up repeated runs but risks memory leaks and cross-run contamination. If a task uses global caches or does not clean up, prefer the default spawn: true.
Grunt maintenance status
Grunt is in maintenance mode. For new projects, tools like Vite, esbuild, or Webpack dev server provide integrated hot module replacement (HMR) with less configuration overhead.
Verification checklist
- Run
grunt --versionandnpm list grunt-contrib-watchto confirm Grunt 1.x and watch 1.1+. - Start the task with
grunt watch(orgrunt default). - Open
http://localhost:9000in a browser. - In DevTools Network tab, filter for
livereload.jsand confirm a 200 response fromlocalhost:35729. - In Console, look for “LiveReload connected” on page load.
- Edit
src/scss/style.scss; the browser should refresh automatically and show the new styles. - Create a
dist/css/style.min.cssfile and modify it; the watcher should not trigger (exclusion via pattern).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.