3 ways webpack surprises web developers
What I learned answering webpack questions on StackOverflow for a month
When I first started working with webpack, I didn't realize how under-prepared I was. I was tasked with integrating webpack into a large Rails app and I made a lot of mistakes along the way. I assumed how things should behave based on my previous experience with the Rails asset pipeline. Many of these assumptions turned out to be wrong. This was frustrating and humbling.
And after spending the last month answering webpack questions on StackOverflow, I've come across plenty of folks going through some of the same mental hurdles I've experienced. I came away with some perspective on what about webpack most commonly trips up developers.
Subscribe to my newsletter, Joy of Rails, to get notified about new content.
The intended audience for this post has a general notion of "why use webpack" or "why use an asset bundler", but for more on that, I recommend The Many Jobs of JS Build Tools and webpack from Nothing: What problem are we solving?. For a rigorous technical overview of the project, I suggest the webpack docs; they have gotten quite good.
For this post, we're going to look at three common surprises web developers face when learning webpack: why using global variables doesn't behave the way you might think, how webpack treats everything as a JavaScript module, and the big learning curve for configuring webpack effectively.
1. Global variables are not your friend
I learned to program using script tags and html files loaded directly in the browser. I tied everything together with global variables. It was great.
And for better or worse, every Rails I've worked on, and it's been dozens over the years, has relied on global variables and script tag snippets to make things work. Here is a basic example:
<!-- app/view/posts/index.html.erb -->
<%= @posts.each do |post| %>
<!-- ... -->
<% end %>
<a href="#" class="button--show-more">Show more</a>
<script>
$('.button--show-more').click(function() {
MyApp.fetchPosts() // etc...
})
// MyApp and $ are global variables
</script>
This approach is typical with old-school bundlers like the Rails asset pipeline because they concatenate JavaScript dependencies in the global scope. This, despite the general notion that global variables are bad. Notably, the Rails asset pipeline came into existence before the rise of Node.js and, subsequently, formal JavaScript modules, and it never adapted. Many prefer this way of doing things. I still lean on global variables now and then.
Things work differently in webpack. It does not expose its bundled modules to the global scope by default. To reference code in another module, it expects explicit imports that reference that module's explicit exports. The scope in which modules are evaluated is local, not global, i.e., the contents of each file are wrapped in a function.
Things are trickier if we expect to access bundled JavaScript from HTML, like MyApp.fetchPosts()
above. Options include manually attaching variables to the global scope, e.g. window.$ = require('jquery')
or modify the webpack configuration to "expose" variables globally, as is demonstrated in this StackOverflow post (and many others).
This serves as an illustration of how a legacy practice would be swimming upstream in a Webpacker-enabled app: it takes effort.
But why?
Webpack is a module bundler
Webpack describes itself as "a static module bundler for modern JavaScript applications". For developers used to unfettered access to JavaScript global scope, the switch to working in a modular system comes as a surprise. I argue that adopting webpack effectively means understanding JavaScript modules.
So what then is a JavaScript module?
For a fantastic introduction to JavaScript modules, I suggest Preethi Kasireddy's Javascript Modules: A Beginner's Guide on freeCodeCamp. I'll attempt to summarize.
Generally speaking, a JavaScript module is a self-contained, reusable piece of code. This definition though is inadequate to capture the behavior of various flavors of JavaScript modules, ranging from simple patterns to formal systems supported by common JavaScript runtimes.
In recent years, several popular JavaScript module definitions have become widely adopted, each with their own characteristics, including CommonJS, Asynchronous Module Definition (AMD), and EcmaScript (ES) Modules to name a few.
Webpack can be configured to recognize any of these module formats.
Webpack transpiles your application's source files into JavaScript modules the browser can understand. It adds code to your bundle to tie these modules together. This has implications for how developers write code which means the old-school patterns that worked with the Rails asset pipeline may not work in the webpack context.
Avoid legacy code if you can
Some of the most frequent webpack issues that pop up on StackOverflow highlight this disparity between the context in which webpack works best and the context for which legacy code was written.
Consider any jQuery plugin in your app that's more than a few years old; any one of them may not play nice with webpack. The plugin system in a way is a relic of the pre-module era; attaching to a global variable was the easy way to reuse and reference functionality across the app.
Many jQuery plugins (or many legacy plugins in general) have been written without awareness of JavaScript modules and assume execution within the global scope. Be ready to weigh the tradeoff of learning how to configure webpack to play nicely with legacy code or replace it with something else altogether.
In webpack, global variables are not your friend, my friend.
2. Webpack treats everything as a JavaScript module
Webpack is so committed to its "module bundler" role it treats other static assets, including CSS, images, fonts, etc., as JavaScript modules too.
Say what?
When I first learned this about webpack, I was totally confused: How does webpack produce stylesheets out of JS? How would I reference the an image tag's src
for bundled images? What does it mean to import an image module in JavaScript?
It helps to understand that webpack must be configured, typically with loaders or plugins, to handle different various files types as modules. How webpack processes various file types as output depends which loaders are used.
Many projects integrate with Babel to process JavaScript files written with ES2015+ syntax. CSS files might be bundled as JavaScript Blob objects that are dynamically inserted in the DOM; otherwise it can be extracted into a CSS stylesheet a side-effect of module compilation.
Webpack only needs one JavaScript file in your source code as an entry point to produce a dependency graph of all the JavaScript, CSS, images, fonts, svg, etc. that you intend to bundle as static assets for the browser.
An interesting consequence of webpack putting JavaScript first is there only needs to be one entry point to produce both a JavaScript and a CSS bundle. In the Rails asset pipeline, the JavaScript and CSS source code is kept completely separate:
app/assets
├── javascripts
│ └── application.js # produces js bundle
└── stylesheets
└── application.css # produces css bundle
In Webpack everything hangs off the javascript entry point, or "packs". So assuming you have statements like import 'styles.css'
somewhere in your JavaScript dependency graph, both application.js
and application.css
bundles will be produced.
app/javascript
└── packs
└── application.js # produces both js and css bundles
The mixing of CSS bundled in JavaScript and treated as JavaScript modules has isn't strictly necessary, but it most certainly a mental leap for the uninitiated.
3. Webpack configuration is extremely pluggable
There's a reason webpack configuration has such a high barrier to entry: webpack is the ultimate delegator.
I continue to be amazed at how many learners seem to almost deliberately avoid reading the actual official docs for the tools they're trying to use. I keep seeing folks asking for Udemy courses and "best tutorials" and stuff.
— Mark Erikson (@acemarke) January 5, 2020
Why do people avoid reading actual docs?
Coming from Rails, which famously values "convention over configuration", the ergonomics of setting up a webpack configuration cause discomfort. It aims to be extremely flexible and extensible; to that end, it succeeds superbly. To serve this goal, webpack provides a large array of configuration options. On top of that, most webpack configurations bring in a number of loader and plugins, each of which have their own configuration requirements.
Faced having to learn webpack, Babel, PostCSS, not to mention, Webpacker's abstractions around webpack, it's no wonder we're intimidated. That's a lot to wrap your head around.
One of Webpacker's goals, in a similar fashion to create-react-app and the vue-cli, is to provide a webpack config with sane defaults, i.e. the "convention". Depending on your project's needs, these "out-of-the-box" setups may get you quite far. Unfortunately, for any non-trivial modification, like getting a large legacy library to work with global variables or optimizing your build time by splitting out vendor dependencies, developers must be prepared to dive into the documentation and search for answers far and wide on StackOverflow and Medium.
4. Bonus: Webpack is a powerful tool
I've grown to love webpack and, I admit, this appreciation was hard-earned. As I've gotten over the initial hurdles of making my webpack config work for my projects, I've come to value a number of webpack's benefits, including optimizing bundle size through tree-shaking, code splitting via asynchronous dynamic imports and the split chunks plugin and support for preloading and prefetching. All of these features are virtually non-existent in the Rails asset pipeline.
These major strengths of webpack all boil down to improving user experience: using it effectively can help improve metrics like Time-to-Interactive and First Contentful Paint. These things matter and are ever more crucial as we lean more heavily on client-side code build rich interfaces delivered across a widening array of devices and networks.
Webpack receives a fair number of criticisms regarding its complexity and some of its surprising traits, like the ones I highlighted here. To be fair, webpack aims to solve a complex problem and solves it quite well. Other asset bundlers are worth your consideration, but, arguably, no other bundler has been as successful.
As we saw in the recent announcement from @dhh and the release of Rails 6 last year, webpack is now the default JavaScript compiler for Rails. Looks like Rails developers will be looking to adopt webpack in their applications, though as we've seen today, they may be in for a few surprises.
webpack is now the default JavaScript compiler for the upcoming Rails 6 🎉 https://t.co/LJzCSoPfCV
— DHH (@dhh) October 1, 2018