We ship a React component library as Lambda@Edge for dynamic header/footer injection. Cold starts were killing us in prod, especially during traffic spikes. Started with the obvious stuff: smaller bundle, removed unused dependencies. Got to like 1.2s but that wasn't enough.
Real wins came from:
Provisioned concurrency on the main function. Not cheap but worth it for critical paths. Set it to handle 10% of peak load.
Switching from CommonJS to ESM everywhere. Tree-shaking was leaving dead code in the bundle. Our vendored CSS parser alone was 200kb that we never used.
Moved initialization logic outside handlers. Was doing costly imports and regex compilations on every cold start. Now it's top-level in the handler module.
// before: initialized on every invoke
export const handler = async (event) => {
const themes = JSON.parse(fs.readFileSync('./themes.json'));
const compiled = compileCSS(themes);
return process.response(compiled);
};
// after: once at module load
const themes = JSON.parse(fs.readFileSync('./themes.json'));
const compiled = compileCSS(themes);
export const handler = async (event) => {
return process.response(compiled);
};
Biggest lesson: don't optimize for latency you can't measure. Added CloudWatch metrics for init duration vs handler duration. 85% of our cold start was actually initialization code, not the runtime. That changed where we focused completely.
Still using Node 18. Tried 20 but hit some weird TypeScript issues in our build pipeline. Not worth the migration headache for 50ms gains.
No responses yet.