Serverless - Bundle Lambda Functions with Webpack Tree Shaking

AuthorPrashanth HN

November 08, 2020

serverlesslambdanodejs

Serverless - Bundle Lambda Functions with Webpack Tree Shaking

Serverless framework is the popular way of deploying nodejs applications into Lambda functions. In this article, we will look into how the Serverless framework bundles the code & why it is not the best option for large applications. And then, we will look into how we can override bundling done by Serverless framework & implement our way of bundling the application more optimally. We will be using Webpack to achieve the same!

How Serverless does it?

Let’s create a sample nodejs project & understand how the Serverless framework bundles the code by default.

sls create --template aws-nodejs --path tree-shake-example

cd tree-shake-example

The first command generates a sample Serverless project using the nodejs template. You will see a hello world function in handler.js and that being referenced in serverless.yml

functions:
  hello:
    handler: handler.hello

Let’s make a copy of handler.js, call it hi.js & add it in serverless.yml

functions:
  hello:
    handler: handler.hello
  hi:
    handler: hi.hello

What we have here is two identical functions. Let’s deploy them and see what happens:

sls deploy

We can open the AWS console and see the size of functions:

First deployment on AWS console

As you can see, both Lambda functions have the same size, which was expected since they were identical. But, there’s more to look when you get inside the function:

Function Directory Content

As you see above, both handler.js & hi.js is bundled into the Lambda function. Ideally, we should have had only handler.js in the handler function and hi.js in the hi function. While this is ok for small applications, as your application grows, you will be sacrificing performance and also start approaching Lambda limits.

Assume that you added a library which you use in only one lambda function, but still, it is bundled in all the lambda function unnecessarily. This will increase the bundle size and can impact cold-start times of the Lambda function. To demonstrate this, I installed axios to the project & imported it into the “hi” function.

npm i --save axios

Add import statement into hi.js

import axios from "axios"

Our function size has gone up from 746bytes to 135.5kb & it has increased for both functions even though we are using axios in only one. No changes were made in the handler.js.

Function size after adding axios

Bundling using Webpack tree-shaking

Tree-shaking is a dead code elimination technique; you can read more about that on Wikipedia. We can achieve this in our project using Webpack. Let’s add webpack, webpack-cli & zip-webpack-plugin (needed to zip the bundle) as dev dependencies.

npm i --save-dev webpack webpack-cli zip-webpack-plugin

We need to tell webpack how we want to bundle our application. To do that, we will have to create webpack.config.js

const ZipPlugin = require("zip-webpack-plugin")
const path = require("path")

const config = {
  //what are the entry points to our functions
  entry: {
    handler: "./handler.js",
    hi: "./hi.js",
  },
  //how we want the output
  output: {
    filename: "[name]/index.js",
    path: path.resolve(__dirname, "dist/"),
    libraryTarget: "umd",
  },
  target: "node",
  mode: "production",
  optimization: { minimize: false },
}
//finally zip the output directory, ready to deploy
const pluginConfig = {
  plugins: Object.keys(config.entry).map(entryName => {
    return new ZipPlugin({
      path: path.resolve(__dirname, "dist/"),
      filename: entryName,
      extension: "zip",
      include: [entryName],
    })
  }),
}

const webpackConfig = Object.assign(config, pluginConfig)
module.exports = webpackConfig
  • We instruct webpack the entry point for each of our Lambda functions; webpack can start here and build a dependency tree based on import statements. This is why we should always do named imports.
  • For each entry point, webpack will generate a single index.js file containing all the dependencies that function needs. Webpack places this file in functionName/index.js under dist directory as specified in config. So, we will have dist/handler/index.js & dist/hi/index.js
  • Then, using zip-webpack-plugin, we zip these individual directories into zip files. The result would be dist/handler.zip & dist/hi.zip

Now that we have done all the configs, we can give it a shot by running webpack. To make it easier, we can add that as a script inside our package.json

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "bundle": "./node_modules/.bin/webpack"
  }

With that in place, we can run:

npm run bundle

The above script should create a new directory dist and create our bundles & create zip files as expected. I went ahead and checked the file size of both the index.js files:

Files after tree-shaking

The bundle of the handler function is at 1kb & hi function, which had axios import, is at 35kb. That confirms our tree-shaking technique is working!

Overriding Serverless framework’s Bundling

Now that we took the job of bundling into our own hands, we need to tell Serverless not to bundle & use the bundle we generated by passing artifact attribute

functions:
  handler:
    handler: handler/index.hello
    package:
      artifact: "./dist/handler.zip"
  hi:
    handler: hi/index.hello
    package:
      artifact: "./dist/hi.zip"

We have done everything needed & good to go with deployment!

sls deploy

Time to load AWS console and see what sizes we get there

Files after tree-shaking

And it looks like we have achieved what we wanted! But if you recollect, the handler function in the first deployment was 746bytes & we haven’t changed it at all! But now it is at 1.5kb! Indeed we are missing something here! Change the minimize flag under optimization in webpack config to true

optimization: {
  minimize: true
}

This enables the minification of our bundle and reduces the size drastically.

Files after tree-shaking

With that, we are done! Happy Tree Shaking!!