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.
- TypeScript: 4.4.3
- Serverless Framework: 2.60.0
- Serverless Webpack: 5.5.4
- Serverless Plugin TypeScript: 2.1.0
- Serverless Offline: 8.2.0
- Webpack: 5.54.0
- Webpack Node Modules externals: 3.0.0
- ts-loader: 9.2.6
- Fork TS Checker Webpack Plugin: 6.3.3
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.