Lambda functions using Node.js often balloon in size, making cold starts slow and deployments painful.
Let’s look at a simple Lambda function and how esbuild can shrink it.
index.js:
import express from 'express';
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
If we were to just npm install express and zip this up, the package size would be significant, mostly due to express and its dependencies.
Now, let’s see how esbuild can help us bundle this more efficiently.
First, install esbuild as a dev dependency:
npm install --save-dev esbuild
Then, create a build script in your package.json:
{
"scripts": {
"build": "esbuild index.js --bundle --outfile=bundle.js --platform=node --format=cjs --external:express"
}
}
Let’s break down these esbuild options:
index.js: This is our entry point.--bundle: This is the magic.esbuildwill analyze your code and include all necessary dependencies.--outfile=bundle.js: The output file name.--platform=node: Tellsesbuildto target a Node.js environment. This ensures it uses Node.js-specific APIs and doesn’t try to bundle browser-specific polyfills.--format=cjs: Generates CommonJS output, which is what Node.js typically uses.--external:express: This is crucial for minimizing package size. It tellsesbuildnot to bundle theexpresspackage. Instead, it assumesexpresswill be available in the Lambda runtime environment. Node.js runtimes in Lambda do includeexpressby default (or you can easily include it in yournode_modulesif you need a specific version).
Run the build script:
npm run build
This will create a bundle.js file. If you inspect this file, you’ll notice it’s much smaller than the original node_modules directory would have been. The bundle.js contains your application code and any other dependencies that were not marked as external.
To deploy this to Lambda, you’d typically zip bundle.js and upload it. For a Lambda function that uses express, you’d usually configure the handler to point to the bundle.js file, and the runtime would handle loading express if it’s a managed dependency or if you’ve explicitly included it.
The mental model here is that esbuild acts as a highly optimized bundler. It understands JavaScript syntax (including ES Modules) and efficiently traverses your dependency graph. By marking large, common dependencies as external, you leverage the pre-existing runtime environment and drastically reduce the size of your deployment package. This means faster uploads, quicker unzipping by Lambda, and ultimately, faster cold starts.
A common pitfall is forgetting to mark all large, common dependencies as external. If you have multiple libraries that are already present in the Lambda Node.js runtime (like aws-sdk, crypto, etc.), and you don’t explicitly mark them as external, esbuild might bundle them unnecessarily, increasing your package size. You’ll want to check the specific Node.js runtime version you’re using in Lambda to see what’s pre-installed.
The next step is often optimizing your Lambda configuration for performance, such as adjusting memory allocation or provisioned concurrency.