Engineering

Reduce the bloat of your Lambdas

With most common JavaScript projects, the bulk of the bundle weight is in the dependencies.

serverless simplifies the creation of Lambdas by interpreting a - simpler than CloudFormation - configuration file and automating the packaging, uploading, and deployment lifecycles of applications. When packaging, it can be smart enough to exclude development dependencies. It is a good start, but it can’t:

  1. identify which files per production dependency are actually used;
  2. only bundle the dependencies used by each function.

There is a better, simpler, and faster way though: serverless-plugin-ncc. This plugin goes through each entry point of your functions and sends it to ncc, which bundles them into a single file with only the required code, tree-shaken.

Let’s see it in practice: we’re going to create 3 functions, one for a GraphQL endpoint, a Playground endpoint, and a simpler function to save payloads into an S3 bucket.

const { graphqlLambda } = require('apollo-server-lambda/dist/lambdaApollo');
const gql = require('graphql-tag');

exports.handler = graphqlLambda({
  // our schema
  typeDefs: gql``,
  // our resolvers
  resolver: {},
});
const lambdaPlayground = require('graphql-playground-middleware-lambda');

const { ENDPOINT_URL = '' } = process.env;
module.exports.handler = lambdaPlayground({
  endpoint: ENDPOINT_URL,
});
const S3 = require('aws-sdk/clients/s3');

const s3 = new S3();
module.exports.handler = async (ev) => {
  // ...
};

And our serverless config:

const { name: service } = require('./package.json');

const { INDIVIDUALLY = '0', NCC = '0' } = process.env;
const IS_INDIVIDUALLY = Boolean(JSON.parse(INDIVIDUALLY));
const IS_NCC = Boolean(JSON.parse(NCC));

module.exports = {
  service,
  plugins: [IS_NCC && 'serverless-plugin-ncc'].filter(Boolean),
  provider: {
    name: 'aws',
    runtime: 'nodejs10.x',
    // eslint-disable-next-line no-template-curly-in-string
    region: '${env:AWS_REGION, "eu-west-3"}',
    // eslint-disable-next-line no-template-curly-in-string
    stage: '${opt:stage, "development"}',
    environment: {
      // eslint-disable-next-line no-template-curly-in-string
      NODE_ENV: '${env:NODE_ENV, "development"}',
    },
  },
  package: {
    individually: IS_INDIVIDUALLY,
    excludeDevDependencies: !IS_NCC,
  },
  custom: {
    ncc: {
      // we can ignore aws-sdk because the node runtime in lambda automatically include is
      externals: ['aws-sdk/clients/s3'],
    },
  },
  functions: {
    GraphQL: {
      handler: 'graphql.handler',
      events: [
        {
          http: {
            path: '/graphql',
            method: 'POST',
          },
        },
      ],
    },
    Playground: {
      handler: 'playground.handler',
      events: [
        {
          http: {
            path: '/playground',
            method: 'GET',
          },
        },
      ],
    },
    Upload: {
      handler: 'upload.handler',
      events: [
        {
          http: {
            path: '/upload',
            method: 'POST',
          },
        },
      ],
    },
  },
};

As you can see, nothing too fancy: three functions declared each with its route and handler.

Setting package.individually will generate a bundle per each function. We control that by assigning it to the INDIVIDUALLY environment variable. We also control whether serverless-plugin-ncc is active based on the NCC environment variable. For each to be active, the variable value needs to be truthy.

With this setup, we can test different configurations via the serverless cli:


λ time NODE_ENV=production ./node_modules/.bin/sls package --stage production
16.37s user 4.49s system 100% cpu 20.681 total
λ ls -lah .serverless
4.7M sls-ncc-example.zip

With a global output and no ncc bundling, packaging takes ~20s and produces a 4.7MB zip.

Here’s what happens when we run the same code and set individually to true:

λ time NODE_ENV=production INDIVIDUALLY=1 ./node_modules/.bin/sls package --stage production
54.07s user 9.74s system 145% cpu 43.994 total
λ ls -lah .serverless
4.7M GraphQL.zip
4.7M Playgroud.zip
4.7M Upload.zip

Because our handlers are small and serverless doesn’t exclude dependencies based on the handler, what we get is practically the same output size for each package. Also, it takes +3x the time it took previously because it’s in practice the same logic but for each function now.

Now, the exciting test:

λ time NODE_ENV=production INDIVIDUALLY=1 NCC=1 ./node_modules/.bin/sls package --stage production
9.36s user 0.86s system 129% cpu 7.923 total
λ ls -lah .serverless
797K GraphQL.zip
5.5K Playgroud.zip
21K  Upload.zip

Not only we reduced our packaging time in at least half, but we also reduced our packages by at least 6x.

Reduce bloat of your Lambdas
was originally published in YLD Blog on Medium.
Share this article: