Files
MinglarBackendNestJS/LAMBDA_OPTIMIZATION_GUIDE.md

491 lines
13 KiB
Markdown
Raw Normal View History

# AWS Lambda Bundle Size Optimization Guide
## Overview
This guide documents how to optimize AWS Lambda function bundle sizes when using:
- **Serverless Framework v4** (with built-in esbuild)
- **Prisma ORM** (with driver adapters)
- **NestJS** or any Node.js framework
## Problem
Lambda functions can become bloated (25+ MB) due to:
1. **Prisma binary engines** (~50MB uncompressed)
2. **AWS SDK v3** being bundled (~5-10MB)
3. **Dependencies copied to node_modules** instead of being bundled
## Solution Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Lambda Function │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Your Code (bundled by esbuild) ~50-500 KB │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Prisma Layer (shared) ~15 MB │ │
│ │ - @prisma/client │ │
│ │ - @prisma/adapter-pg │ │
│ │ - .prisma/client (generated) │ │
│ │ - pg driver │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ AWS Lambda Runtime │ │
│ │ - AWS SDK v3 (built-in for Node.js 18+) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## Step-by-Step Setup
### 1. Project Structure
```
your-project/
├── serverless.yml
├── package.json
├── prisma/
│ └── schema.prisma
├── layers/
│ └── prisma/
│ └── nodejs/
│ └── package.json
├── src/
│ └── ... your code
└── build-prisma-layer.ps1 (or .sh for Linux/Mac)
```
### 2. Prisma Schema Configuration
**`prisma/schema.prisma`**
```prisma
generator client {
provider = "prisma-client-js"
// For Prisma 7+ with driver adapters, no binary targets needed
// The WASM-based query engine is used automatically
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
```
> **Note**: Prisma 7+ uses WASM-based query compiler instead of binary engines when using driver adapters, which is much smaller.
### 3. Layer Package.json
**`layers/prisma/nodejs/package.json`**
```json
{
"name": "prisma-layer",
"version": "1.0.0",
"description": "Lambda layer for Prisma with pg driver adapter",
"dependencies": {
"@prisma/client": "^7.0.0",
"@prisma/adapter-pg": "^7.0.0",
"pg": "^8.13.0"
}
}
```
### 4. Serverless Configuration
**`serverless.yml`**
```yaml
service: your-service-name
provider:
name: aws
runtime: nodejs22.x
region: your-region
memorySize: 512
# Apply Prisma layer to ALL functions
layers:
- !Ref PrismaLambdaLayer
environment:
DATABASE_URL: ${env:DATABASE_URL}
# ... other env vars
# esbuild configuration (Serverless v4 built-in)
build:
esbuild:
bundle: true
minify: true
sourcemap: false
target: node20
platform: node
# Mark packages as external (not bundled into JS)
external:
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma/client'
- '.prisma'
- 'pg'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
# Exclude from npm install in zip (CRITICAL!)
exclude:
- 'aws-sdk'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma'
- '.prisma/client'
- 'pg'
- 'pg-*'
- 'postgres-*'
- 'pgpass'
- 'split2'
- 'xtend'
# Define the Prisma layer
layers:
prisma:
path: layers/prisma
name: ${self:service}-prisma-layer-${sls:stage}
description: Prisma client with pg driver adapter
compatibleRuntimes:
- nodejs22.x
retain: false
# Package configuration
package:
individually: true
patterns:
- '!node_modules/**'
- '!node_modules/@prisma/**'
- '!node_modules/.prisma/**'
- '!**/*.test.js'
- '!**/*.spec.js'
- '!**/test/**'
- '!**/__tests__/**'
- '!package-lock.json'
- '!yarn.lock'
- '!README.md'
- '!.git/**'
functions:
myFunction:
handler: src/handlers/myHandler.handler
events:
- httpApi:
path: /my-endpoint
method: get
plugins:
- serverless-offline
```
### 5. Build Script for Prisma Layer
**Windows (PowerShell) - `build-prisma-layer.ps1`**
```powershell
# Build Prisma Lambda Layer
$layerPath = "layers\prisma\nodejs"
Write-Host "Building Prisma Lambda Layer..." -ForegroundColor Cyan
# 1. Clean existing node_modules in layer
Write-Host "Cleaning layer node_modules..."
if (Test-Path "$layerPath\node_modules") {
Remove-Item -Recurse -Force "$layerPath\node_modules"
}
# 2. Install dependencies in layer
Write-Host "Installing layer dependencies..."
Push-Location $layerPath
npm install --omit=dev
Pop-Location
# 3. Generate Prisma client
Write-Host "Generating Prisma client..."
npx prisma generate
# 4. Copy .prisma/client to layer
Write-Host "Copying generated Prisma client to layer..."
$sourcePrisma = "node_modules\.prisma"
$destPrisma = "$layerPath\node_modules\.prisma"
if (Test-Path $sourcePrisma) {
if (Test-Path $destPrisma) {
Remove-Item -Recurse -Force $destPrisma
}
Copy-Item -Recurse $sourcePrisma $destPrisma
Write-Host "Copied .prisma/client successfully!" -ForegroundColor Green
} else {
Write-Host "ERROR: .prisma folder not found. Run 'npx prisma generate' first." -ForegroundColor Red
exit 1
}
# 5. Show layer size
$layerSize = (Get-ChildItem "$layerPath\node_modules" -Recurse | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Host "`nTotal layer size: $([math]::Round($layerSize, 2)) MB" -ForegroundColor Yellow
Write-Host "Prisma layer built successfully!" -ForegroundColor Green
```
**Linux/Mac (Bash) - `build-prisma-layer.sh`**
```bash
#!/bin/bash
set -e
LAYER_PATH="layers/prisma/nodejs"
echo "Building Prisma Lambda Layer..."
# 1. Clean existing node_modules in layer
echo "Cleaning layer node_modules..."
rm -rf "$LAYER_PATH/node_modules"
# 2. Install dependencies in layer
echo "Installing layer dependencies..."
cd "$LAYER_PATH"
npm install --omit=dev
cd -
# 3. Generate Prisma client
echo "Generating Prisma client..."
npx prisma generate
# 4. Copy .prisma/client to layer
echo "Copying generated Prisma client to layer..."
if [ -d "node_modules/.prisma" ]; then
rm -rf "$LAYER_PATH/node_modules/.prisma"
cp -r "node_modules/.prisma" "$LAYER_PATH/node_modules/.prisma"
echo "Copied .prisma/client successfully!"
else
echo "ERROR: .prisma folder not found. Run 'npx prisma generate' first."
exit 1
fi
# 5. Show layer size
LAYER_SIZE=$(du -sm "$LAYER_PATH/node_modules" | cut -f1)
echo "Total layer size: ${LAYER_SIZE} MB"
echo "Prisma layer built successfully!"
```
### 6. Prisma Client Usage
**`src/common/database/prisma.client.ts`**
```typescript
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { Pool } from 'pg';
// Connection pool for serverless
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 5, // Limit connections in Lambda
});
const adapter = new PrismaPg(pool);
// Single instance for Lambda warm starts
let prisma: PrismaClient;
export function getPrismaClient(): PrismaClient {
if (!prisma) {
prisma = new PrismaClient({
adapter,
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
}
return prisma;
}
export { prisma };
```
---
## Deployment Workflow
```bash
# 1. Build the Prisma layer (run after schema changes)
./build-prisma-layer.ps1 # Windows
# or
./build-prisma-layer.sh # Linux/Mac
# 2. Deploy
npx serverless deploy --stage=dev
# 3. Deploy single function (faster, uses existing layer)
npx serverless deploy function -f myFunction --stage=dev
```
---
## Key Configuration Explained
### esbuild `external` vs `exclude`
| Property | Purpose |
|----------|---------|
| `external` | Tells esbuild NOT to bundle these into the JS file. They become `require()` calls. |
| `exclude` | Tells Serverless NOT to `npm install` these packages into the function zip. |
**Both are required!**
- `external` alone = esbuild doesn't bundle, but Serverless still installs to node_modules
- `exclude` alone = Serverless doesn't install, but esbuild bundles the code
### Layer Reference
```yaml
# This creates a CloudFormation reference to the layer defined in the same stack
layers:
- !Ref PrismaLambdaLayer
```
The `PrismaLambdaLayer` name comes from the layer key (`prisma`) converted to PascalCase + `LambdaLayer`.
### Why Exclude pg-* packages?
When `pg` is external, its dependencies still get installed:
- `pg-connection-string`
- `pg-pool`
- `pg-protocol`
- `pg-types`
- etc.
These must all be in the `exclude` list to prevent duplication.
---
## Expected Results
| Function Type | Before | After |
|---------------|--------|-------|
| Simple handlers | 25+ MB | **50-100 kB** |
| With validation (zod/yup) | 25+ MB | **300-500 kB** |
| With S3 uploads | 30+ MB | **1-2 MB** |
---
## Troubleshooting
### 1. "Cannot find module '@prisma/client'"
**Cause**: Layer doesn't have the generated `.prisma/client`
**Fix**: Run `build-prisma-layer.ps1` to regenerate the layer
### 2. Function size still large
**Debug**: Extract and inspect the zip:
```powershell
Expand-Archive ".serverless\build\your-function.zip" -DestinationPath "extracted"
Get-ChildItem "extracted\node_modules" -Directory
```
If you see `@prisma` or `pg` folders, the `exclude` config isn't working.
### 3. "Cannot resolve CloudFormation reference"
**Cause**: Using `${cf:...}` reference before first deploy
**Fix**: Use `!Ref PrismaLambdaLayer` instead (works on first deploy)
### 4. Cold starts still slow
Consider:
- **Provisioned Concurrency**: Pre-warm instances
- **Reduce memory**: Sometimes lower memory = same speed, lower cost
- **Connection pooling**: Use tools like PgBouncer for RDS
---
## Additional Optimizations
### 1. Remove duplicate validation libraries
Pick ONE of: `zod`, `yup`, or `class-validator`. Don't use all three.
### 2. Tree-shake NestJS
If not using full NestJS, import only what you need:
```typescript
// Instead of
import { Controller, Get } from '@nestjs/common';
// For Lambda handlers, you might not need NestJS at all
```
### 3. Use AWS SDK v3 selectively
```yaml
external:
- '@aws-sdk/*' # Exclude all
```
Then in code:
```typescript
// AWS SDK v3 is available in Lambda runtime (Node.js 18+)
import { S3Client } from '@aws-sdk/client-s3';
```
---
## Quick Reference
```yaml
# Minimal serverless.yml for Prisma + Lambda optimization
build:
esbuild:
bundle: true
minify: true
external:
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma/client'
- '.prisma'
- 'pg'
- '@aws-sdk/*'
exclude:
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma'
- '.prisma/client'
- 'pg'
- 'pg-*'
- 'postgres-*'
- 'pgpass'
- 'split2'
- 'xtend'
- '@aws-sdk/*'
layers:
prisma:
path: layers/prisma
name: ${self:service}-prisma-${sls:stage}
compatibleRuntimes:
- nodejs22.x
provider:
layers:
- !Ref PrismaLambdaLayer
```
---
## Version Compatibility
| Tool | Tested Version |
|------|----------------|
| Serverless Framework | v4.x |
| Prisma | v7.x |
| Node.js | 20.x, 22.x |
| AWS Lambda Runtime | nodejs20.x, nodejs22.x |
---
*Last updated: December 2025*