There's been a strange explosion in misinformation about browserify recently, particularly in comparisons to webpack.
Generally speaking, most of this confusion stems from how webpack is more willing to pull features into its core to ease discoverability while browserify is more likely to push features out to userland instead.
I think that longer-term, separability has more benefits from a maintenance and experimentation perspective, but that doesn't mean that you can't do pretty much everything webpack can do in the browserify ecosystem. It just means that there might be several different ways of doing that task to serve different use-cases. Diversity is good!. Diversity means that you can choose an approach that more closely matches the trade-offs that you need, but you might need to put in more work to evaluate the different options.
Hopefully this document will help somewhat with finding options to evaluate.
Webpack, to its credit, provided some useful ideas about how to handle splitting bundles up into multiple pages automatically. However, this idea was quickly taken up in browserify using the plugin API into factor-bundle.
If you want to, you can still split up bundles using -x
and -r
, but for
creating multi-page bundles, it's pretty easy to automate that away with
factor-bundle in a very similar way.
In both cases for browserify and webpack you will end up with 2 <script>
tags
you can include on each page.
To split up common assets from entry points x.js
and y.js
into bundles
bundle/x.js
and bundle/y.js
with a common bundle bundle/common.js
, in
webpack you can edit the webpack config with:
var webpack = require('webpack');
module.exports = {
entry: {
x: "./x.js",
y: "./y.js",
},
output: {
path: "bundle",
filename: "[name].js"
},
plugins: [
new webpack.optimize.CommonsChunkPlugin("common.js")
]
};
or in browserify with factor-bundle you can just do:
browserify x.js y.js -p [ factor-bundle -o bundle/x.js -o y.js ] \
-o bundle/common.js
or from the API, that would be:
var browserify = require('browserify');
var fs = require('fs');
var files = [ 'x.js', 'y.js' ];
var b = browserify(files);
b.plugin('factor-bundle', { outputs: [ 'bundle/x.js', 'bundle/y.js' ] });
b.bundle().pipe(fs.createWriteStream('bundle/common.js'));
To load bundles asynchronously you can easily whip something up with xhr and eval() or for a more automatic option there's partition bundle.
This is a small thing, but writing to stdout by default can be very handy.
Instead of how in webpack you do:
webpack main.js bundle.js
In browserify you can also write to a file without using the shell:
browserify main.js -o bundle.js
but if you don't specify -o
, output goes to stdout. This is a very handy
feature! It means that for example you can pipe to a minifier like uglify in a
very simple and straightforward way:
browserify main.js | uglifyjs -cm > bundle.js
This way, browserify doesn't need to include uglify by default, unlike webpack. Users are free to use different minifiers and will not be beholden to whatever version of uglilify that browserify ships with.
Plus you can make fancier pipelines without mucking about reading browserify core docs. Want to make both a minified and a minified+gzipped version?
browserify main.js | uglifyjs -cm | tee bundle.js | gzip > bundle.js.gz
These tricks work with watchify too:
watchify main.js -o 'uglifyjs -cm | tee bundle.js | gzip > bundle.js.gz' -dv
and factor-bundle:
browserify files/*.js \
-p [ ../ -o 'uglifyjs -cm | tee bundle/`basename $FILE` | gzip > bundle/`basename $FILE`.gz' ] \
| uglifyjs -cm | tee bundle/common.js | gzip > bundle/common.js.gz
Or you can use the API directly.
Webpack uses "loaders" to preprocess files, while browserify uses transforms.
In webpack you might do:
{
module: {
loaders: [
{ test: /\.coffee$/, loader: "coffee-loader" }
]
}
}
but this configuration applies globally across an entire project. This is considered rather risky in browserify land and reserved for special occasions. In browserify, you would do:
browserify -t coffeeify main.coffee > bundle.js
but this transform would only apply to local files, not to modules installed with npm and placed in node_modules/. This is because those modules were written by other folks with different opinions about how to structure their projects. In browserify-land, decisions about how to transform source code are made very locally at the package level, whereas loaders in webpack are global.
Additionally in browserify-land, any package may have a package.json that specifies special transforms to use. These transforms are applied automatically and transparently with the reasoning that the people who wrote the code for those packages know best what transformations are necessary to make the code work. Webpack takes the opposite opinion: that application developers should be responsible for all the loader transformations across the entire dependency graph. I don't think this idea will pan out at scale the way that it already has in the browserify ecosystem.
Webpack loaders also care about file extensions to an unusual degree and are trying to make a central registry of file extensions to loaders. In browserify, file extensions are handled at the individual application and transform level because transformations might apply to the same file extensions in different contexts: one to handle inlining static assets, another to expand file globs.
Handily, transforms can also be configured from both package.json, the api, and the command-line.
In webpack you can do:
webpack --devtool inline-source-map main.js bundle.js
whereas in browserify you would do:
browserify --debug main.js > bundle.js
Inline source maps are the default in browserify because they require no additional configuration about how or where to serve up extra static assets.
In webpack, separate source maps appear with:
webpack --devtool eval main.js bundle.js
which implicitly creates an external source map file that you will need to configure to be serve from the appropriate web root.
This can be done in browserify with an external tool, exorcist:
browserify --debug main.js | exorcist bundle.js.map > bundle.js
This creates a file bundle.js.map
that bundle.js
will point at as an
external source map.
In webpack, AMD "just works" when the files that webpack needs are hosted in the proper directory.
In browserify, you can obtain AMD with deamdify or browserify-shim.
In webpack lingo this is confusingly called "incremental compilation" and is built into webpack core with:
webpack --watch main.js bundle.js
In browserify, you can just do:
watchify main.js -o bundle.js -v
to watch and also print out a message when a build is complete. Unless something is very misconfigured, rebuilding with watchify after the first build should take a few hundred milliseconds at most, similar to webpack.
So many times people keep claiming that browserify "doesn't care about css". This is true, but very misleading. Browserify itself does not concern itself with css, but has plenty of extensibility points for different approaches toward modular css to bloom.
Webpack has one solution: put everything into the webpack asset graph. browserify has many approaches. I think that many different approaches map much better to the diversity of requirements that people have for building modular components.
First of all, there are lots of css transforms. Some of them are generic like brfs that you can combine with other libraries like insert-css:
var fs = require('fs');
var insertcss = require('insert-css');
insertcss(fs.readFileSync(__dirname + '/style.css', 'utf8'));
and then just compile with brfs:
browserify -t brfs main.js > bundle.js
or you can use format-specific css preprocessors such as lessify:
require('./whatever.less');
and then:
browserify -t lessify main.js > bundle.js
parcelify is a very overlooked but powerful answer to handling css in a modular way. parcelify uses browserify to build a dependency graph and automatically reads package.json files as necessary to concatenate all the necessary css.
To make a package with css assets, just put:
{
"name" : "my-module",
"version": "1.5.0",
"style" : "*.css"
}
into the package.json for the module.
Now when you require('my-module')
elsewhere starting from an entry point
main.js
and compile with browserify like usual:
browserify main.js > bundle.js
you'll also be able to run:
parcelify main.js -c bundle.css
to compile your css all in one big bundle!
There are options to apply css transforms and do file watching too.
You can also use npm-css to have
@import
for css files similarly to require()
in javascript code:
@import "typeahead";
@import "./foo.css";
.foo {
color: red
}
which will resolve typeahead
from node_modules
and foo.css
from the same
directory as the css file just like how require()
behaves.
npm-css doesn't actually have anything to do with browserify at all! It just so
happens to pair nicely for handling css. Instead of a main
field in
package.json, use a style
field to specify a css entry point.
atomify provides a slightly more opinionated solution, insofar as it's possible
to do that in the browserify ecosystem, for handling both javascript and css. It
can resolve @import
statements with the node require algorithm and uses
rework behind the scenes to provide
prefixes, inline images, and variables.
browserify and webpack disagree one some fundamentals. browserify prefers:
- more local decisions for better composition
- node and npm compatability
- userland experimentation and diversity instead of a large core
Browserify takes great pains to compose well at scale across many packages from many different authors. To do this, it's very important to make decisions locally instead of globally. When people mouth off that something like webpack is "more powerful" than browserify, first of all that doesn't mean anything. Second of all, don't assume that power is always good. The bigger a project gets and the deeper its dependency graph, the more important structuring effects at scale becomes. When transforms can operate globally over the entire graph, it's hard to predict where those transformations could fail or interfere with existing code. It's still possible to achieve these kinds of global actions with plugins and global transforms because occasionally nothing else will do, but ordinary transforms with narrower effect are much more highly encouraged.
webpack encorages overloading require()
to mean something very different from
what it means in node:
require("coffee!./cup.coffee");
This will make it very hard to reuse your code in anything that is not webpack, particularly node.
browserify will never encourage this, opting for simple node-style require() and node compatability where possible
browserify puts in a lot of tiny, subtle features so that modules from npm with
mostly "just work". The module lookup algorithm is exactly the same, including
tiny things like the order of operations and default extensions like .json
.
Node core modules that make sense in a browser context like path
and util
also just work without any special configuration.
According to some sophisticated static analysis done by airportyh, around half of the packages on npm are likely to just work with browserify. You can search this subset of packages on browserifysearch.org.
Be wary of bullet-point engineering.
Comparing technologies strictly on what checks all the boxes might seem like it will solve your problems faster, but lumping too many features together is a recipe for scope creep and inflexibility later on.