Exploring the Future of Serverless with Sheen Brisals: Why It's Becoming the New Norm
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:
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:
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
.
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 infunctionName/index.js
underdist
directory as specified in config. So, we will havedist/handler/index.js
&dist/hi/index.js
- Then, using
zip-webpack-plugin
, we zip these individual directories into zip files. The result would bedist/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:
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
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.
With that, we are done! Happy Tree Shaking!!