You make a change to your codebase. You hit save. You wait… and wait… and wait.
Web development used to be all about instant feedback. But with the introduction of web bundlers like webpack and Parcel, web development is no longer as simple as saving and refreshing your browser.
When you use a bundler, you’re stuck waiting for entire sections of your application to rebuild every time you change just one line of code.
How long do we spend waiting for these bundlers? This is something that I started thinking about recently. It was bugging me so much that I decided to find out.
I dusted off old sites on my laptop, reached out to old co-workers, and got some hard statistics on the four major applications I’d worked on over the last three years.
Here were the results:
OK, so let’s do some quick math.
On average, let’s say you test a change in your browser 10 times per hour, and start up the app every 3 hours (to change branches, detect new files, etc).
So, if you worked on App #3 (37-second start-time, 2.5 second recompile time) non-stop for one week, a full 40-hour week would introduce about 25 minutes of non-stop wait time.
For App #1 (42-second start-time, 11 second recompile time) that same math would have you waiting on your dev environment for over 1 hour (~82 minutes) every week.
Now, multiply that over years — that’s a lot of time spent waiting around for tooling. That figure is especially frustrating when you consider that JavaScript is a language already understood by the browser.
We’re not compiling Java here. For the most part, we’re writing browser-friendly JavaScript.
Is it possible to remove the bundler and skip this developer experience nightmare entirely? Simple demos already work fine without bundling, but what about building a real, fully featured web app? Can you do that?
It turns out that you can. Not only is modern “unbundled” development possible, but it gives you a dramatically faster developer experience.
No more 1,000+ dependency node_module/
folders, no more waiting for slow startups, and no more momentum-killing bundle rebuilds.
To show you what I mean, let’s walk through what it looks like to build a modern web app without a bundler today.
What’s the least amount of tooling you need to start out with? Browsers can’t load files directly from your computer, so the first thing you’ll need is a local static asset server.
Serve is a popular, simple CLI that serves any directory on your machine to http://localhost/
. It also comes with some extra goodies, such as Single Page Application (SPA) support and automatic live-reloading whenever a file changes.
By running npx serve
in your dev directory, you can easily spin up a basic site serving CSS, HTML & JavaScript locally:
You can get pretty far with this setup alone. Thanks to native ES Modules (ESM) syntax (supported in all modern browsers for the last 1+ years), you can import and export JavaScript natively using the type="module"
script attribute.
You can load your entire applications this way, all without a single line of additional tooling or configuration.
At some point, however, you’ll want to grab some code from npm. So, let’s try to use one of those native imports to load up the React framework to use in our app.
import React from 'react'; /* TypeError: Failed to resolve module specifier 'react' */
“Huh… that’s odd. This always works with webpack…”
Unbundled roadblock #1: Browsers don’t yet support importing by package name (known as importing by “bare module specifiers”).
Bundlers make modern web development possible by resolving specifiers like “react” to the correct entry point file automatically at build-time.
The browser doesn’t know where the “react” file lives, or where on the server your node_modules directory is served from for that matter.
To continue, you’re going to need to import packages by their true file path.
import React from '/node_modules/react/index.js'; /* ReferenceError: process is not defined */
“Ugh, now what?”
Unbundled roadblock #2: Most npm packages—even primarily web-focused packages—require a Node.js-like environment and will fail in the browser.
You’re seeing a “process is not defined” error because the first thing that React does is check process.env.NODE_ENV
, a Node.js-only global that is also normally handled by the bundler.
It’s important to remember that npm started as a Node.js ecosystem, and its packages are expected to run directly as written on Node.js.
Bundlers bandaid over these Node-isms for the browser, but at the expense of all of this extra tooling and wait time we highlighted above.
Even most web-friendly packages will still use that same “bare module specifier” pattern for any dependencies since there’s no way for an npm package to know where its dependencies will be installed relatively.
A few npm packages (Preact, lit-html, and others) are written to be served directly after installing, but you’re more or less limited to packages that have no dependencies and are authored by only a few thoughtful package maintainers.
So we’ve seen why npm packages can’t run in the browser without a bundler. But in the section before that, we also saw our own source code run in the browser just fine.
Doesn’t it seem like overkill to send our entire application through a time-consuming dev pipeline on every change just to solve a problem in our dependencies?
I started @pika/web to experiment: If modern JavaScript has evolved to the point where it has a native module system, we no longer need to run it through a bundler. In that case, can you re-scope bundling to focus only on the remaining issues in npm?
Dependencies change much less frequently — this new tool would only need to run on your node_modules/
folder after npm/yarn install, not after every change.
@pika/web installs any npm packages into a single JavaScript file that runs in the browser. When it runs, internal package imports are resolved to something that the browser will understand, and any bad Node-isms are converted to run in the browser.
It is an install-time tool focused only on your dependencies, and it doesn’t require any other application build step.
For best results, you should look to use modern packages containing native ESM syntax.
NPM contains over 70,000 of these modern packages; chances are that you’re probably already using some in your web application today. You can visit pika.dev to search and find ones for any use case.
If you can’t find the exact package you’re looking for, @pika/web is also able to handle most non-ESM, legacy NPM packages.
Lets use @pika/web to install the smaller ESM alternative to React: Preact. In a new project, run the following:
npm init # Create an empty package.json, if you haven't already npm install preact --save # Install preact to your node_modules directory npx @pika/web # Install preact.js to a new web_modules directory serve . # Serve your application
Now, your application can use the following import directly in the browser, without a build step:
import {h, render} from '/web_modules/preact.js'; render(h('h1', null, 'Hello, Preact!'), document.body); /* <h1>Hello, Preact!</h1> */
Try running that in your browser to see for yourself. Continue to add dependencies, import them in your application as needed, and then watch serve
live-reload your site to see the changes reflected instantly.
No one likes to use raw h()
calls directly. JSX is a popular syntax extension for React & Preact, but it requires a build-step like Babel or TypeScript to work in the browser.
Luckily, Preact’s Jason Miller created a web-native alternative to JSX called htm
that can run directly in the browser:
import {h, render} from '/web_modules/preact.js'; import htm from '/web_modules/htm.js'; const html = htm.bind(h); render(html`<h1>Hello, ${"Preact!"}</h1>`, document.body)
Likewise, if you want to apply CSS to your UI components, you can use a web-native CSS library like CSZ:
import css from '/web_modules/csz.js'; // Loads style.css onto the page, scoped to the returned class name const className = css`/style.css`; // Apply that class name to your component to apply those styles render(html`<h1 class=${headerClass}>Hello, ${"Preact!"}</h1>`, document.body);
There’s a ton of excitement growing around this “unbuilt” development. If you use @pika/web to install modern npm packages, you’ll never need to wait for a build step or recompilation step again.
The only thing you’re left waiting for is the 10-20ms live-reload time on your local dev server.
You can still always choose to add a build step like Babel or even TypeScript without adding a bundling step.
Build tools are able to compile single-file changes in a matter of milliseconds, and TypeScript even has an --incremental
mode to keep start-time quick by picking up where you last left off.
/* JavaScript + Babel */ import {h, render} from '/web_modules/preact.js'; render(<h1>Hello, Preact!</h1>, document.body); /* CLI */ babel src/ --out-dir js/ --watch
With Babel, you’re also able to grab the @pika/web Babel plugin, which handles the bare module specifier conversion (“preact” → “web_modules/preact.js“) automatically.
/* JavaScript + Babel + "@pika/web/assets/babel-plugin.js" */ import {h, render} from 'preact'; render(<h1>Hello, Preact!</h1>, document.body);
Our final code snippet is indistinguishable from something you would see in a bundled web app.
But by removing the bundler, we were able to pull hundreds of dependencies out of our build pipeline for a huge dev-time iteration speed-up.
Additionally, the @pika/web README has instructions for those interested in using React instead of Preact.
Content delivery networks (CDNs) are capable of serving assets for public consumption, which means that they’re also capable of fixing up bad npm packages for us.
CDNs are becoming increasingly popular for full dependency management, and some projects like Deno embrace them for all dependency management.
There are two options worth checking out when it comes to running npm packages directly in the browser:
?module
flag that will rewrite imports from bare specifiers (e.g., lodash-es
) to relative UNPKG URLs (e.g., lodash-es/v3.1.0/lodash.js
).The biggest concern around unbundled web development is that it will only run on modern browsers. Caniuse.com reports that 86 percent of all users globally support this modern ESM syntax, which includes every major browser released in the last 1–2 years.
But that still leaves 14 percent of users on legacy browsers like IE11 or UC Browser (a web browser popular in Asia).
For some sites — especially those focused on mobile and non-enterprise users — that might be fine. https://www.pika.dev, for example, generally has a more modern user base and we’ve only received a single complaint about serving modern JavaScript over the last year of operation.
But, if you need to target legacy browsers or are worried about loading performance, there’s nothing stopping you from using a bundler in production. In fact, that kind of setup would get you the best of both worlds: a local dev environment that lets you iterate quickly, and a slower production build pipeline powered by webpack or Parcel that targets older browsers.
<!-- Modern browsers load the unbundled application --> <script type="module" src="/js/unbundled-app-entrypoint.js"></script> <!-- Legac browsers load the legacy bundled application --> <script nomodule src="/dist/bundled-app-entrypoint.js"></script>
For the first time in a long time, you get to choose whether or not you use a bundler.
Projects like Pika and tools like @pika/web are all about giving you back that choice. They’re about giving everyone that choice, especially anyone who doesn’t feel as confident with JavaScript yet, or bundler configuration, or 1000+ dependency installations, or all the breaking changes and oddities that come up across a bundler’s plugin ecosystem.
I expect that the next few years of web development will be all about simplicity: support advanced tooling for advanced users, and at the same time drop barriers to entry for others.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowMaking carousels can be time-consuming, but it doesn’t have to be. Learn how to use React Snap Carousel to simplify the process.
Consider using a React form library to mitigate the challenges of building and managing forms and surveys.
In this article, you’ll learn how to set up Hoppscotch and which APIs to test it with. Then we’ll discuss alternatives: OpenAPI DevTools and Postman.
Learn to migrate from react-native-camera to VisionCamera, manage permissions, optimize performance, and implement advanced features.
4 Replies to "Building without bundling: How to do more with less"
Thank you, your post is so helpful to me! I hate compiling stuff
The package is now called snowpack and is nothing like the stuff you mentioned in the article. It now builds your code too, like webpack, has a built in devserver and it is far from lightweight, since upon installing it will add 209 packages to your node_modules folder. There is a create-snowpack-app and a snowpack.config.json
This have esentially became another webpack. If your goal is to not have to transform any of my js files I would say that this is not the tool you are looking for. If it would do what the article said and only remain at transforming node_modules to web_modules, then it would be perfect.
I forgot about this article! You’re right, `@pika/web` is now Snowpack and is focused more on helping you build & develop your entire website (not just your dependencies). But, if you want the functionality described in this article `snowpack install` is still the same command as the original `@pika/web`.
I’ve followed your guide, all went Ok except for serve not having any hot reload feature. The package description page doesn’t mention it at all. I keep asking myself since started to learn some web modern development, and with several years of C/C++ experience on the back: ÂżWhy is all this shit trumpeted as the 21th century miracle? Broken functionality everywhere, all the time. Have any superstar web programmer ever touched an ANSI C compiler? Can web people embrace the philosophy of a lasting and working piece of software?