Things about software engineering

Serverless with Webpack and TypeScript

September 24, 2021

Who this is for

Anyone working with Serverless (Framework) evaluating use of Webpack or similar or looking to squeeze a bit more out of it. You don’t need to be a Webpack expert, it’ll all be explained. I’ll be referring to AWS, but it’s probably as applicable for Azure, GCP, et. al.

Also, this us for me, so I only have to remember one place, rather than many projects, to cross reference.

Package Verions

There is every possibility that this will be out of date in short order as these tools are constantly evolving. So, to that end, here are the current versions at the time of writing.

Why

Deployment package limitations

AWS currently enforces a size restriction of 50MB on (compressed) deployment packages, and if you’re using Sererless’ built-in bundler, there’s a good chance you’ll hit this pretty quickly.

Performance

The more code you ship, the longer everything takes. Using a bundler like Webpack will allow you to deploy only the code that’s needed, resulting in less painful cold starts and generally faster execution.

TypeScript path aliasing

If you’re using the de facto plugin for TypeScript, Serverless Plugin TypeScript , then at least one feature it doesn’t support is path aliasing. It’s not entirely a deal breaker, but if you’re particular about formatting and order (and I am), then it’s something you’ll probably be missing.

Additionally, though it’s recently had a major release, the plugin seems to be generally abandoned.

How

Dependencies

The below presumes you already have serverless and typescript installed.

yarn add -D serverless-webpack webpack webpack-node-externals ts-loader fork-ts-checker-webpack-plugin

Use an appropriate tsconfig for your Node version

Node supports a great deal of the modern features available to you through TypeScript. Consider installing and extending one of the base tsconfigs from tsconfig/bases, or copying and pasting one.

For reference, below is the tsconfig for Node 14 — it’s pretty small!

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Node 14",

  "compilerOptions": {
    "lib": ["es2020"],
    "module": "commonjs",
    "target": "es2020",

    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Add Serverless Webpack to your Serverless plugins

# serverless.yml
plugins:
  - serverless-webpack

Add a Webpack config

Rather than try and explain the below config in one hit, it’ll be thoroughly commented.

const path = require('path');
const slsw = require('serverless-webpack');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  // `mode` will be set to `production` and comes with included optimizations
  // when building to be run on AWS or similar. 
  // https://webpack.js.org/configuration/mode/
  mode: slsw.lib.webpack.isLocal ? 'development' : 'production',

  // to determine what source maps to use per dev or prod
  // https://webpack.js.org/configuration/devtool/
  devtool: slsw.lib.webpack.isLocal ? 'source-map' : 'cheap-source-map',
  
  // the provided argument will be an object referencing functions as defined
  // in your `serverless.yml` .
  // https://webpack.js.org/concepts/entry-points/
  entry: slsw.lib.entries,

  target: 'node',

  resolve: {
    // What file extensions we want Webpack to care about, and in what order
    // https://webpack.js.org/configuration/resolve/#resolveextensions
    extensions: ['.cjs', '.mjs', '.js', '.ts'],
    // `yarn add -D tsconfig-paths-webpack-plugin` if you need path aliases
    // plugins: [new TsconfigPathsPlugin()],
  },

  // Where the bundled files will be output. Not strictly necessary with 
  // Serverless Webpack.
  // https://webpack.js.org/configuration/output/
  output: {
    libraryTarget: 'commonjs2',
    path: path.join(__dirname, '.webpack'),
    filename: '[name].js',
  },

  // Anything that will be available to the bundled code in the runtime 
  // environment and does not need to be included in any of the bundles.
  // 
  // In AWS Lambda, the `aws-sdk` is available and we almost certainly want to 
  // exclude it from our bundle(s). Similarly, because it's a Node lambda, 
  // Node's native modules will also be available. 
  externals: ['aws-sdk', nodeExternals()],
  module: {
    // Instruct Webpack to use the `ts-loader` for any TypeScript files, else it
    // won't know what to do with them. 
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        loader: 'ts-loader',
        exclude: [
          [
            path.resolve(__dirname, '.webpack'),
            path.resolve(__dirname, '.serverless'),
          ],
        ],
        // And here we have options for ts-loader
        // https://www.npmjs.com/package/ts-loader#options
        options: {
          // Disable type checking, this will lead to improved build times
          transpileOnly: true,
          // Enable file caching, can be quite useful when running offline
          experimentalFileCaching: true,
        },
      },
    ],
  },
  // We still want type checking, just without the burden on build performance, 
  // so we use a plugin to take care of it on another thread.
  plugins: [new ForkTsCheckerWebpackPlugin()],
};

Results

In the most recent, most outrageous case, I was able to reduce a deploy package’s size from over 50MB to 725kb.

The former was using the default serverless bundler with serverless-plugin-typescript, the latter with an optimized tsconfig and webpack.

I wouldn’t call the final result production ready as it didn’t include sourcemaps, it was (by that stage) an exercise in seeing how low can it go.

Issues

The only issue I’ve run into, so far, is when using serverless-webpack in conjunction with serverless-offline. Periodically, it will run out of memory and crash.

You can increase the amount of memory available to Node but it’s only a bandaid. This has been an ongoing issue for some time but it’s hardly critical.