Skip to content

SignatureV4 module loads unneeded dependencies #1159

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
dreamorosi opened this issue Feb 7, 2024 · 1 comment · Fixed by #1233
Closed

SignatureV4 module loads unneeded dependencies #1159

dreamorosi opened this issue Feb 7, 2024 · 1 comment · Fixed by #1233
Assignees

Comments

@dreamorosi
Copy link

dreamorosi commented Feb 7, 2024

As per title, the @smithy/signature-v4 package loads some dependencies (@aws-crypto/*) even though they are not needed / not used in the context of signing a request. This happens because of the package importing a utility function from the @smithy/eventstream-codec, and in doing so, it also loads the full content of the package and its dependencies.

This results in a bundle size that is 47% larger than it could be. Below an in depth exploration with reproduction steps.

Use Case

In a Node.js environment (AWS Lambda) I am using the @smithy/signature-v4 package to sign requests. The use case is a Lambda@Edge attached to a CloudFront distribution with a Lambda function URL as origin. Given that Lambda function URLs support only IAM as authentication method, I'm using the Lambda@Edge to manipulate the request & sign it (among other validations), so that by the time it reaches the origin it's authenticated.

Below a high level diagram of what I just described above:

image

Reproduction steps

I have prepared a bare bones repository with the function that uses the @smithy/signature-v4 package that you can use to reproduce the steps below, you can find it here and follow along.

1. Clone the repository & install dependencies

Run git clone [email protected]:dreamorosi/smithy-sigv4.git and then npm ci to setup the repo.

2. Bundle code

Run npm run bundle in the project's root.

This step uses esbuild (which is default in both AWS CDK and AWS SAM) to create a bundle of the function (index.ts) and all its dependencies. As you can see from the command I use to bundle, I am using ESM and enabling tree shaking:

esbuild --bundle index.ts --format=esm --platform=node --main-fields=module,main --tree-shaking=true --outfile=out/index.mjs --metafile=out/meta.json

The command generates two output files, one is the function code including dependencies (out/index.mjs) and the other is a meta file (out/meta.json) which we'll use in the next step. The main output file is not minified so you can optionally inspect the content to verify the claims of this issue.

3. Analyze the bundle

In your browser, open https://esbuild.github.io/analyze/ and drag (or select) the meta file (out/meta.json) generated at the previous step. The resulting chart should look like this:

image

image

As you can see from the images above, the final bundle size (unminified) is 57.2 kb, half of which is comprised by dependencies under @aws-crypto, namely: @aws-crypto/util and @aws-crypto/crc32.

These two dependencies are also the only two that are not dual bundled (aka CJS + ESM like all @smithy/* and @aws-sdk/* packages are), which result in them not being excluded from tree shaking entirely.

By analyzing at the dependency tree, we can see that the @aws-crypto/* packages are brought in via the @smithy/eventstream-codec package:

image

A quick search in the node_modules/@smithy/signature-v4 shows that the @smithy/eventstream-coded package is imported only once:

find node_modules/@smithy/signature-v4 -type f -exec grep -l "@smithy/eventstream-codec" {} + 

# result
node_modules/@smithy/signature-v4/dist-es/SignatureV4.js
node_modules/@smithy/signature-v4/dist-cjs/index.js
node_modules/@smithy/signature-v4/package.json

And specifically only to use a single function (see import here):

import { HeaderMarshaller } from "@smithy/eventstream-codec";

This function, interestingly enough, doesn't rely nor import either of the @aws-crypto/* packages as evidenced in its implementation, but since it's brought in via barrel file it still get them included in the bundle.

4. Create alternate bundle to verify

To verify that this is the case, and only for demonstration purposes (read next section for more alternatives), we can manually modify the import found at L1 of node_modules/@smithy/signature-v4/dist-es/SignatureV4.js (which corresponds to this line in the source) like this:

-- import { HeaderMarshaller } from "@smithy/eventstream-codec";
++ import { HeaderMarshaller } from "@smithy/eventstream-codec/dist-es/HeaderMarshaller";

Using this method we are bypassing the default export and instead importing only that specific file (and other any file it imports).

If we run npm run bundle again, and analyze the output we can see that the @smithy/eventstream-codec module is now tree shaken correctly and the @aws-crypto/* are no longer part of the final bundle:

image
image

Not only that, but also the total size (unminified) drops to 30kb (-47.5%)

Potential Solutions

The workaround mentioned above is definitely not viable nor sustainable, however I believe the team should consider isolating that HeaderMarshaller utility function and avoid bringing in the rest of the @smithy/eventstream-codec package and all its dependencies (@aws-crypto/*).

Extract the utility in its own package or move to other existing package

This is pretty self explanatory, but essentially the suggestion is to move the HeaderMarshaller utility either in its own published package or in some other package (i.e. @smithy/signature-v4 or @smithy/http-protocol).

The team is better positioned to make a more informed choice on where it should land since I don't know where else that function is used.

Allow stable sub path exports

If you want to keep the HeaderMarshaller where it's now, another option would be to allow consumer modules to import it in isolation. This could be done for example via the exports field in the package.json.

To achieve this, you'd need to add following sections to the package.json file of the @smithy/eventstream-codec:

{
  "exports": {
    ".": {
      "import": "./dist-es/index.js",
      "require": "./dist-cjs/index.js"
    },
    "./headermarshaller": {
      "import": "./dist-es/HeaderMarshaller.js",
      "require": "./dist-cjs/HeaderMarshaller.js"
    }
  },
  "typesVersions": {
    "<4.0": {
      "dist-types/*": [
        "dist-types/ts3.4/*"
      ]
    },
    "*": {
      "headermarshaller": [
        "dist-types/ts3.4/*"
      ]
    }
  },
}

This way, consumer module (i.e. @smithy/signature-v4) would be able to import it like this:

-- import { HeaderMarshaller } from "@smithy/eventstream-codec";
++ import { HeaderMarshaller } from "@smithy/eventstream-codec/headermarshaller";

using this notation lets the module resolution skip the default import, which results in the same 30kb bundle I showed above.

@dreamorosi
Copy link
Author

Hello, is there any update/feedback/comment from the team on this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants