Back to Table of Contents

Bundling and Import Maps

What CoPilot thinks a cat looks like

By Jesse Pence

Introduction

So, we now have two versions our app. In one, we have fully embraced a build step, and we are using TypeScript to compile our code to JavaScript. In the other, we are using JSDoc annotations and the --checkJs flag to get most of the benefits of TypeScript without the build step. These two versions of our app will diverge even further in this chapter by exploring the world of bundling.

To fully demonstrate the benefits of bundling, we will have to add a few more features to our app. We will add a calendar to our app for upcoming events, and we will bring in Stripe to handle our payments. Our beautiful custom checkout form will be replaced with Stripe’s boring one. So much for all that hard work, right?

I’ll be using some heavy external libraries to demonstrate tree-shaking and code-splitting. Other than Stripe, we will use FullCalendar and date-fns for the event calendar. These are some popular libraries, but they are all quite large. date-fns alone is larger than our entire app.

In our TypeScript version, we will experiment with a few different bundlers to see how they can help us optimize the process of adding these new features to our app. In our JSDoc version, we will try out a new browser API that will allow us to easily use these external modules without a bundler— import maps. Now available in every major browser, they can be a great way to get the development experience and reliability of bundling without the build step. Let’s get started.

Why Bundle JavaScript?

Including a modular program that consists of 200 different files in a web page produces its own problems… Because fetching a single big file tends to be faster than fetching a lot of tiny ones, web programmers have started using tools that roll their programs (which they painstakingly split into modules) back into a single big file before they publish it to the Web. Such tools are called bundlers. —Marijn Haverbeke, Eloquent JavaScript

As we explored in our chapter about EcmaScript Modules, people have been trying to make it easier to use multiple javascript files together for a long time. People started by just stringing together multiple script tags in the head of the document, but this led to many issues. Not only did it make it hard to manage dependencies, but it was terrible for performance.

Until HTTP2, browsers would only download a few files at a time. So, if you had a lot of files, the browser would have to wait for each file to download before it could start downloading the next one. The browser’s main UI thread is blocked while the browser downloads and executes each script. This means that if you have a large script in the head of your document, the browser stops rendering the page until that script has been downloaded and executed. This can lead to a poor user experience as the user stares at a blank screen or tries to interact with elements that don’t have event listeners yet.

Before Webpack

These bundlers started to be created, what they do, their main premise is to allow you to write CommonJS modules. But, then getting it bundled, stripping those statements, and then executing it in the same order so that it works in the web exactly as it might work or you would expect it to work in your code. —Sean Larkin, History of Modules

To solve these issues, the simplest answer is to just put all of your JavaScript in one file. This is called bundling. In the early years of CommonJS, people would sometimes use task runner tools like Grunt and Gulp to do this. These tools could concatenate all of your JavaScript files together— literally putting the code for each file in one big file.

This helped performance, but it didn’t solve the dependency management issues. You still had to manually add each file to the task runner, and you had to make sure that you were including the files in the correct order. This was a pain, and it was error-prone.

Browserify and RequireJS use a slightly different approach to fix this. These tools provide their own versions of the require function and the module.exports object. They recursively traverse your code to find all of the dependencies and assign them unique identifiers.

Then, they create a bundle by concatenating all of the files together and wrapping them in an IIFE to prevent the variables from leaking into the global scope. This allowed you to use modules in the browser before EcmaScript Modules were supported. While Browserify was built to use CommonJS modules in the browser, RequireJS used its own AMD syntax which allowed asynchronous loading of modules rather than loading everything at once.

Benefits of Bundling

Bundling modules means combining several files with modules into a single file. That is done for three reasons: Fewer files need to be retrieved in order to load all modules, Compressing the bundled file is slightly more efficient than compressing separate files, [and] during bundling, unused exports can be removed, potentially resulting in significant space savings. —Dr. Axel Rauschmayer, The future of bundling JavaScript modules

There can be more benefits to bundling than simply putting all of your JavaScript files together. Because you are already processing your code, you can use this compilation step to do other things. Our app uses TypeScript for example. All of the major bundlers either support TypeScript out of the box or have plugins that allow you to use TypeScript. So, you don’t have to worry about converting it to JavaScript before you bundle it.

This is called transpiling. This doesn’t have to be limited to TypeScript, however. You can also use features that haven’t been added to the language yet or write your code with a different syntax. For example, if you want to use JSX in your app, you can transpile it to JavaScript that browsers can understand while you are bundling it. Babel is the most popular tool for this. Like TypeScript, it is either built into or supported by all of the major bundlers.

Static analysis can also be used to optimize your code. If you are using a library like lodash, you might only be using a few of the functions in the library. A bundler can analyze your code and only include the functions that you are using. This is called tree shaking.

Minification is another common optimization that bundlers can do. This is the process of removing unnecessary characters from your code. Not just eliminating whitespace, comments, and semicolons, but also renaming variables to shorter names. Each character is a byte that the browser has to download, so this makes your code smaller and faster to download. Terser is one of the industry standards for this, and it is either built into or supported by all of the major bundlers.

If you’ve ever tried to look at minified code, you know that it can be hard to read. So, many bundlers also support source maps to allow you to debug your code in the browser. Here’s an example.

// index.js
function verboseThing(youNeedLotsOfContextToUnderstandThisParameter, wtfDoesThisMinifiedVariableEvenMean) {
  return youNeedLotsOfContextToUnderstandThisParameter + wtfDoesThisMinifiedVariableEvenMean
}
const cheese = "cheddar"
const crackers = "ritz"
console.log(verboseThing(cheese, crackers))
// index.min.js
function verboseThing(e,r){return e+r}const cheese="cheddar",crackers="ritz";console.log(verboseThing(cheese,"ritz"));
// index.min.js.map
{
"file": "index.min.js",
"sources": ["index.js"],
"names": ["verboseThing", "youNeedLotsOfContextToUnderstandThisParameter", "wtfDoesThisMinifiedVariableEvenMean", "cheese", "crackers", "console", "log"],
"mappings": "AAAA,IAAI,IAAI,IAAM,CAAC, etc" // I made these up
"sourcesContent": ["function verboseThing(youNeedLotsOfContextToUnderstandThisParameter, wtfDoesThisMinifiedVariableEvenMean) {\n  return youNeedLotsOfContextToUnderstandThisParameter + wtfDoesThisMinifiedVariableEvenMean\n}\nconst cheese = \"cheddar\"\nconst crackers = \"ritz\"\nconsole.log(verboseThing(cheese, crackers))"]
}

As you can see, the minified code is much harder to read than the original code. But, the source map allows the browser to link the minified code back to the original code. This allows you to debug your code in the browser as if it were the original code.

Now that we have an idea of what bundlers can do, let’s look at some of the most popular ones today. First, I’ll introduce you to each one, then we’ll take a look at what each configuration file looks like so we can compare and contrast them. Let’s get started.

Webpack

Webpack and Browserify are often seen today as solutions to the same problem, but Webpack’s initial focus was a bit different from Browserify’s. Whereas Browserify’s goal was to make Node modules run in the browser, Webpack’s goal was to create a dependency graph for all of the assets in a website - not just JavaScript, but also CSS, images, SVGs, and even HTML. -Nolan Lawson, A brief and incomplete history of JavaScript bundlers

It is impossible to talk about bundling without mentioning Webpack. Created in 2014, it is still the most popular bundler by far. It is also the most flexible. It has a loader system that allows you to easily bundle other files like images and CSS files, and it has a plugin system for basically anything else.

This flexibility comes at a cost, however. Webpack has a steep learning curve, and it can be hard to configure. It also has a reputation for being slow, but this has improved in recent years. In my tests, it was only slightly slower than the alternatives.

Because it is a product of the CommonJS era like Browserify and RequireJS, Webpack works similarly. It wraps each module in a function and it creates a dependency graph of these functions. Then, it creates a bundle by concatenating all of these functions together and wrapping them in an IIFE.

While Browserify only supported CommonJS modules and RequireJS used its own AMD syntax, Webpack supports both of these. It even allowed you to use EcmaScript Modules before they were supported in the browser. This, along with its healthy ecosystem, gave Webpack a huge advantage over the competition.

Rollup

Rollup was created for a different reason: to build flat distributables of JavaScript libraries as efficiently as possible, taking advantage of the ingenious design of ES2015 modules… ES2015 modules enable a different approach, which Rollup uses. All your code is put in the same place and evaluates in one go, resulting in leaner, simpler code that starts up faster. — Rich Harris, Webpack and Rollup: the same but different

As I mentioned, Webpack was created before ES Modules were widely supported. Instead, it used CommonJS modules and IIFE’s to create its dependency graph. This worked great for the most part, but it could lead to performance issues as the application grew.

Developed by Rich Harris in 2018, Rollup uses a different approach. Instead of concatenating IIFE’s together, Rollup uses the ES Module syntax. Because ES Modules are loaded asynchronously, this allows Rollup to create a more efficient dependency graph. It can do more advanced tree shaking because it can see the entire dependency graph at once— the static analysis that I mentioned earlier.

Rollup was originally targeted at library authors. But, now that every major browser supports ES Modules, it is a great option for bundling web applications as well. I found Webpack to be slightly more flexible, but Rollup was much easier to configure. It also has a reputation for being faster than Webpack. I found this to be true in my tests, but not as fast as our next bundler.

ESBuild

Our current build tools for the web are 10-100x slower than they could be. The main goal of the esbuild bundler project is to bring about a new era of build tool performance, and create an easy-to-use modern bundler along the way. —ESBuild

ESBuild is a relative newcomer to the bundling scene. Created by Evan Wallace in 2020, it is written in Go and is much faster than other bundlers. It is also much simpler to configure. It supports TypeScript and JSX out of the box, and it doesn’t require a configuration file at all. You can simply use the CLI. But, we’ll use the JavaScript API for the sake of consistency.

While ESBuild is extremely fast and simple, it is still a bit immature. It doesn’t support as many features as the other bundlers, and it doesn’t have as many plugins. But, the performance is undeniable.

Parcel

Parcel is a zero configuration build tool for the web. It combines a great out-of-the-box development experience with a scalable architecture that can take your project from just getting started to massive production application. —Parcel

Parcel is another relative newcomer to the bundling scene. Created by Devon Govett in 2017, it is the simplest bundler to configure by far. All you need to do is add a source field to your package.json file. Parcel will use this to find the entry point of your app.

Unlike the other bundlers, Parcel expects an html file as the entry point. It looks for any script and style tags in the html file, and then finds all of the images, styles, scripts and dependencies from there. It also supports TypeScript out of the box— as well as JSX, SASS, and many other file types.

Surprisingly, I could not find a reason to configure anything. But, if you want to, you can add a .parcelrc file to your project. Speaking of configuring things, let’s take a look at what each configuration file looks like for each bundler.

Configuring Bundlers

Your Webpack configuration is just a JavaScript file which exports an object or an array of object, and that object is your configuration. It’s gonna have some properties at the root level, and these properties describe how Webpack is supposed to bundle your code. —Sean Larkin, Changelog 233

These have a few commonalities. Although the naming differs slightly between them, let’s define some of the main options that we will be using in each configuration file. We’ll use the Webpack terms, but I’ll also include the Rollup, ESBuild, and Parcel terms in parentheses.

We have a few requirements for our app that I will be using to evaluate each bundler.

Finally, it’s a bit of a hassle to copy and paste the index.html and favicon.ico files into the dist folder every time we build. So, we’ll use a plugin to do that for us. Let’s take a look at how each bundler handles these requirements.

Bundler Config Files

webpack.config.js

import path from "path"
import HtmlWebpackPlugin from "html-webpack-plugin"
import { fileURLToPath } from "url"

const isProduction = process.env.NODE_ENV == "production"

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const srcDir = path.resolve(__dirname, "src")

const config = {
  entry: "./src/App.ts",
  output: {
    path: path.resolve(__dirname, "dist"),
  },
  devServer: {
    open: true,
    host: "localhost",
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      favicon: "./src/favicon.ico",
    }),
  ],
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/i,
        loader: "ts-loader",
        exclude: ["/node_modules/"],
      },
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
        include: srcDir,
      },
      {
        test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif|avif)$/i,
        type: "asset/resource",
        include: srcDir,
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".jsx", ".js", "..."],
  },
  optimization: {
    usedExports: true,
  },
}

export default () => {
  if (isProduction) {
    config.mode = "production"
  } else {
    config.mode = "development"
  }
  return config
}

rollup.config.js

import { defineConfig } from "rollup"
import typescript from "@rollup/plugin-typescript"
import css from "rollup-plugin-import-css"
import terser from "@rollup/plugin-terser"
import { nodeResolve } from "@rollup/plugin-node-resolve"
import image from "@rollup/plugin-image"
import html2 from "rollup-plugin-html2"

export default defineConfig({
  input: "src/App.ts",
  treeshake: true,
  output: {
    dir: "dist",
    format: "esm",
  },
  plugins: [
    typescript(),
    css(),
    terser(),
    nodeResolve(),
    image(),
    html2({
      template: "index.html",
      favicon: "src/favicon.ico",
      minify: true,
      entries: {
        App: {
          type: "module",
        },
      },
    }),
  ],
})

esbuild.mjs

import * as esbuild from "esbuild"
import { htmlPlugin } from "@craftamap/esbuild-plugin-html"

await esbuild.build({
  entryPoints: ["src/App.ts"],
  bundle: true,
  outdir: "dist",
  minify: true,
  loader: {
    ".jpg": "file",
  },
  treeShaking: true,
  metafile: true,
  plugins: [
    htmlPlugin({
      files: [
        {
          entryPoints: ["src/App.ts"],
          filename: "index.html",
          htmlTemplate: "index.html",
          favicon: "src/favicon.ico",
        },
      ],
    }),
  ],
})

package.json (just parcel)

{
  "name": "parcel-demo",
  "version": "1.0.0",
  "description": "This is way worse in the actual demo since we are using 4 different bundlers.",
  "source": "src/index.html",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server",
    "parcel": "parcel build",
    "dev": "parcel",
  },
  "type": "module",
  "author": "Jesse Pence",
  "license": "ISC",
  "dependencies": {
    "@fullcalendar/core": "^6.1.6",
    "@fullcalendar/daygrid": "^6.1.6",
    "@stripe/stripe-js": "^1.52.1",
    "cors": "^2.8.5",
    "date-fns": "^2.30.0",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "stripe": "^12.3.0"
  },
  "devDependencies": {
    "@types/cors": "^2.8.13",
    "@types/dom-navigation": "^1.0.0",
    "@types/dom-view-transitions": "^1.0.0",
    "@types/express": "^4.17.17",
    "parcel": "^2.8.3",
  }
}

package.json (all)

{
  "name": "my-webpack-project",
  "version": "1.0.0",
  "description": "My webpack project",
  "source": "src/index.html",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server",
    "webpack": "webpack --mode=production --node-env=production",
    "rollup": "rollup -c",
    "parcel": "parcel build",
    "esbuild": "node esbuild.mjs",
    "build:dev": "webpack --mode=development",
    "watch": "webpack --watch",
    "serve": "webpack serve"
  },
  "type": "module",
  "sideEffects": false,
  "author": "Jesse Pence",
  "license": "ISC",
  "dependencies": {
    "@fullcalendar/core": "^6.1.6",
    "@fullcalendar/daygrid": "^6.1.6",
    "@stripe/stripe-js": "^1.52.1",
    "cors": "^2.8.5",
    "date-fns": "^2.30.0",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "stripe": "^12.3.0"
  },
  "devDependencies": {
    "@craftamap/esbuild-plugin-html": "^0.5.0",
    "@rollup/plugin-image": "^3.0.2",
    "@rollup/plugin-node-resolve": "^15.0.2",
    "@rollup/plugin-terser": "^0.4.1",
    "@rollup/plugin-typescript": "^11.1.0",
    "@types/cors": "^2.8.13",
    "@types/dom-navigation": "^1.0.0",
    "@types/dom-view-transitions": "^1.0.0",
    "@types/express": "^4.17.17",
    "@webpack-cli/generators": "^3.0.2",
    "css-loader": "^6.7.3",
    "esbuild": "^0.17.18",
    "html-webpack-plugin": "^5.5.1",
    "parcel": "^2.8.3",
    "rollup": "^3.21.2",
    "rollup-plugin-favicons": "^0.5.0",
    "rollup-plugin-html2": "^4.0.0",
    "rollup-plugin-import-css": "^3.2.1",
    "style-loader": "^3.3.2",
    "ts-loader": "^9.4.2",
    "webpack": "^5.81.0",
    "webpack-cli": "^5.0.2"
  }
}

Comparing Bundlers

You’re trusting this bundler for your production. That’s why you’re using it in the first place, but it’s also become your development experience, too. You’re trusting it on both sides of that equation. —Chris Coyier, Shop Talk 452

I am really impressed by Parcel. There was not a single thing that I needed to configure. It just works. Webpack is the most complicated, but it’s also the most flexible. Rollup has my favorite plugin system, and it’s the easiest to configure. ESBuild is the fastest, but I legitimately could not get it to split my javascript into multiple files. I’m sure that I’m missing something, but I couldn’t find any documentation on how to do it.

So, ESBuild was the fastest bundler, but Rollup did a better job of modularizing our code with advanced tree shaking. It also has a great plugin system, and it was the easiest to configure outside of Parcel. It’s a bit of a pain to have to even think about how to configure a bundler. Wouldn’t it be great if we had a tool that combined the speed of ESBuild for quick development builds with the tree shaking of Rollup for production builds— all without having to configure anything? Well, that’s exactly what Vite does.

1-bundling

Vite

When we first started on the web - because you just load a script into an HTML file, you refresh the page, everything just reloads… You don’t have to wait for things to compile. So native ESM kind of gives you that really snappy thing; you just write native ES modules, the browser can handle it… It’s really fast, up to a certain point. —Evan You, Changelog: JSParty 212

Vite is a build tool created by Evan You in 2020. Vite doesn’t bundle your code for development. It uses ESBuild to pre-bundle dependencies, but the actual source code is served unbundled. This allows Vite to start the development server much faster than something like Webpack, and it uses Hot Module Replacement to quickly update the page as you make changes to your code.

When you are ready to deploy your app, Vite will use Rollup to bundle your code. This allows Vite to take advantage of the tree shaking that Rollup provides, so you get the best of both worlds. This works with no set-up, but you can also configure Vite with Rollup plugins if you want— most of them are compatible with the Vite plug-in system.

I wanted to highlight Vite because it is quickly growing to be a lynchpin of the web development community. Vite’s combination of flexibility and simplicity has led to a lot of recent innovation. Because it has built-in support for server-side rendering, it has become the build tool of choice for several meta-frameworks.

SvelteKit led the charge on this. In fact, the ESModule transform on the server that Vite leverages was conceptualized by Rich Harris himself. This very website is built with Astro, and it also uses Vite under the hood.

2-vite

Import Maps

The main benefit of this system is that it allows easy coordination across the ecosystem. Anyone can write a module and include an import statement using a package’s well-known name, and let the Node.js runtime or their build-time tooling take care of translating it into an actual file on disk (including figuring out versioning considerations). —WICG Proposal for Import Maps

While Vite and Parcel eliminate the vast majority of the hassle of bundling, they still require a build step. While most developers have simply accepted this as part of modern web development, there are ways to get much of the benefit of bundling without chaining yourself to complex build tools. Import maps have recently been added to all major browsers. They allow you to centrally declare all of your app’s dependencies so that you can easily use them in your code.

While you can simply import your dependencies directly in each file, this can be repetitive and difficult to manage. For example, if you want to use an external library like date-fns, you have to copy and paste the same URL over and over. To avoid bugs, you have to make sure that it is the same version in every file— imported from the same location. This can make it hard to manage your dependencies.

With import maps, you can centrally declare all of your dependencies in one place. Using JSON syntax, you can declare the name of the dependency and the location of the file. While you cannot do an external json file for now, you can use a script tag to declare your import map. I’m using a little workaround to import a script that appends an import map to the document. Here’s what our app’s import map looks like.

importmap.js

const im = document.createElement("script")
im.type = "importmap"
im.textContent = JSON.stringify({
  "imports": {
    "stripe-js":
      "https://unpkg.com/@stripe/stripe-js@1.52.1/dist/stripe.esm.js",
    "date-fns": "https://unpkg.com/date-fns@2.27.0/esm/index.js",
    "@fullcalendar/core": "https://cdn.skypack.dev/@fullcalendar/core@6.1.6",
    "@fullcalendar/daygrid":
      "https://cdn.skypack.dev/@fullcalendar/daygrid@6.1.6",
    "Product": "/components/Product.js",
    "render": "/components/render.js",
    "Router": "/components/Router.js",
    "Routes": "/components/Routes.js",
    "store": "/components/store.js",
    "cart": "/features/cart.js",
    "calendar": "/features/calendar.js",
    "search": "/features/search.js",
    "theme": "/features/theme.js",
    "hamburger": "/features/hamburger.js",
    "About": "/pages/About.js",
    "Cart": "/pages/Cart.js",
    "Checkout": "/pages/Checkout.js",
    "Home": "/pages/Home.js",
    "Nope": "/pages/Nope.js",
    "ProductPage": "/pages/ProductPage.js",
    "Products": "/pages/Products.js",
    "Types": "/Types.js",
  },
})

document?.currentScript?.after(im)

This allows us to import our dependencies like this despite not having a bundler.

calendar.js

// without an import map (i'm pretending that we have date-fns installed locally)
import { differenceInDays, formatISO, add } from "../node_modules/date-fns/esm/index.js"
import { Calendar } from "https://cdn.skypack.dev/@fullcalendar/core"
import dayGridPlugin from "https://cdn.skypack.dev/@fullcalendar/daygrid"
// with an import map
import { differenceInDays, formatISO, add } from "date-fns"
import { Calendar } from "@fullcalendar/core"
import dayGridPlugin from "@fullcalendar/daygrid"

Unfortunately, since we are checking our JSDoc annotations with TypeScript, we will also have to help the TypeScript compiler understand our import map. We can do this by adding a paths field to our tsconfig.json file. Here’s what that looks like.

tsconfig.json

{
  "compilerOptions": {
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "moduleResolution": "nodenext",
    "module": "esnext",
    "resolveJsonModule": true,
    "allowJs": true,
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "paths": {
      "stripe-js": [
        "https://unpkg.com/@stripe/stripe-js@1.52.1/dist/stripe.esm.js"
      ],
      "date-fns": ["https://unpkg.com/date-fns@2.27.0/esm/index.js"],
      "@fullcalendar/core": [
        "https://cdn.skypack.dev/@fullcalendar/core@6.1.6"
      ],
      "@fullcalendar/daygrid": [
        "https://cdn.skypack.dev/@fullcalendar/daygrid@6.1.6"
      ],
      "Product": ["./public/components/Product.js"],
      "render": ["./public/components/render.js"],
      "Router": ["./public/components/Router.js"],
      "Routes": ["./public/components/Routes.js"],
      "store": ["./public/components/store.js"],
      "cart": ["./public/features/cart.js"],
      "hamburger": ["./public/features/hamburger.js"],
      "search": ["./public/features/search.js"],
      "theme": ["./public/features/theme.js"],
      "calendar": ["./public/features/calendar.js"],
      "About": ["./public/pages/About.js"],
      "Cart": ["./public/pages/Cart.js"],
      "Checkout": ["./public/pages/Checkout.js"],
      "Home": ["./public/pages/Home.js"],
      "Nope": ["./public/pages/Nope.js"],
      "ProductPage": ["./public/pages/ProductPage.js"],
      "Products": ["./public/pages/Products.js"],
      "Types": ["./public/Types.js"]
    }
  },
  "include": ["public/**/*.js", "public/index.d.ts"],
  "exclude": ["node_modules"]
}

This… almost works! Unfortunately, because the external libraries are not actually installed in our project, the TypeScript compiler will still complain about them. And, for good reason— we don’t have any type definitions for them!

We could go ahead and install the packages locally, but that seems like a shame when we are already using the import map. It would be nice to only download the types, but the @types/date-fns library is deprecated. I would love to hear some of your workarounds in the comments. But, I’m going to take a bit of a dramatic step instead. Join me in the next chapter to find out what it is.

3-import-maps

Conclusion

Our general recommendation is to continue using bundlers before deploying modules to production. In a way, bundling is a similar optimization to minifying your code: it results in a performance benefit, because you end up shipping less code. Bundling has the same effect! Keep bundling. —V8 blog, JavaScript Modules

So, I hope the benefits of bundling are clear to you now. It can be a great way to optimize your code and make your app faster. But, it can also be a pain to configure. While Vite and Parcel solve much of this, there is a part of me that wishes things could be even simpler.

Part of what makes the web so beautiful is how easy it is to get started. You can just write some code, open it in the browser, and it works. It’s a shame to pollute that simplicity with complex build tools just to make our apps performant. It feels like there has to be a better way.

While I have been making a great effort to not use a UI framework so far, it is hard to see the full benefits of a build step without using one. In fact, our unbundled app is only marginally larger than the bundled and minified versions. But, this is not the reality of most web apps today. Most use something like React. That, along with Babel or other additions to the transform pipeline, can complicate this process significantly.

And, some frameworks like Svelte and Astro have fully embraced the idea of compilation. They use proprietary files that allow them to do some really cool things, but they are useless without the compiler. It’s an ongoing question if this is a good thing or not. While it allows for some incredible optimizations, it also introduces a lot of complexity and domain-specific knowledge.

Anyways, I don’t want to add a framework just yet. I want to explore each of them in their own series of articles. But, I want to spend one more chapter exploring some exciting new tools that have been made possible by the advent of EcmaScript Modules. I hope you’ll join me to find out what they are.

Additional Resources

Table of Contents Comments View Source Code Next Page!