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.
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.
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
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:
- fetch files;
- look for non-relative
importstatements and remap them to the
- return amended files to the main thread.
More about Service Workers on MDN: Using Service Workers
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
- Transformation: to change the source code and the transpilation of non-standard syntaxes;
- Resolution: find and resolve
- 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
module.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;
- 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.
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
TodoListcomponent class definition.
Now, just run a local server (here installed globally):
npm install -g http-server
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.
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
- 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.
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:
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.