<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hugh</title>
    <description>The latest articles on DEV Community by Hugh (@hughp135).</description>
    <link>https://dev.to/hughp135</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1799294%2F3cc018b8-b78a-4ba8-8e2e-6f5876ea4f60.png</url>
      <title>DEV Community: Hugh</title>
      <link>https://dev.to/hughp135</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hughp135"/>
    <language>en</language>
    <item>
      <title>Optimising package size for Typescript AWS Lambda functions using serverless-esbuild</title>
      <dc:creator>Hugh</dc:creator>
      <pubDate>Thu, 18 Jul 2024 11:05:37 +0000</pubDate>
      <link>https://dev.to/hughp135/optimising-package-size-for-typescript-aws-lambda-functions-using-serverless-esbuild-2eg4</link>
      <guid>https://dev.to/hughp135/optimising-package-size-for-typescript-aws-lambda-functions-using-serverless-esbuild-2eg4</guid>
      <description>&lt;p&gt;Here is how I reduced the package size of my lambda functions from 60MB per function to ~1MB and cold-start init times from 606ms to 291ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;We have recently been using the &lt;a href="https://www.npmjs.com/package/serverless-esbuild" rel="noopener noreferrer"&gt;serverless-esbuild&lt;/a&gt; plugin over &lt;a href="https://www.npmjs.com/package/serverless-plugin-typescript" rel="noopener noreferrer"&gt;serverless-plugin-typescript&lt;/a&gt; package in new Typescript serverless projects as it can transpile much faster (saving development time) and is easy to optimize for bundle sizes.&lt;/p&gt;

&lt;p&gt;However, without proper configuration, your transpiled code can end up surprisingly large. This can impact the speed of deployment pipelines and cold start times of your lambda functions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Related article: &lt;a href="https://medium.com/@adtanasa/size-is-almost-all-that-matters-for-optimizing-aws-lambda-cold-starts-cad54f65cbb" rel="noopener noreferrer"&gt;Size is (almost) all that matters for optimizing AWS Lambda cold starts | by Adrian Tanasa | Medium&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Furthermore, if the package size of a single function gets too large (50MB zipped), it will actually fail to deploy due to AWS limits on lambda package size (this happened to me and was the motivation for me to optimise file size and make this guide).&lt;/p&gt;

&lt;p&gt;So, here are the steps to optimise serverless-esbuild settings:&lt;/p&gt;

&lt;h2&gt;
  
  
  Analyzing your bundle size
&lt;/h2&gt;

&lt;p&gt;To package your serverless project locally, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;serverless package -s offline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: the stage option (&lt;code&gt;-s offline&lt;/code&gt;) is optional, depends on if you use different stages or not.&lt;/p&gt;

&lt;p&gt;This generates a &lt;code&gt;.serverless&lt;/code&gt; folder in the root of your project containing a zip file of your package. Open the zip file to check the &lt;code&gt;index.js&lt;/code&gt; and &lt;code&gt;index.js.map&lt;/code&gt; file sizes.&lt;/p&gt;

&lt;p&gt;By setting the metafile option in serverless.yml, you can analyze the size of dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# serverless.yml
custom:
  esbuild:
    metafile: true # for local analysis only!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don’t forget to comment out this option or revert to false before deploying, as the metafile is unnecessary in production and will increase the package size.&lt;/p&gt;

&lt;p&gt;Afte packaging your project again, you should see the meta files in the zipped package (&lt;code&gt;index-meta.json&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;You can use the &lt;a href="https://esbuild.github.io/analyze/" rel="noopener noreferrer"&gt;esbuild - Bundle Size Analyzer&lt;/a&gt; tool to easily analyze the size of your dependencies:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6hxew1avumtao2fq35fm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6hxew1avumtao2fq35fm.png" alt="Image description" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you see packages that shouldn't be there in the function (e.g. a mysql library where your function doesn't perform any DB operations), find where it's imported and remove it.&lt;/p&gt;

&lt;p&gt;If you're unsure how the package was imported, it may be a dependency of another package. To locate which package uses the subpackage, you can use the npm explain command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm explain package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Optimize esbuild settings
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#serverless.yml
plugins:
  - serverless-esbuild

package:
  individually: true

custom:
  # Here you can define your esbuild options
  esbuild:
    bundle: true
    sourcemap: true
    sourcesContent: false # Omits original source code from sourcemaps (set to false to allow debugging in AWS)
    minify: true
    exclude: ['@aws-sdk']
    # custom plugin to remove vendor sourcemaps
    plugins: esbuild-plugins.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explanation of settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package:
  individually: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The option will create a separate zip file for each lambda function, speeding up deployment and lowering size per function. If you don’t set this to true, every one of your lambda functions will contain the entire source code of the whole project and all dependencies, potentially increasing package size a lot.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sourcemap: true&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Sourcemap option should be true because we want to be able to locate stack traces thrown in production environments easily. Note: to enable sourcemaps in AWS, the environment variable &lt;code&gt;NODE_OPTIONS=--enable-source-maps&lt;/code&gt; must also be set.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sourcesContent: false&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;By default esbuild will duplicate your source code in the sourcemaps, so that debugging can be possible with the AWS debugger. However, this can increase bundle size significantly.&lt;/p&gt;

&lt;p&gt;However, there is a major downside: sourcemaps will include all node_modules packages resulting in an increase in package size. I will explain how to remedy this in the next section.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;minify: true&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Minifies code, resulting in further file size reduction&lt;/p&gt;

&lt;p&gt;&lt;code&gt;exclude: ['@aws-sdk']&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Exclude aws-sdk from your package (and move it from &lt;code&gt;dependencies&lt;/code&gt; to &lt;code&gt;devDependencies&lt;/code&gt; in your &lt;code&gt;package.json&lt;/code&gt;), since Node.js lambda functions automatically have access to the AWS libraries when deployed!&lt;/p&gt;

&lt;h2&gt;
  
  
  Removing vendor source maps
&lt;/h2&gt;

&lt;p&gt;The source map files generated by esbuild can be extremely large if you have many heavy dependencies (larger than the source code). This is because esbuild generates sourcemaps not just for your code, but for all imported node_modules too.&lt;/p&gt;

&lt;p&gt;While it can sometimes be useful to have vendor sourcemaps when debugging code, the use case for debugging 3rd party package’s source code from production stack traces is quite uncommon.&lt;/p&gt;

&lt;p&gt;To exclude vendor scripts from your source files, we need to write a custom plugin for esbuild.&lt;/p&gt;

&lt;p&gt;First add this option to your serverless.yml:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# serverless.yml
custom:
  esbuild:
    plugins: esbuild-plugins.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a esbuild-plugins.js file in the same directory as the &lt;code&gt;serverless.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// esbuild-plugins.js

const { readFileSync } = require('fs');

const excludeVendorFromSourceMapPlugin = {
  name: 'excludeVendorFromSourceMap',
  setup(build) {
    build.onLoad({ filter: /node_modules/ }, (args) =&amp;gt; {
      if (args.path.endsWith('.js') || args.path.endsWith('.mjs') || args.path.endsWith('.cjs')) {
        return {
          contents:
            readFileSync(args.path, 'utf8') +
            '\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJtYXBwaW5ncyI6IkEifQ==',
          loader: 'default',
        };
      }
    });
  },
};

module.exports = [excludeVendorFromSourceMapPlugin];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this script does is replace sourcemaps of files from the node_modules folder with an empty sourcemap template.&lt;/p&gt;

&lt;p&gt;Applying this plugin reduced sourcemap file in each lambda function of my project from 10MB to about 2.5MB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid importing dependencies that aren’t needed
&lt;/h2&gt;

&lt;p&gt;ESBuild has &lt;a href="https://en.wikipedia.org/wiki/Tree_shaking" rel="noopener noreferrer"&gt;tree shaking&lt;/a&gt; built in, meaning that only modules your lambda function imports (from the module containing lambda handler's entry point) will be included in the bundle. However, it is easy to unintentionally import modules that your lambda function doesn’t need indirectly through commonly shared modules.&lt;/p&gt;

&lt;p&gt;Use the bundle analyzer and find out if there are any large dependencies that look like they shouldn’t be needed in the specific lambda function you’re analyzing.&lt;/p&gt;

&lt;p&gt;In one project, I used the sequelize package to connect to a database. However, only some of these functions actually required a database connection.&lt;/p&gt;

&lt;p&gt;The code was unintentionally importing the ‘sequelize’ package in every lambda function indirectly through a commonly imported module, adding around 1MB of script size per lambda function.&lt;/p&gt;

&lt;p&gt;A simple refactor to remove the sequelize import when a DB connection wasn’t needed reduced the file size significantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If your main goal is to save on infrastructure costs, spending a lot of time to optimise your lambda package sizes may not be worth the effort.&lt;/p&gt;

&lt;p&gt;Although you won't be charged extra by AWS for having larger package sizes, there may be a significant performance impact on cold starts.&lt;/p&gt;

&lt;p&gt;Double checking your esbuild configuration is a low-hanging-fruit that can reduce a lot of "digital waste" with minimal effort.&lt;/p&gt;

&lt;p&gt;Before optimising esbuild, my lambda package was 60MB, which was higher than the limit allowed by AWS.&lt;/p&gt;

&lt;p&gt;After reducing this to 35MB with excluding vendor sourcemaps, I still hadn’t packaged functions individually, so every single lambda function would have contained the full 35MB of source code + vendor scripts, making the total storage space used 630MB over 18 lambda functions.&lt;/p&gt;

&lt;p&gt;These optimization changes reduced package size to around 0.5MB-1.5MB per function, totalling only 16MB uploaded to AWS, a dramatic reduction.&lt;/p&gt;

&lt;p&gt;Furthermore, the cold start init duration of a simple ‘Hello World’ function was reduced from 606ms to 291ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;aws-sdk&lt;/code&gt; to the exclude array in esbuild options (reducing script slightly) - the aws library should exist in the deployed environment.&lt;/li&gt;
&lt;li&gt;Set the esbuild &lt;code&gt;minify&lt;/code&gt; option to true in &lt;code&gt;serverless.yml&lt;/code&gt; (another slight reduction)&lt;/li&gt;
&lt;li&gt;Package functions individually using serverless.yml setting (large reduction of total size uploaded to AWS for multiple functions)&lt;/li&gt;
&lt;li&gt;Add a plugin to exclude vendor sourcemaps from the scripts (big reduction) exclude node_modules from source map  &lt;a href="https://github.com/evanw/esbuild/issues/1685" rel="noopener noreferrer"&gt;Issue #1685 · evanw/esbuild · GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;serverless package&lt;/code&gt; command locally to check the file size output in .serverless folder.&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://esbuild.github.io/analyze/" rel="noopener noreferrer"&gt;esbuild - Bundle Size Analyzer&lt;/a&gt; (after setting metafile esbuild option to true) to observe dependencies size&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>serverless</category>
      <category>typescript</category>
      <category>lambda</category>
      <category>node</category>
    </item>
  </channel>
</rss>
