Making Grunt Builds Incremental with grunt-newer
Prefixing Grunt tasks with newer: makes builds incremental by skipping unchanged files. Learn how the timestamp cache works, where it breaks, and how to verify the speedup.
18 Jun 2026, 20:37 UTC

The problem: every save rebuilds the world
If you use grunt watch, you know the pattern: save a file, and the pipeline lints, compiles, and minifies everything again. On a small project that is fine. As the repository grows, feedback time starts to scale with the total number of source files rather than the size of your edit. You save one Sass partial and wait while Grunt re-touches hundreds of unchanged JavaScript files.
The useful takeaway is that you do not have to rewrite your build to fix this. The grunt-newer plugin lets you prefix existing task names with newer: so Grunt only processes files that have changed since the last successful run.
How the newer: prefix works
grunt-newer is a third-party plugin, not part of Grunt core. It acts as a wrapper around another task. When you run grunt newer:jshint, the plugin reads a cache file that records the timestamp of each source file from the previous successful run. It compares those timestamps against the current files on disk and passes only the changed files to the underlying jshint task. If the wrapped task finishes successfully, the cache is updated.
Because it is a prefix rather than a rewrite, it works with any task that follows Grunt's standard src/dest files API. You do not need to change your task configuration; you only change how you invoke it.
A worked example
Install the plugin in your project root:
npm install grunt-newer --save-dev
Then load it in your Gruntfile and define both an incremental alias and a full-build alias:
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-newer');
grunt.initConfig({
jshint: {
all: ['src/scripts/**/*.js']
},
sass: {
dist: {
files: [{
expand: true,
cwd: 'src/styles',
src: ['**/*.scss'],
dest: 'dist/styles',
ext: '.css'
}]
}
},
uglify: {
dist: {
files: [{
expand: true,
cwd: 'dist/styles',
src: ['**/*.js'],
dest: 'dist/scripts',
ext: '.min.js'
}]
}
},
watch: {
scripts: {
files: ['src/scripts/**/*.js'],
tasks: ['newer:jshint']
},
styles: {
files: ['src/styles/**/*.scss'],
tasks: ['newer:sass']
}
}
});
grunt.registerTask('build', [
'newer:sass',
'newer:uglify',
'newer:jshint'
]);
grunt.registerTask('full-build', [
'sass',
'uglify',
'jshint'
]);
};
The first grunt build processes everything because the cache is empty. A second immediate run finds nothing to do for unchanged files. If you edit src/scripts/app.js and run grunt newer:jshint again, only that file is handed to JSHint. You can verify this by running the command twice in a row and confirming the second invocation processes no files.
To test the filtering manually, run touch src/scripts/app.js from your project root, then rerun grunt newer:jshint. The task output should list only the touched file. This is a quick way to confirm the cache is working before you rely on it in a watch loop.
Trade-offs: cache fidelity and task semantics
The main limitation is that the cache is timestamp-based. A git checkout, a fresh CI runner, or a manual touch can produce false positives—files that look changed but are not. A missing or stale cache file silently degrades to a full rebuild, which is safe but can mask a broken incremental setup.
More importantly, newer only adds work; it never cleans. If you delete a source file, the compiled artifact remains in the output directory because the incremental build has no reason to remove it. A full build with a clean task would remove it.
Tasks that aggregate many inputs into one output are also a poor fit. If you wrap concat, revisioning, or banner injection, newer may hand the task a subset of files and produce an incomplete bundle. Leave those tasks unwrapped.
| Task pattern | Wrap with newer:? | Reason |
|---|---|---|
| Per-file lint, compile, minify | Yes | Idempotent, one source maps to one destination |
concat, revisioning, banner | No | Subset input yields incomplete or inconsistent output |
clean | No | Global side effects; file filtering assumes per-file semantics |
A practical policy
Use newer: for per-file, idempotent tasks during local development and inside watch. Keep a plain full-build alias such as grunt full-build for CI and releases so you always have a path that processes every file from scratch. Add the cache file to .gitignore so it is never shared between machines or users—sharing the cache can skip files that genuinely changed on another checkout.
Because grunt-newer is a third-party plugin, confirm it is maintained and compatible with your Grunt and Node versions before adopting it. Before committing to the pattern, time grunt build against grunt full-build on your actual repository. If the saving is marginal, the extra moving part may not be worth it. If it is significant, you have turned a linear-time feedback loop into a per-edit one without rewriting your Gruntfile.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.