Azure Functions + esbuild = ๐Ÿ˜๐Ÿคฏ

Azure Functions + esbuild = ๐Ÿ˜๐Ÿคฏ

Reducing bundle size and build time with an awesome new tool.

ยท

3 min read

I first discovered esbuild as a builder for Angular in this Reddit post. I was amazed! Seeing this, I thought: "There must be other ways to utilize this!"

What is esbuild?

esbuild calls itself "An extremely fast JavaScript bundler". I can definitely confirm that! And the main reason for that is, that it's written in Go - unlike our ol' reliable tsc. Also, it's plugin API makes it extremely flexible. Did I mention that it also has native TypeScript support?

What advantages does this bring?

  • Reduced bundle size: esbuild has two features that enable this - bundling and code-splitting. These features allow us to compile all external dependencies (from our node_modules folder) into the final bundle. Because esbuild can also minify the bundle, this decreases bundle size drastically
  • Reduced build time: Like I mentioned before - esbuild is extremely fast. Combined with a watch mode this makes for crazy fast development speed. Build time for my Function App with 28 Functions: 800ms ๐Ÿคฏ Now, this is only half the truth. esbuild doesn't do type checking so we have to run tsc --noEmit at least in our CI pipeline (which isn't quite as fast unfortunately).

How do I build Azure Functions with it?

Great question! Here's how you can do it:

First, install esbuild using npm: npm i -D esbuild

Then, create a build script in the root of the project:

// build.mjs -> .mjs so we can use TLA (Top-level-await)

import { build } from 'esbuild';

await build({
  entryPoints: ['my-func/index.ts'],
  format: 'esm',
  splitting: true,
  minify: true,
  bundle: true,
  platform: 'node',
  target: 'node12'
  sourcemap: false,
  watch: false,
  outdir: 'dist',
  outExtension: { '.js' : '.mjs' }
});

Now, simply adjust the scriptFile property in every function.json to account for the new file extension and you're good to go.

Or are you? ๐Ÿง While writing this article, I fell into some traps and rabbitholes. Like for example:

  • __dirname and __filename not being available in ESM
  • esbuild not being able to convert CJS requires to ESM imports
  • The output directory not being correct when there is only one Azure Function

What now?

These pitfalls and rabbitholes caused me to develop a tool that catches all of those and allows for easy extension of the esbuild config that I think is a good default. You can also either use it from the CLI or via code (which I would recommend).

Let's have a look:

First, install it in your project using npm: npm i -D esbuild-azure-functions.

Then, create a build script in the root of the project:

// build.mjs -> .mjs so we can use TLA (Top-level-await)

import { build, BuilderConfigType } from 'esbuild-azure-functions';

const config: BuilderConfigType = {
  project: process.cwd(),
  esbuildOptions: {
    outdir: 'myoutdir'
  }
};

await build(config);

And that's the basic config. By default, the tool looks for every index.ts file in the project directory. For a more detailed documentation, checkout the GitHub repo below!

Benchmark

To wrap it up, here are some of the number I've observed

Bundle size

Bundle size decreased massively. Most of that probably comes from the fact that we don't just throw the node_modules folder into the app package.

Now, I've included zip file sizes for both Linux and Windows. I run my Node.js Azure Functions on Windows and Windows produces a zip file that's quite a lot smaller than one created on Linux.

Bundle size chart

Build time

As mentioned above, build time is very fast.

az-func-esbuild_speed-chart.png

I hope you liked this blog post! If you have any questions, feel free to hit me up!

Cheers!

Wanna buy me a coffee?

ย