How to Improve the Frontend Dev Experience without a Bundler

A look at how you can use ES6 modules and Service Workers to launch a web app without Webpack or Rollup.

Typical example of a developer waiting for bundle to be ready.

Introduction

🇮🇹 Also available in Italian

The ECMAScript 2015 (ES6) specification brought JavaScript and web development into a new era, made of clean syntaxes, better scaffolding of source files and a set of developer tools available in other programming contexts — like static code analysis, dependencies systems, autocompletion and more.

All of this came at a price: launching a web app in the browser can now require hundreds of node modules, a watcher to detect file changes and exhausting source rebuilds. A few seconds lost can easily become hours in a week, a waste of disk space, extreme RAM and CPU consumption and, for slow performing computers, fans swirling like aeronautical engines.

Case study

As a developer, I would like to create a classic To-Do List app, using a component library with JSX support and run it in the browser, without having to start a bundler, a transpiler and a watcher.

😱😱😱

Let’s say that we all love Preact (as it should be) — getting the boilerplate (and its dependencies) alone, means downloading ~1.500 node modules, ~26.000 files, and requires an overall disk space of about 200MB. Most of those dependencies are used to run a web server baked by Webpack.

But we don’t want any of this, so we have to rely only on the features of a modern browser.

How to load an ES6/7/X application in the browser without a bundler

Since Chrome 62, Edge 16, Safari 11 and Firefox 54, it is possible to import an ES6 module into the browser:

When the HTML parser meets this tag, it fetches the source file specified and recursively resolves all import and export statements.

Although this new feature is a wonderful one, it is not sufficient to launch a modern and complex web application. Browsers are able to resolve only relative dependencies and they have no information about NPM dependencies. Furthermore, JSX usage will throw multiple Syntax Errors, because it is not a standard of the JavaScript language.

Service Workers to the rescue

When everything seems lost, Service Workers come to our aid. Those special Workers, once registered, can intercept network requests and handle their response.

So, we may use SW to:

  • intercept JavaScript requests;
  • fetch files;
  • look for non-relativeimport statements and remap them to the node_modules folder;
  • detect JSX syntax and transpile all JSX expressions to JavaScript;
  • return amended files to the main thread.

At this point the browser should be able to handle the JavaScript files and resolve their dependencies.

More about Service Workers on MDN: Using Service Workers

Developing Unchained

Unchained is a proof of concept hosted on GitHub that puts this idea into practice. Like bundlers and transpilers we already use, it was designed to split the process into steps and it is pluggable.

It provides some useful helpers to register Service Workers, a polyfill to support dynamic import and its core (similar to the Rollup API) can resolve JavaScript file imports through the following steps:

  • Transformation: to change the source code and the transpilation of non-standard syntaxes;
  • Resolution: find and resolveimport statements;
  • Finalization: returns the generated code.

All updates to the final bundle are done with Babel Standalone, a Babel distribution that runs within the browser and enables plugins to handle the Abstract Syntax Tree (AST) of the file directly, tracking changes and generating the final sourcemap.

The following Unchained plugins are already available (and quite basic at the moment):

  • common: replaces CommonJS expressions, such as require andmodule.exports, with ES6 statements;
  • babel: communicates with Babel Standalone and its plugins in order to transpile non-standard syntaxes like JSX, Flow or Typescript(!);
  • text: converts text files into ES6 modules;
  • json: converts JSON files into ES6 modules;
  • env: performs the injection of environment variables (passed using a JavaScript Object, since browsers have no direct access to actual env variables);
  • resolve: is a partial implementation of the node resolution algorithm.

Moreover, Unchained uses the cache interface of the browser to skip already resolved files. Using the ETag header, it can also detect changes, so page reloads and code injections are damn fast.

Make the developer happy

Going back to our case, we may think of a solution and, to implement it, we are using the simple example shown on the home page of preactjs.com.

Project setup

At start, we need only two dependencies:

npm init -y
npm install preact unchained-js
# 2 modules, 65 items, < 2MB

and the following files:

  • index.html: where the Service Worker is registered and application files are imported
  • sw.js: the Service Worker to be registered, it uses Unchained library to transpile the source code

Now, just run a local server (here installed globally):

npm install -g http-server
http-server .

and… done!

In the console, Unchained logs all the files imported at launch: later, it is going to use the cache for reloads that follow.

On each file change, e.g. if we edit the button’s label in todolist.component.js and save, only the that file is going to be reloaded by the browser.

A Git project of this example is available here.

Conclusions

Despite browser support being limited to Chrome and Firefox (as for this one it is not available by default, but you need to turn on the flag dom.moduleScripts.enabled), I think that this approach may simplify the project structure and its configuration:

  • it considerably reduces the amount of dependencies to install;
  • transpiling does not block the main UI thread, because everything happens in the Service Worker’s context;
  • after the first launch, source code updates are really fast;
  • code splitting with dynamic import() just works;
  • sourcemaps support.

But there are cons too:

  • the first launch is not performant (due to the lack of direct file system access and the need to create an AST for every single file);
  • the treeshaking is missing, a feature available in Rollup and Webpack 4.

Real test

Things are always easy in a To-Do List app, but what about real world examples? I replaced Rollup with Unchained in a complex project I am working on, that features many dependencies like jQuery, Moment, CKEditor and a component library, with the following results:

Comparison made with a MacBook Pro (Retina, 15-inch, Mid 2015) and Chrome 63.0.3239.84

Future

Although Unchained covers all the aspects of this simple study, I would like to deepen some topics and improve the results:

  • speed up the first execution by using only AST transformations (disabled because at the moment there is a problem generating sourcemaps);
  • verify if it is possible to integrate Turbo once available, in order to improve the non-relative paths resolution process;
  • extend the compatibility to browsers that don’t support Service Workers or ES6 modules;
  • extend the support to the node environment to obtain application bundles that are 100% compatible with the in-browser preview;
  • blob (for images and fonts loading) and postCSS plugins.

Special thanks
A huge thanks to xho and SteRosanelli, for their patience in reviewing this article.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Edoardo Cavazza

Software Architect and Accessibility Consultant @ Chialab. Working on synergy between designers, developers and tools. More: WebComponents, Edtech, Typography