DEV Community

Cover image for Building without bundling: How to do more with less
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Building without bundling: How to do more with less

Written by Fred Schott✏️

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 coworkers, and got some hard statistics on the four major applications I’d worked on over the last 3 years.

Here were the results:

A comparison of real-world dev performances across apps.

Okay, 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.

LogRocket Free Trial Banner

Reclaiming your time

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.

Minimum viable build tooling

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:

<!-- Glitch EMBED -->
<div class="glitch-embed-wrap" style="height: 420px; width: 100%;">
  <iframe
    src="https://glitch.com/embed/#!/embed/laced-newsprint-1?path=&previewSize=0"
    title="undefined on Glitch"
    allow="geolocation; microphone; camera; midi; vr; encrypted-media"
    style="height: 100%; width: 100%; border: 0;">
  </iframe>
</div>
Enter fullscreen mode Exit fullscreen mode

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.

Roadblock: NPM

import React from 'react';

/* TypeError: Failed to resolve module specifier 'react' */
Enter fullscreen mode Exit fullscreen mode

“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 */
Enter fullscreen mode Exit fullscreen mode

“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.

Re-defining the bundler

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.

How @pika/web installs any npm package into a single JavaScript file that runs 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.

Pika quickstart

Let's 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
Enter fullscreen mode Exit fullscreen mode

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> */
Enter fullscreen mode Exit fullscreen mode

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.

Pika in action

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)
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

I miss my build tools

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

CDNs – avoiding dependency management entirely

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:

  • UNPKG: A popular CDN that serves the file-by-file contents of any npm package. Comes with a really neat ?module flag that will rewrite imports from bare specifiers (ex: “lodash-es”) to relative UNPKG URLs (ex: “/lodash-es/v3.1.0/lodash.js”).
  • Pika CDN: The Pika Project has a CDN as well, but instead of serving individual files it serves entire single-file packages. Think of it like a hosted @pika/web. These single-file packages load much faster than UNPKG, and the CDN is smart enough to serve you the minimal number of polyfills/transpilation needed by your browser. But the downside is that you can’t import by internal package files—for now, it’s only entire packages.

What about legacy browsers?

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).

This graph displays load times for JavaScript modules across different browsers.

For some sites — especially those focused on mobile & 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>
Enter fullscreen mode Exit fullscreen mode

Conclusion

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.


Editor's note: Seeing something wrong with this post? You can find the correct version here.

Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
Try it for free.


The post Building without bundling: How to do more with less appeared first on LogRocket Blog.

Top comments (0)