Replacing Webpack with Snowpack in a Phoenix Application
Tags: elixir, phoenix, webpack, snowpack
Published: 2021-03-17 15:58:15

Snowpack 3 was [recently released](https://www.snowpack.dev/posts/2021-01-13-snowpack-3-0) and now comes with [esbuild](https://esbuild.github.io) support built-in which should mean much faster builds with fewer dependencies, so I replaced Webpack in a fresh phoenix app to see the difference.

tldr; It is incredibly fast and lightweight. See [this commit](https://github.com/moomerman/snowball/commit/84eece762b50ef92e0c1c73986d85e48917a75eb) for all the changes required to get this working.

I created a new Phoenix application with LiveView to test this out.  LiveView so all the asset config is already set up and we can switch out Webpack for Snowpack.

Firstly I removed all `node_modules`, `webpack.config.js`, `yarn.lock`, `.babelrc` and all dependencies in `package.json` except `phoenix_html` and `nprogress` which we'll keep.

Then I ran `yarn add --dev snowpack` to add Snowpack, the final `package.json` looks like this.

```javascript
// /assets/package.json

{
  "repository": {},
  "description": " ",
  "license": "MIT",
  "scripts": {
    "deploy": "snowpack build",
    "watch": "snowpack build --no-bundle --watch"
  },
  "dependencies": {
    "phoenix_html": "file:../deps/phoenix_html"
  },
  "devDependencies": {
    "nprogress": "^0.2.0",
    "snowpack": "^3.0.11"
  }
}
```

I also added the equivalent `deploy` and `watch` commands to replace the Webpack ones.

Next was to create a config file for Snowpack that matches the default asset file structure for Phoenix, and configures `esbuild` to optimize the assets for production.

```javascript
// /assets/snowpack.config.js

module.exports = {
  mount: {
    "js": { url: "/js" },
    "css": { url: "/css" },
    "static": { url: "/", static: true, resolve: false }
  },
  buildOptions: {
    out: "../priv/static/"
  },
  optimize: {
    entrypoints: ["./static/index.html"],
    bundle: true,
    minify: true,
    target: 'es2018'
  },
};
```

Only odd thing here is the fake HTML entrypoint I had to set up which references the `app.js` and `app.css`.  I found without this the optimizer didn't run for `app.css`, maybe there is a better way of doing this.

Snowpack creates a `_snowpack` folder in the output directory so we need to add this to the Static Plug in `/lib/snowball_web/endpoint.ex`.

Snowpack creates ES modules for your JS assets so we need to update the `script` tag for `app.js` from `type="text/javascipt"` to `type="module"`.

Then we need to update the asset watcher that Phoenix starts up to run Snowpack, which now looks like this.

```elixir
# /config/dev.exs

watchers: [
  node: [
    "node_modules/snowpack/index.bin.js",
    "build",
    "--watch",
    cd: Path.expand("../assets", __DIR__)
  ]
]
```

The final hurdle was getting the `phoenix` and `phoenix_live_view` assets to load.  Snowpack didn't like the default import that Phoenix generates so I ended up linking to the Snowpack `skypack` CDN to get ES module versions of those dependencies and that works great.

The result is incredibly fast start and rebuild times for assets in development and in production.

``` bash
# development

$ snowpack build --no-bundle
[snowpack] ! building source files...
[snowpack] ✔ build complete [0.01s]
[snowpack] ! building dependencies...
[snowpack] ✔ dependencies ready! [0.09s]
[snowpack] ! verifying build...
[snowpack] ✔ verification complete [0.00s]
[snowpack] ! writing build to disk...
[snowpack] ! optimizing build...
[snowpack] ✔ optimize complete [0.09s]
[snowpack] ▶ Build Complete!

✨  Done in 0.68s.
```

```bash
# production (on Heroku)

$ snowpack build
[snowpack] ! building source files...
[snowpack] ✔ build complete [0.03s]
[snowpack] ! building dependencies...
[snowpack] ✔ dependencies ready! [0.29s]
[snowpack] ! verifying build...
[snowpack] ✔ verification complete [0.00s]
[snowpack] ! writing build to disk...
[snowpack] ! optimizing build...
[snowpack] ✔ optimize complete [0.05s]
[snowpack] ▶ Build Complete!

Done in 0.84s.
```

That is a vast improvement over the equivalent Webpack setup and results in 17MB of node_modules vs 225MB before the change.  

There are caveats to using the built-in `esbuild` as it says it isn't quite ready for production use yet, but you can add other tried-and-tested plugins to Snowpack if you prefer.

Overall the future is looking good, and I'll be using this from now on.

If you have any comments please add them to [this thread on Twitter](https://twitter.com/moomerman/status/1350855425810436097).