← All writing
Craft · · 5 min

Gulp, because Grunt

Streams beat config files. I'm much less sure that's the reason I switched.

Tooling

I moved a project from Grunt to Gulp last month. The build went from ~14 seconds to ~3. It took the better part of a week.

I’ve done the math. At roughly 40 builds a day, 11 seconds each, I’d make that back in something like 6 weeks of continuous work on that project, assuming I never have to touch the Gulpfile again… which I will.

I’m not mad about it. The new build is nicer to work in and I’d probably do it again. But I want to write down what the actual difference is while I can still remember, because I’ve now watched this cycle twice and I’m starting to think the important thing isn’t which tool won.

What’s in a grunt or a gulp, really

Grunt is configuration. You describe a big object, task by task, and each task reads files off disk, does its thing, and writes files back to disk. Then the next task reads those files again.

Gulp is code. You write a function, you pipe a stream through some transforms, and the intermediate results stay in memory.

gulpfile.js
gulp.task('css', function () {
  return gulp.src('src/scss/main.scss')
    .pipe(sass())
    .pipe(autoprefixer())
    .pipe(minifyCss())
    .pipe(gulp.dest('dist/css'));
});
JavaScript

That’s the whole thing, and the reason it’s faster is right there in the shape of it. Sass compiles, the result goes straight into autoprefixer as a stream, that goes straight into the minifier. Disk gets touched twice instead of eight times. On a big Sass tree that’s most of the 11 seconds.

The other real difference is that when it goes wrong you’re debugging JS rather than debugging a config object, and I’d forgotten how much better that is. You can console.log in the middle of a task. You can pull a bit out into a function. It’s just a file that runs.

Same difference

But here’s the thing I keep bumping into. Both of them are a layer over a set of tools that already have command line interfaces.

Sass has a command. Autoprefixer has a command. The minifier has a command. What Grunt and Gulp are actually selling is a way to sequence those and watch for file changes, and I’ve now spent a cumulative week of my life on two different implementations of “run these four commands in order.”

And in both cases the cost isn’t the sequencing, it’s the plugin ecosystem. Every tool needs a wrapper, the wrapper is maintained by someone who isn’t the tool’s author, and the wrapper lags. Twice this month I’ve wanted a flag that the underlying tool has had for a year and the plugin hadn’t exposed yet. Once I ended up shelling out to the real binary from inside the task, which works fine and also rather undermines the point.

I've spent a cumulative week of my life on two different implementations of "run these four commands in order."

There’s a growing argument that you should skip both and just use npm scripts, since npm already runs commands and every one of these tools is a command. I’ve tried it on a small thing and it was fine, right up to the point where I wanted to watch files and run three things in parallel, and then the scripts block turned into an indecipherable shell one-liner. So I don’t think that’s free either. It’s just a different place to put the complexity.

Optimization theater

This is the second time in about two years I’ve rewritten a build to move onto the tool everyone moved onto. And each time the pitch was real, and each time I got something out of it, and each time the migration cost more than the benefit for that project in isolation.

The honest defense is that the benefit isn’t per-project. It’s that the ecosystem moves and the plugins and the answers all move with it, so staying on the old thing gets more expensive over time in a way that’s hard to see month to month.

The less flattering read is that we are an industry with a lot of restless people in it and churn feels like progress. I hold both of these at once, probably in the wrong proportion.

What I’ve landed on, and I’ll see if it survives contact with next year: I’ll migrate a project when I’m already opening it for something substantial, and not before. No dedicated migration weeks. If the build works and I’m not blocked, then the fact that it’s built on the previous tool du jour is not a defect.

Grunting or gulping

If you’re starting something new right now I’d use Gulp, for the reasons above, with the caveat that I’d have said Grunt with equal confidence in 2013 and I was not wrong then either.

But mostly I’d say: put the sourcemaps in, whichever one you pick. That’s the bit that changed my day-to-day. The 11 seconds turned out to be the part of the story I could measure, which is not the same as the part that mattered, and I suspect that’s a general hazard.

Ask me again in 18 months when there’s a new one 🙃

Read similar posts
9 min

Write it down once

Most of what makes Claude Code work for me isn't prompting, it's a few files I wrote once: a plan-mode habit, a /simplify pass welded onto /commit, a hook that won't let me hand-write a commit, and a CLAUDE.md that's mostly scar tissue.

16 min

It said it was working

Porting a Claude Code safety tool from Linux to macOS turned into six weeks of discovering that nearly every failure mode on the new platform was silent, and that the tool's own adversarial test suite was the only reason I could see any of them.