Broccoli: First Beta Release (2025)

Broccoli is a new build tool. It’scomparable to the Rails asset pipeline in scope, though it runs on Node and isbackend-agnostic.

After a long slew of 0.0.x alpha releases, I just pushed out the first betaversion, Broccoli 0.1.0.

Update March 2015: This post is still up-to-date with regard toarchitectural considerations, but the syntax used in the examples isoutdated.

Table of Contents:

  1. Quick Example
  2. Motivation / Features
  3. Architecture
  4. Background / Larger Vision
  5. Comparison With Other Build Tools
  6. What’s Next

1. Quick Example

Here is a sample build definition file (Brocfile.js), presented withoutcommentary just to illustrate the syntax:

12345678910111213141516
module.exports = function (broccoli) { var filterCoffeeScript = require('broccoli-coffee'); var compileES6 = require('broccoli-es6-concatenator'); var sourceTree = broccoli.makeTree('lib'); sourceTree = filterCoffeeScript(sourceTree); var appJs = compileES6(sourceTree, { ... outputFile: '/assets/app.js' }); var publicFiles = broccoli.makeTree('public'); return [appJs, publicFiles];};

Run broccoli serve to watch the source files and continuously serve thebuild output on localhost. Broccoli is optimized to make broccoli serve asfast as possible, so you should never experience rebuild pauses.

Run broccoli build dist to run a one-off build and place the build output inthe dist directory.

For a longer example, see thebroccoli-sample-app.

2. Motivation / Features

2.1. Fast Rebuilds

The most important concern when designing Broccoli was enabling fastincremental rebuilds. Here’s why:

Let’s say you’re using Grunt to build an application written withCoffeeScript, Sass, and a few more such compilers. As you develop, you want toedit files and reload the browser, without having to manually rebuild eachtime. So you use grunt watch, to rebuild automatically. But as yourapplication grows, the build gets slower. Within a few months of developmenttime, your edit-reload cycle has turned into an edit-wait-10-seconds-reloadcycle.

So to speed up your build, you try rebuilding only the files that havechanged. This is difficult, because sometimes one output file depends onmultiple input files. You manually configure some dependency rules, to rebuildthe right files depending on which files were modified. But Grunt was neverdesigned to do this well, and your custom rule set won’t reliably rebuild theright files. Sometimes it rebuilds files when it doesn’t have to (making yourbuild slow). Worse, sometimes it doesn’t rebuild files when it should (makingyour build unreliable).

With Broccoli, once you fire up broccoli serve, it will figure out by itselfwhich files to watch, and only rebuild those that need rebuilding.

In effect, this means that rebuilds tend to be O(1) constant-time with thenumber of files in your application, as you generally only rebuild one file.I’m aiming for under 200 ms per rebuild with a typical build stack, since thattype of delay feels near-instantaneous to the human brain, though anything upto half a second is acceptable in my book.

2.2. Chainable Plugins

Another concern was making plugins composable. Let me show you how easy itis to compile CoffeeScript and then minify the output with Broccoli.

1234
var tree = broccoli.makeTree('lib')tree = compileCoffeeScript(tree)tree = uglifyJS(tree)return tree

With Grunt, we’d have to create a temporary directory to store theCoffeeScript output, as well as an output directory. As a result of all thisbookkeeping, Gruntfiles tend to grow rather lengthy. With Broccoli, all thisis handled automatically.

3. Architecture

For those who are curious, let me tell you about Broccoli’s architecture.

3.1. Trees, Not Files

Broccoli’s unit of abstraction to describe sources and build products is not afile, but rather a tree – that is, a directory with files and subdirectories.So it’s not file-goes-in-file-goes-out, it’s tree-goes-in-tree-goes-out.

If we designed Broccoli around individual files, we’d be able to compileCoffeeScript just fine (as it compiles 1 input file into 1 output file), butthe API would be unnatural for compilers like Sass (which needs to read morefiles as it encounters @import statements, and thus compiles n input filesinto 1 output file).

On the other hand, with Broccoli’s design around trees, n:1 compilers likeSass are no problem, while 1:1 compilers like CoffeeScript are an easilyexpressible sub-case. In fact, we have a Filter base class for such 1:1compilers to make them very easy to implement.

3.2. Plugins Just Return New Trees

This one is slightly more subtle: At first, I had designed Broccoli with twoprimitives: a “tree”, which represents a directory with files, and a chainable“transform”, which takes an input tree and returns a new compiled tree.

This implies that transforms map trees 1:1. Surprisingly, this is not a goodabstraction for all compilers. For instance, the Sass compiler has a notion of“load paths” that it searches when it encounters an @import directive.Similarly, JavaScript concatenators like r.js have a “paths” option to searchfor imported modules. These load paths are ideally represented as a set of“tree” objects.

As you can see, many real-world compilers actually map n trees into 1 tree.The easiest way to support this is to let plugins deal with their input treesthemselves, thereby allowing them to take 0, 1, or n input trees.

But now that we let plugins handle their input trees, we don’t need to knowabout compilers as first-class objects in Broccoli land anymore. Pluginssimply export functions that take zero or more input trees (and perhaps someoptions), and return an object representing a new tree. For instance:

12345
broccoli.makeTree('lib') // => a treecompileCoffeeScript(tree) // => a treecompileSass(tree, { loadPaths: [moreTrees, ...]}) // => a tree

3.3. The File System Is The API

Remember that because Grunt doesn’t support chaining of plugins, we end uphaving to manage temporary directories for intermediate build products in ourGrunt configurations, making them overly verbose and hard to maintain.

To avoid all this, our first intuition might be to abstract the file systemaway into an in-memory API, representing trees as collections of streams. Gulpfor instance does this. I tried this in an early version of Broccoli, but itturns out to make the code quite complicated: With streams, plugins now haveto worry about race conditions and deadlocks. Also, in addition to having anotion of streams and paths, we need file attributes like last-modified timeand size in our API. And if we ever need the ability to re-read a file, orseek, or memory-map, or if we need to pass an input tree to another processwe’re shelling out to, the stream API fails us and we have to write out theentire tree to the file system first. So much complexity!

But wait. If we’re going to replicate just about every feature of the filesystem, and in some cases we have to fall back to turning our in-memoryrepresentation into an actual tree on the file system and back again, then …why don’t we use the actual file system instead?

Node’s fs module already provides as compact an API to the file system as wecould wish for.

The only disadvantage is that we have to manage temporary directories behindthe scenes, and clean them up. But that’s easy to do in practice.

People sometimes worry that writing to disk is slower. But even if you hit theactual disk drive (which thanks to paging is rare), the bandwidth of modernSSDs has become so high compared to CPU speed that the overhead tends to benegligible.

3.4. Caching, Not Partial Rebuilding

When I originally tried to solve the problem of incremental rebuilds, I triedto devise a way to check whether each existing output file is stale, so thatBroccoli could trigger the rebuild for a subset of its input files. But this“partial rebuild” approach requires that we are able to trace which files anoutput file depends on, all the way back to the source files, and it alsomakes file deletion tricky. “Partial rebuilds” is the classical approach ofMake, as well as the Rails asset pipeline, Rake::Pipeline, and Brunch, butI’ve come to believe that it’s unnecessarily complicated.

Broccoli’s approach is much simpler: Ask each plugin to cache its build outputas appropriate. When we rebuild, start with a blank slate, and re-run theentire build process. Plugins will be able to provide most of their outputfrom their caches, which takes near-zero time.

Broccoli started off providing some caching primitives, but it turned outunnecessary to have this in the core API. Now we just make sure that thegeneral architecture doesn’t stand in the way of caching.

For plugins that map files 1:1, like the CoffeeScript compiler, we canuse common caching code (provided by thebroccoli-filter package), leavingthe plugin code lookingvery simple.Plugins that map files n:1, like Sass, need to be more careful aboutinvalidating their caches, so they need to provide custom caching logic. Iassume that we’ll still be able to extract some common caching logic in thefuture.

3.5. No Parallelism

If we all suffer from slow builds, should we try to parallelize builds,compiling multiple files in parallel?

My answer is no: The reason is that parallelism makes it possible to haverace conditions in plugins, which you might not notice until deploy time.These are the worst kinds of bugs, and avoiding parallel execution eliminatesthis entire class of bugs.

On the other hand, Amdahl’s lawstops us from gaining much performance through parallelizing. For a simplifiedexample, say our build process takes 16 seconds in total. Let’s say 50% of itcan be parallelized, and the rest needs to run in sequence (e.g.CoffeeScript-then-concatenate-then-UglifyJS). If we run this on a 4-coremachine, the build would take 8 seconds for the sequential part plus 8 / 4 = 2seconds for the parallel part, still totaling 10 seconds, less than a 40%performance gain.

For incremental rebuilds, which constitute the hot path that we really careabout, caching tends to eliminate most of the parallelizable parts of thebuild process anyway, so we are left with little to no performance gain.

Because of that, in general I believe that parallelizing the build process isnot a good trade. In principle you could write a Broccoli plugin that performssome work in a parallel fashion. However, Broccoli’s primitives, as well asthe helper code that I’ve published on GitHub, actively encouragedeterministic sequential code patterns.

4. Background / Larger Vision

There are two main motivators that made me tackle writing a good build tool.

The first motivator is better productivity, through fast incremental rebuilds.

I generally believe that developer productivity is largely determined by thequality of the libraries and tools we use. The “edit file, reload browser”cycle that we perform hundreds of times a day is probably the core feedbackloop when we program. A great way to improve our tooling is getting thisedit-reload feedback loop to be as fast as humanly possible.

The second motivator is encouraging an ecosystem of front-end packages.

I believe that Bower and the ES6 module system will help us build a greatecosystem, but Bower by itself is useless unless you have a build tool runningon top. This is because Bower is a content-agnostic transport tool that onlydumps all your dependencies (and their dependencies, recursively) into thefile system—it’s up to you what to do with them. Broccoli aims to become themissing build tool sitting on top.

Note that Broccoli itself is angnostic about Bower or ES6 modules—you can useit for whatever you like. (I am aware there are other stacks, like npm +browserify, or npm + r.js.) I will discuss all of this in more detail in afuture blog post.

5. Comparison With Other Build Tools

If you are almost convinced but also wondering how other build tools stack upagainst Broccoli, let me tell you why I wrote Broccoli instead of using any ofthe following:

Grunt is a task runner, and it never set out to be a build tool. If youtry to (ab)use it as a build tool, you quickly find that because it doesn’tattempt to handle chaining (composition), you end up having to managetemporary directories for intermediate build products yourself, adding a lotof complexity to your Grunt configuration. It also does not support reliableincremental rebuilds, so your rebuilds will tend to be slow and/or unreliable;see section “Fast Rebuilds” above.

That said, Grunt’s utility as a task runner is in providing a cross-platformway to run shell-script type functionality, such as deploying your app orgenerating scaffolding. Broccoli will be able to act as a Grunt plugin in thefuture, so that you can call it from your Gruntfile.

Gulp tries to solve the problem of chaining plugins,but in my view it gets the architecture wrong: Rather than passing aroundtrees, it passes around sequences (= event streams) of files (= streams orbuffers).This works fine for cases where one input file maps into one outputfile. But when a plugin needs to follow import statements, and thus needs toaccess input files out of order, things get complicated.For now, plugins that follow import statements tend to just just bypass thebuild tool and read directly from thefile system.In the future, I hear that there will be helper libraries to turn all thestreams into a (virtual) file system and pass that to the compiler. I wouldclaim though that all this complexity is a symptom of an impedance mismatchbetween the build tool and the compiler. See “Trees, Not Files” above for moreon this. I’m also not convinced that abstracting away files behind a stream orbuffer API is helpful at all; see “The File System Is The API” above.

Brunch, like Gulp, uses a file-based (not tree-based) in-memory API (seethis methodsignature).Like with Gulp, plugins end up falling back to bypassing the buildtoolwhen they need to read more than one file.Brunch also tries to do partial rebuilding rather than caching; see section“Caching, Not Partial Rebuilding” above.

Rake::Pipeline is written in Ruby, which is less ubiquitous than Node infront-end land. It tries to do partial rebuilds as well. Yehuda says it’s notheavily maintained anymore, and that he’s betting on Broccoli.

The Rails asset pipeline uses partial rebuilds as well, and uses verydifferent code paths for development mode and production (precompilation)mode, causing people to have unexpected issues when they deploy. Moreimportantly it’s tied to Rails as a backend.

6. What’s Next

The list of plugins is stillsmall. If they are enough for you, I cautiously recommend giving Broccoli atry right now: https://github.com/joliss/broccoli#installation

I would like to see other people get involved in writing plugins. Wrappingcompilers is easy, but the hard and important part is getting caching andperformance right. We’ll also want to work on generalizing more cachingpatterns in addition tobroccoli-filter, so that pluginsdon’t suffer from excessive boilerplate.

Over the next week or two, my plan is to improve the documentation and cleanup the code base of Broccoli core and the plugins. We will also have to add atest suite to Broccoli core, and figure out an elegant way to integration-testBroccoli plugins against Broccoli core.Another thing that’s missing with the existing plugins is source map support.This is slightly complicated by performance considerations, as well as thefact that chained plugins need to consume other plugins’ source maps andinteroperate properly, so I haven’t found the time to tackle this yet.

Broccoli will see active use in the Ember ecosystem, powering the defaultstack emitted by ember-cli (anupcoming tool similar in functionality to the rails command). We are alsohoping to move the build process used for generating the Ember core andember-data distributions from Rake::Pipeline and Grunt to Broccoli.

That said, I would love to see Broccoli adopted outside the Ember community aswell. JS MVC applications written with frameworks like Angular or Backbone, aswell as JavaScript and CSS libraries that require build steps, are all primecandidates for being built by Broccoli.

I don’t currently see any major roadblocks on the path to Broccoli becomingstable. By using it for real-world build scenarios, we should gain confidencein its API, and I’m hoping that we can bump the version to 1.0.0 within a fewmonths’ time.

This blog post is the first comprehensive explanation of Broccoli’sarchitecture, and the documentation is still somewhat sparse. I’m happy tohelp you get started, and fix any bugs you encounter. Come find me on#broccolijs on Freenode, or atjoliss42@gmail.com on Google Talk. I’ll alsorespond to any issues you post on GitHub.

Thanks to Jonas Nicklas, Josef Brandl, Paul Miller, Erik Bryn, Yehuda Katz,Jeff Felchner, Chris Willard, Joe Fiorini, Luke Melia, Andrew Davey, and AlexMatchneer for reading and critiquing drafts of this post.

Discuss on Twitter

Broccoli: First Beta Release (2025)
Top Articles
Latest Posts
Recommended Articles
Article information

Author: Melvina Ondricka

Last Updated:

Views: 5902

Rating: 4.8 / 5 (48 voted)

Reviews: 87% of readers found this page helpful

Author information

Name: Melvina Ondricka

Birthday: 2000-12-23

Address: Suite 382 139 Shaniqua Locks, Paulaborough, UT 90498

Phone: +636383657021

Job: Dynamic Government Specialist

Hobby: Kite flying, Watching movies, Knitting, Model building, Reading, Wood carving, Paintball

Introduction: My name is Melvina Ondricka, I am a helpful, fancy, friendly, innocent, outstanding, courageous, thoughtful person who loves writing and wants to share my knowledge and understanding with you.