Make Gulp Image Tasks Incremental with `lastRun` and `since`
A slow `gulp images` task re-admits every file on each run. Use `gulp.lastRun` with `src(..., { since })` to process only changed files, while keeping a clean full build for CI.
22 Dec 2025, 09:49 UTC

The problem: every run reprocesses every image
Your gulp images task feels fine with ten files. Then the design team adds a few thousand product photos, and every invocation grinds for minutes. The culprit is usually not the optimizer itself; it is the glob—the wildcard pattern src/images/**/* that re-admits every matching file on every run. Unchanged files flow through the pipe again and again.
The useful takeaway: add since: lastRun(images) to that src call so the task only sees files modified after its last successful run, and keep a separate full-build task for CI where correctness cannot depend on incremental state.
Treat lastRun as a build clock, not a cache
Gulp's lastRun is a function that returns the timestamp of a task's last successful completion. It is not a content cache. It does not hash files, and it does not remember what was written. It simply answers one question: when did this task last finish successfully?
You feed that timestamp into gulp.src via the since option. Gulp then filters the glob results, dropping any file whose filesystem modification time (mtime) is older than that timestamp. The result is a smaller stream entering your transform stage.
Because lastRun only advances after a successful run, a failed build leaves the previous timestamp in place. The next run will reprocess files changed since that earlier success, which is usually the safe behavior.
Worked example: an incremental images task
Assume you have gulp-imagemin installed and a source tree at src/images. In your gulpfile.js:
const { src, dest, lastRun } = require('gulp');
const imagemin = require('gulp-imagemin');
function images() {
return src('src/images/**/*', { since: lastRun(images) })
.pipe(imagemin())
.pipe(dest('dist/images'));
}
exports.images = images;
Run it from your project root:
gulp images
Run it again immediately. The second invocation should enter the stream with zero files, because nothing in src/images has an mtime newer than the first run's completion time.
Now touch one file:
touch src/images/logo.png
gulp images
Only logo.png should reach imagemin(). Its siblings are filtered out by since.
Verification: proving the filter works
Before you trust this in a release pipeline, verify the behavior locally.
- Run the task twice without changing files. The second run should process no files. If it processes everything, confirm that
lastRunis actually receiving the task function reference, not a string. - Log the timestamp. Temporarily add
console.log('lastRun:', lastRun(images));at the top of the task. Before any successful run it should printundefined; after a success it should print a millisecond timestamp. - Touch exactly one source file and rerun. Only that file should reach the transform stage.
- Temporarily remove the
sinceoption and run a full build. The incremental run should process a strict subset of the full run's files.
Trade-offs and when incremental fails
since trusts filesystem mtimes. That assumption breaks in common workflows:
- Fresh checkouts and CI restores: A clean clone or a cache restore may reset mtimes to the checkout time, causing every file to appear newer than
lastRunand forcing a full reprocess. - Timestamp-preserving copies: Tools like
rsync -aorcp -ppreserve mtimes, which can make unchanged files look old and get skipped even when they are new to the destination. - Coarse mtime granularity: Some filesystems record mtimes with one-second or two-second granularity. Rapid successive edits can fall inside the same tick and be missed.
- Aggregate tasks: If a task must read all files together—for example, concatenating every stylesheet into one bundle—
sincecannot help, because the task needs the complete set.
Also, since filters by path glob and mtime, not content hashes. Identical files with new timestamps will be processed again.
Keep a full-build path for CI
Never let release correctness depend solely on incremental state. Keep a clean task that omits since and processes everything:
const { src, dest, series } = require('gulp');
const imagemin = require('gulp-imagemin');
function imagesFull() {
return src('src/images/**/*')
.pipe(imagemin())
.pipe(dest('dist/images'));
}
exports.build = series(imagesFull);
In CI, run the full build. Locally, run the incremental task for fast feedback.
Actionable closing
Pick your slowest leaf task—a task that transforms files independently, such as image optimization or individual script transpilation, rather than one that aggregates everything. Add since: lastRun(taskFn) to its src call. Verify with a double run and a single touched file. Then wire a separate full-build task into CI. If the verification steps fail, check your installed Gulp major version and Node version, because incremental behavior is version- and runtime-sensitive. Confirm that no other task in the same run writes into the glob you are filtering, or generated outputs will be mistaken for new inputs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.