AWS Developer Tools Blog

Modular packages in AWS SDK for JavaScript

As of December 15th, 2020, the AWS SDK for JavaScript, version 3 (v3) is generally available.

On October 19th, 2020, we published the Release Candidate (RC) of the AWS SDK for JavaScript, version 3 (v3). One of the major changes in the JavaScript SDK v3 is modularized packages. This blog post explains why we decided to publish modular packages, and gives an example of performance improvement due to bundle size reduction.

Motivation

The v2 of AWS SDK for JavaScript is published as a single package. If you import the entire SDK even if your application uses just a subset of SDK’s functionalities, there are performance implications noticeable in resource-constrained environments, like IoT devices or browsers on low-end mobile devices.

You can reduce the bundle size of your application with dead-code elimination, commonly known as tree shaking, using tools like webpack or Rollup. Tree shaking prunes unused code paths from your application, but it requires knowledge and expertise of the tools. Rather than installing the entire SDK and subsequently prune dead code, it is better to only import parts of the JavaScript SDK that are used by your application.

Usage

In v3 of AWS SDK for JavaScript, we achieved modularity by breaking the JavaScript SDK core into multiple packages and publishing each service as its own package. These packages are published under @aws-sdk/ scope on NPM to make it easy to identify packages that are part of the official AWS SDK for JavaScript.

Importing a service client

The service clients are prefixed with client- followed by service name. In v2, the S3 client can be created using single monolithic package:

const AWS = require("aws-sdk");

const s3Client = new AWS.S3({});
await s3Client.createBucket(params);

The service can also be imported as sub-module from in v2, which provides some benefit of bundle size reduction.

const S3 = require("aws-sdk/clients/S3");

const s3Client = new S3({});
await s3Client.createBucket(params);

In v3, you can create the modular aggregated S3 client by importing @aws-sdk/client-s3:

const { S3 } = require("@aws-sdk/client-s3");

const s3Client = new S3({});
await s3Client.createBucket(params);

The v3 also lets you import a modular bare-bones client with specific commands that help further reduce application bundle size!

const { S3Client, CreateBucketCommand } = require("@aws-sdk/client-s3");

const s3Client = new S3Client({});
await s3Client.send(new CreateBucketCommand(params));

Importing a high level operation

This modularization also applies to high-level operations, which are no longer a part of the client package. These high-level operations share lib- prefix followed by the operation name. For example, the S3 Multipart Upload is a part of S3 client in v2:

const AWS = require("aws-sdk");
const multipartUpload = new AWS.S3.ManagedUpload({
  params: {Bucket: 'bucket', Key: 'key', Body: stream}
});

In v3, the high-level operation is moved into the new package @aws-sdk/lib-storage:

const { S3Client } = require("@aws-sdk/client-s3");
const { Upload } = require("@aws-sdk/lib-storage");

const multipartUpload = new Upload({
    client: new S3Client({}),
    params: {Bucket: 'bucket', Key: 'key', Body: stream},
});

Importing a utility function

The v3 also publishes utility packages that have a util- prefix followed by the utility name. For example, we publish marshall and unmarshall operations for DynamoDB in @aws-sdk/util-dynamodb to convert JavaScript object into DynamoDB record and vice-versa.

const { DynamoDB } = require("@aws-sdk/client-dynamodb");
const { marshall, unmarshall } = require("@aws-sdk/util-dynamodb");

const client = new DynamoDB(clientParams);
const putParams = {
  TableName: "Table",
  Item: marshall({
    HashKey: "hashKey",
    NumAttribute: 1,
    BoolAttribute: true,
    ListAttribute: [1, "two", false],
    MapAttribute: { foo: "bar" },
    NullAttribute: null,
  }),
};

await client.putItem(putParams);

const getParams = {
  TableName: "Table",
  Key: marshall({
    HashKey: "hashKey",
  }),
};

const { Item } = await client.getItem(getParams);
unmarshall(Item);

Implementation

Even though v3 has 300+ packages published on npm, it is managed in a single multi-package repository (also called a monorepo) on GitHub. We use lerna and yarn workspaces to split the large codebase into separate independently versioned packages.

Metrics

To measure bundle size reduction, we created a self-guided workshop that provides step-by-step instructions to migrate a simple note taking application from using JavaScript SDK v2 to v3. The application manages notes in a DynamoDB table using AWS SDK for JavaScript in Node.js in a lambda backend, and manages files in S3 using the JavaScript SDK in the browser on the frontend.

Backend

In the workshop README for backend, we import the entire v2 which results in lambda bundle size for each of the create, read, update, delete, list operations to be ~817 kB.

A list of lambda functions with Runtime and Code sizes in v2

We update the code to use submodules in v2, followed by importing entire client in v3 and importing bare-bones client with commands. The final code results in each lambda bundle size to reduce to ~23 kB!

A list of lambda functions with Runtime and Code sizes in v3

To examine performance benefits of reduction in bundle size during cold start, we wrote a cloudwatch event to trigger both lambdas every 20 minutes for 18 values over 6 hours. The data for AWS::Lambda::Function (in ms) shows ~40% improvement in function executions times in our experiment.

Average Min Max Median 90th percentile
Entire JavaScript SDK v2 1171.5 1013 1431 1093.5 1193.39
JavaScript SDK v3 client+command 735.22 693 786 738 775.6

Frontend

In the workshop README for frontend, we import the entire v2 which emits main chunk of size ~395 KB in the production bundle.

File sizes after gzip:

  395.2 KB  build/static/js/2.9a081e7a.chunk.js
  2.88 KB   build/static/js/main.9af70d78.chunk.js
  792 B     build/static/js/runtime-main.64ddd279.js

We update the code to use submodules in v2, followed by importing entire client in v3 and importing bare-bones client with commands. Modular imports now allow us to use code-splitting, which reduces the amount of code needed during the initial load. This reduces the main chunk size to ~48 KB in the production bundle!

File sizes after gzip:

  47.81 KB  build/static/js/1.7e51cbd2.chunk.js  
  46.96 KB  build/static/js/4.818586d4.chunk.js  
  7.85 KB   build/static/js/0.6a9c1fc3.chunk.js  
  3.02 KB   build/static/js/6.c7a500e3.chunk.js  
  2.5 KB    build/static/js/5.12e58bc3.chunk.js  
  1.72 KB   build/static/js/7.3d0fbc81.chunk.js  
  1.33 KB   build/static/js/8.074d72d1.chunk.js  
  1.24 KB   build/static/js/runtime-main.fb721bd4.js  
  525 B     build/static/js/main.ad4e136c.chunk.js

To examine performance benefits of reduction in bundle size in the frontend, we serve a production bundle over localhost and hard reload it by simulating “Fast 3G” under network tab in Chrome Developer Tools.

The frontend bundle created by importing entire v2 takes ~17 seconds to fire Load event in our experiment.

Screenshot of Chrome Developer Tools Network tab showing Network Activity for bundle build using v2

Where as the frontend bundle which imports bare-bones client with commands in v3 and uses code splitting takes less than 3 seconds to fire Load event!

Screenshot of Chrome Developer Tools Network tab showing Network Activity for bundle build using v3 with code splitting

This note taking application code clearly shows the performance improvement due to bundle size reduction using modular packages in AWS SDK for JavaScript v3 in a real-world application, both in the frontend as well as in the backend.

Feedback

We value your feedback, so please tell us what you like and don’t like by opening an issue on GitHub on AWS SDK for JavaScript v3 repository and workshop repository.

Trivikram Kamat

Trivikram Kamat

Trivikram is maintainer of AWS SDK for JavaScript in Node.js and browser. Trivikram is also a Node.js Core collaborator and have contributed to HTTP, HTTP/2 and HTTP/3 over QUIC implementations in the past. He has been writing JavaScript for over a decade. You can find him on Twitter @trivikram and GitHub @trivikr.