Skip to content

Commit

Permalink
docs: add docs on performance improvements (#5848)
Browse files Browse the repository at this point in the history
  • Loading branch information
trivikr authored Mar 1, 2024
1 parent 1be7104 commit 7ae5125
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 0 deletions.
16 changes: 16 additions & 0 deletions supplemental-docs/performance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Performance

Performance is crucial for the AWS SDK for JavaScript because it directly impacts the user experience and cost efficiency of applications.

The faster response times lead to smoother interactions for end users, enhancing the overall experience of software built using the SDK. Improved performance can help reduce the cost of running applications on AWS by reducing the amount of resources required to handle the same workload. Performance optimizations enable applications to handle increased traffic and scale more efficiently, ensuring that they can meet the demands of growing user bases.

When working on new major version v3 of AWS SDK for JavaScript, we listened closely to the requirements from our customers and implemented performance improvements to better meet their needs. Please refer to the topics below for details on each improvement.

The time it takes for a Node.js application to start up or load is directly related to how many dependencies it uses that need to be resolved, as well as the number of files the runtime has to read. If you’re sensitive to initialization times, like cold start time in AWS Lambda, try reducing the number of dependencies that need resolution and the number of files in your application. One way to do this without changing your existing code is by bundling your application. When you bundle your application, the Node.js runtime doesn't need to resolve any modules and only has to read the single bundle file, which speeds up startup time. Please refer to our blog post on [reducing Lambda cold start times](https://aws.amazon.com/blogs/developer/reduce-lambda-cold-start-times-migrate-to-aws-sdk-for-javascript-v3/) which provides benchmarks on how bundled application using v3 has the fastest cold start times.

Topics:

- [Publish and Install Sizes](./publish-and-install-sizes.md)
- [Bundle Sizes](./bundle-sizes.md)
- [Dynamic Imports](./dynamic-imports.md)
- [Dependency File Count Reduction](./dependency-file-count-reduction.md)
53 changes: 53 additions & 0 deletions supplemental-docs/performance/bundle-sizes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Performance > Bundle sizes

In the context of web development, a JavaScript application bundle refers to a single file or a collection of files that contain all the necessary code needed to run a web application. The bundling process, using tools like Webpack, Rollup, Parcel or esbuild, combines all the application files into a smaller number of files (often just one or a few) that can be easily loaded by the web browser.

Here are some of the key benefits of bundling:

- **Improved Performance**: Combining multiple files into one reduces the number of HTTP requests needed to fetch resources, which can improve loading times, especially on slower networks.
- **Code Organization**: Bundling can help organize your codebase by allowing you to split your code into smaller, more manageable modules without sacrificing performance.
- **Code Optimization**: Bundling can also include optimizations such as minification (removing unnecessary characters like white spaces and comments) and tree shaking (removing unused code) to further reduce the bundle size.
- **Caching**: Bundling can improve caching efficiency, as the browser only needs to cache less files, reducing the likelihood of cache fragmentation.
- **Compatibility**: You can optionally use the latest features of JavaScript or TypeScript language in your source code, and target your bundle for older runtimes which do not them. And your bundler will provide polyfills for those features.

Although bundling was invented to reduce file count and size on the frontend, it is helpful in the backend too. Just like browsers, the initialization time improves in the backend since runtime has to read just one bundle file and does not have to spend time in resolving dependencies. This helps reducing cold start times in Serverless Environment, like AWS Lambda. Bundling can also reduce deployment times, as there are fewer files to deploy.

The JS SDK v3 has smaller bundle sizes than those of v2, as it follows coding styles to enable dead code elimination. For example, we use named exports instead of default exports, i.e. export only the functions which need to be exposed. This makes it easier for tools to identify and remove unused code. We also use pure functions in the code base which does not have side effects. This makes it easier for tools to determine if a function is used or not, and safely remove unused functions.

The bundle sizes of AWS SDK for JavaScript can be verified from third party tools, like [BundlePhobia](https://bundlephobia.com/), which helps find overall performance impact of npm packages. For example, the below screenshots show that v2 package has bundle size of 3.4 MB, while v3 DynamoDB package has bundle size of 234.6 kB.

<!-- prettier-ignore-start -->
aws-sdk | @aws-sdk/client-dynamodb
:-------------------------:|:-------------------------:
![Image: https://bundlephobia.com/package/aws-sdk](./img/bundlephobia-aws-sdk.png) | ![Image: https://bundlephobia.com/package/@aws-sdk/client-dynamodb](./img/bundlephobia-aws-sdk-v3.png)
<!-- prettier-ignore-end -->

```console
$ npm install [email protected] [email protected] --save-exact

$ echo 'import { DynamoDB } from "aws-sdk";
const client = new DynamoDB();' > index.js

$ npx esbuild index.js --bundle --minify \
--main-fields=module,main --outfile=out.js

out.js 3.2mb ⚠️

⚡ Done in 53ms
```

```console
$ npm install @aws-sdk/[email protected] [email protected] --save-exact

$ echo 'import { DynamoDB } from "@aws-sdk/client-dynamodb";
const client = new DynamoDB();' > index.js

$ npx esbuild index.js --bundle --minify \
--main-fields=module,main --outfile=out.js

out.js 214.2kb

⚡ Done in 37ms
```

Here we compare the bundle sizes of two different applications containing equivalent code which imports DynamoDB client from AWS SDK for JavaScript and creates a client. The bundle size of application built using v2 is 3.2 MB, while the bundle size of equivalent application built using v3 is 214.2 kB.
45 changes: 45 additions & 0 deletions supplemental-docs/performance/dependency-file-count-reduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Performance > Dependency File Count Reduction

While benchmarking cold start times on serverless environments like AWS Lambda, we noticed that the duration Node.js runtime spends reading dependency code is directly correlated with the number of files required to read from the artifacts.

The AWS SDK for JavaScript v3 chose [modularity](https://aws.amazon.com/blogs/developer/modular-packages-in-aws-sdk-for-javascript/) for reducing install sizes, but it leads to increase in the number of files required to be read. There are several other benefits of modularity which are more beneficial in software development:

- **Ease of maintenance**: Each function is contained in its own file, making it easier to understand and update without affecting other parts of the codebase.
- **Readability**: Smaller files are often easier to read and navigate, especially when dealing with large codebases. It can also make it easier for new developers to understand the code.
- **Reusability**: Individual functions can be reused more easily across different parts of your application.
- **Testing**: With each function in its own file, it can be easier to write unit tests for each function, as you can test them in isolation.
- **Performance**: While the impact might be minimal, having smaller files can potentially improve performance by reducing the amount of code that needs to be loaded into memory at once.
- **Collaboration**: It can make collaboration easier, as different developers can work on different functions without stepping on each other's toes.

In order to retain the benefits of modularity while reducing the number of files the Node.js runtime need to read, we inline the dependency code of our internal packages in a single file. Our benchmarking showed cold start times reduced by **27-32%** from this change. Please bump your SDK version to >=v3.495.0 to avail these improvement.

You can verify the number of files read by comparing two versions of example dependency `@aws-sdk/util-dynamodb` as follows:

```console
$ echo 'const cachedFilesBefore = Object.keys(require.cache);

const packageName = "@aws-sdk/util-dynamodb";
require(packageName);

const cachedFilesAfter = Object.keys(require.cache);
const cachedFiles = cachedFilesAfter.filter(
(file) => !cachedFilesBefore.includes(file)
);

const version = require(`${packageName}/package.json`).version;
console.log(
`${cachedFiles.length} file(s) are read when importing ${packageName}@${version}`
);' > test.js

$ npm install @aws-sdk/[email protected] --save-exact

$ node test.js
8 file(s) are read when importing @aws-sdk/[email protected]

$ npm install @aws-sdk/[email protected] --save-exact

$ node test.js
1 file(s) are read when importing @aws-sdk/[email protected]
```

If you are sensitive to initialization times of your application, then you need to reduce file access and module resolutions required to be done by Node.js runtime, which can be easily achieved by bundling your application. Please refer to our blog post on [reducing cold start times](https://aws.amazon.com/blogs/developer/reduce-lambda-cold-start-times-migrate-to-aws-sdk-for-javascript-v3/) which provides detailed benchmarks for different use cases on AWS Lambda.
69 changes: 69 additions & 0 deletions supplemental-docs/performance/dynamic-imports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Performance > Dynamic Imports

A [dynamic import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) is a function-like expression that allows loading an ECMAScript module asynchronously and dynamically. Unlike the declaration-style counterpart, dynamic imports are only evaluated when needed, and permit greater syntactic flexibility.

Dynamic imports were introduced in ECMAScript 2020, and they have the following benefits:

- **Lazy Loading**: You can load modules only when they are needed, improving the initial loading time of your application. This can be particularly useful for large applications where loading all modules at once might be inefficient.
- **Code Splitting**: Dynamic imports enable code splitting, which means you can split your code into smaller chunks and load them on demand. This can lead to a smaller initial bundle size per chunk, reducing the time it takes to load your application.
- **Improved Performance**: By loading modules asynchronously, you can prevent blocking the main thread, which can improve the performance of your application, especially in cases where modules are large or take time to load.
- **Reduced Memory Usage**: Since modules are loaded only when needed, you can reduce the amount of memory your application uses, especially if you have a large number of modules.
- **Better Resource Management**: Dynamic imports allow for better resource management, as you can control when and how modules are loaded, making it easier to manage dependencies and avoid unnecessary loading.

The dynamic imports are usually used in application code for the mentioned benefits. But they can be used in dependencies too. Since the AWS SDK for JavaScript v3 supports Node.js 14+ (ES2020), we can use Dynamic imports to only load the modules when required. This is especially helpful in reducing the Lambda cold start times.

In v3, the dynamic imports are used in credential providers. You can verify the module cache on loading the credential providers in JS SDK v2 vs v3.

```console
$ echo 'const fs = require("fs");

const cachedFilesBefore = Object.keys(require.cache);

const getCacheStats = (modulePath) => {
require(modulePath);
const cachedFilesAfter = Object.keys(require.cache);

const cachedFiles = cachedFilesAfter.filter((file) => !cachedFilesBefore.includes(file));

const noOfFilesInCache = cachedFiles.length;
let bytesInCache = 0;
cachedFiles.forEach((filePath) => {
bytesInCache += fs.statSync(filePath).size;
});

return {
noOfFilesInCache,
bytesInCache,
};
};

module.exports = { getCacheStats };' > getCacheStats.js
```

```console
$ npm install [email protected] --save-exact

$ echo 'const { getCacheStats } = require("./getCacheStats");
console.log(getCacheStats(
"aws-sdk/lib/credentials/credential_provider_chain"
));' > cache-v2.js

$ node cache-v2.js
{ noOfFilesInCache: 53, bytesInCache: 397715 }
```

```console
$ npm install @aws-sdk/[email protected] --save-exact

$ echo 'const { getCacheStats } = require("./getCacheStats");
console.log(getCacheStats(
"@aws-sdk/credential-provider-node"
));' > cache-v3.js

$ node cache-v3.js
{ noOfFilesInCache: 8, bytesInCache: 26127 }
```

In the above code samples, we compare the number of files and bytes loaded in cache when importing equivalent credential providers. In v2, there are 53 files loaded in cache with total size of 398 kB. While in v3, there are only 9 files loaded in cache, which total size of 26 kB.

If you’re bundling your application, the Dynamic imports are not supported by default in some old versions of bundlers. For example, if your setup is using `@babel/core@<7.8.0`, you need to explicitly add `@babel/plugin-syntax-dynamic-import` plugin to transform the code.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions supplemental-docs/performance/publish-and-install-sizes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Performance > Publish and Install sizes

A “publish size” of an npm package is the size of the source code published to npm. The "install size" of an npm package refers to the amount of disk space that will be used when the package is installed on your system. This includes not only the size of the package itself, but also any dependencies that it relies on. When you install an npm package, npm will download the package and all of its dependencies to a node_modules directory in your project, so the install size is the total size of all of these files combined.

If you import the entire SDK even if your application uses just a subset of SDK’s functionalities, it’ll increase application size. Your application may potentially exceed disk size quota in resource-constrained environments, like Serverless or IoT devices, blocking your application development. There are other performance and efficiency issues with large install sizes:

- **Installation Time**: Larger dependencies take longer to download and install, which can slow down the development process.
- **Resource Consumption**: Larger dependencies may consume more memory and CPU resources, affecting the scalability and efficiency of the application, especially in serverless environments.
- **Dependency Management**: Managing large dependencies can be more complex, as they may have their own dependencies and version requirements, leading to potential conflicts and issues.
- **Deployment Size**: Larger dependencies increase the size of deployment packages, which can be a concern for deployment pipelines and storage costs.

It's generally advisable to keep dependencies as small and as minimal as possible to improve the performance and maintainability of Node.js applications. In v3, we divided the JavaScript SDK core into multiple [modular packages](https://aws.amazon.com/blogs/developer/modular-packages-in-aws-sdk-for-javascript/) and publishing each service as its own package, thus reducing both the publish and install sizes.

The install and publish sizes of AWS SDK for JavaScript can be verified from third party tools, like [PackagePhobia](https://packagephobia.com/), which measures the actual byte size of the artifacts. For example, the below screenshots show that v2 package has install size of 93.6 MB, while v3 DynamoDB package has install size of 6.29 MB.

<!-- prettier-ignore-start -->
aws-sdk | @aws-sdk/client-dynamodb
:-------------------------:|:-------------------------:
![Image: https://packagephobia.com/result?p=aws-sdk](./img/packagephobia-aws-sdk.png) | ![Image: https://packagephobia.com/result?p=@aws-sdk/client-dynamodb](./img/packagephobia-aws-sdk-client-dynamodb.png)
<!-- prettier-ignore-end -->

The install size on disk can also be verified locally on your machine as follows:

```console
$ npm install [email protected] --save-exact

$ du -sh node_modules
101M node_modules
```

```console
$ npm install @aws-sdk/[email protected] --save-exact

$ du -sh node_modules
17M node_modules
```

The install size on disk is larger, as it’s the actual size on disk which depends on blocks are allocated. It may be different on your machine, but the relative difference between the sizes would be similar.

Most of AWS SDK for JavaScript applications use one to three clients. Even if you install the five most popular clients, the install size is less than a third of that in v2.

```console
$ npm install @aws-sdk/client-s3 @aws-sdk/client-kinesis \
@aws-sdk/client-sns @aws-sdk/client-cloudwatch @aws-sdk/client-api-gateway

$ du -sh node_modules
30M node_modules
```

0 comments on commit 7ae5125

Please sign in to comment.