Jul 04, 2021

Faster TypeScript Feedback Loops with esbuild

A short feedback loop between writing and running code is critical for seeing how changes apply to a codebase. The longer the time to see results, the more inefficient and cumbersome it becomes to write and test software.

Usually, delays are caused by build tooling, which performs steps like compiling or transpiling code, making it usable by older systems, bundling up all files, minifying everything so it reaches your users faster, and so on.

In the last years, building a codebase for a modern web application has become painfully slow, which has prompted maintainers of popular frameworks like Next.js to focus on build performance equally as much as on introducing new features.

Issues in build time performance haven't been exclusive to frontend applications though: For growing codebases, the TypeScript compiler can become awfully slow, and optimizing and tweaking every possible option can take weeks just to get your application to build faster.

More than a year ago, Figma's Evan Wallace released an extremely fast JavaScript bundler and minifier, esbuild, which supports TypeScript and JSX out of the box.

To complement esbuild, Estrella is a build tool offering file watching, running type-checks, or starting up the application after building.

At Hygraph, we've been using esbuild and Estrella for a couple of services for around 9 months now, and adopting it has completely changed how we build, debug, and run these services.

Previously, we used the TypeScript compiler to build Node.js services handling requests. We also used source maps for debugging, to map the running code back to the TypeScript source, which was essential.

With some optimizations in place, our codebase took around 20 seconds to build. Every time. Adding a minor change such as changing some error message, or adding another case, every modification forced our engineers to wait that time.

Out of curiosity, we implemented a similar build process using esbuild and Estrella, compiling our TypeScript source files to non-bundled JavaScript code, while maintaining important conventions such as CommonJS modules required for Node.js, and backporting certain language features the runtime would not support yet. We did not use bundling as this is relatively uncommon for backend applications where individual files may be easier to reason about.

Compared to the twenty seconds, our builds now take around 500ms, usually less. This is a 40x improvement, which changes everything. While you had to think about what you wanted to add explicitly, then build and wait, then run, you can now get into a flow of constant changes, rebuilding everything as you need. Tests also benefit from rapid build times and can be run much more frequently.

If you want to try out a simple version of configuring Estrella and esbuild, check out the following build script

#!/usr/bin/env node

const globby = require('globby');
const { build } = require('estrella');

globby(`src/**/*.ts`).then(paths =>
    // Compile all TypeScript files in src/**/*.ts
    entry: paths,
    outdir: 'dist/src/',

    // Don't bundle or minify files, so we can read them
    bundle: false,
    minify: false,

    // Include inline sourcemaps for debugging
    sourcemap: 'inline',
    sourcesContent: true,

    // Only perform type-checks when TSC_CHECK=true
    tslint: process.env.TSC_CHECK === 'true' ? 'on' : 'off',

    // Set platform to build for Node.js
    platform: 'node',

    // Target Node.js v14 feature set
    target: 'node14',

    // Make sure to always export CommonJS for Node.js
    format: 'cjs'

Note that we conditionally enable type checks, when we want to make sure everything is alright in terms of type-safety. This is especially important for production builds, but it uses the regular TypeScript compiler, so all improvements are undone for these builds.

With this configuration, debugging our services was straightforward as well, as both VS Code and IntelliJ-based IDEs support debugging the built entry points while using source maps to enhance stack traces and allowing to place breakpoints in the TypeScript source rather than the generated output.

Of course, even without using esbuild, there are ways to speed up the TypeScript compiler, including incremental builds, which unfortunately won't help in CI without caches and configuration. When comparing the magnitude of improvement, nothing comes close to esbuild though.

It's safe to say that decreasing time to feedback has been a game-changing enhancement for our TypeScript-based services and we're closely following the development of build tools in the ecosystem to improve our developer experience (DX) even further.

I hope you enjoyed this post! If I got you interested in enhancing DX and working with my team on building the layer that helps teams manage and deliver content across the globe, check out our careers page, we're hiring!