DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 964,423 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Compiling a Apollo Federated Graph with esBuildΒ πŸš€
Henry Arbolaez
Henry Arbolaez

Posted on

Compiling a Apollo Federated Graph with esBuildΒ πŸš€

Introduction

At Course Hero, we are starting to build our Apollo Federated Graph Services. For our local environment, we use Kubernetes to deploy our code. Keeping the advantages/disadvantages on the side, when it comes to building our local code it's going to take time due that we need to bundle the binary and sync it to K8 to be able to see it.

Our goal is to bundle and ship that local code as soon as possible to reduce the waiting time. Saving seconds here are goals here.

Below we're going go into details about how we were able to save around ~21 seconds when it comes to building the app binary, with esbuild πŸš€

EsBuild build: Done in around 313ms
Webpack build: Done in around 21.07sec

Current Setup

To give a background on the current setup of the project;Β 

Current Local Build Process

The current process of building a package locally is by running through a gulp task, using ttypescript to compile the TS and @vercel/ncc to build the binary:

npx gulp graph-accounts:local
Enter fullscreen mode Exit fullscreen mode

Stats of the build, without esBuild:

[19:46:41] Starting 'graph-accounts:compile'...
[19:46:45] Finished 'graph-accounts:compile' after 4.07s
Enter fullscreen mode Exit fullscreen mode
// ttypescript.gulp.compile.js

const project = ts.createProject(`packages/${projectName}/tsconfig.json`, {
  typescript: require("ttypescript"),
});

return project
  .src()
  .pipe(project())
  .pipe(gulp.dest(`packages/${projectName}/lib`));
Enter fullscreen mode Exit fullscreen mode
[19:46:45] Starting 'graph-accounts:binary'...
[19:47:02] Finished 'graph-accounts:binary' after 17s
Enter fullscreen mode Exit fullscreen mode
npx @vercel/ncc build ./packages/graph-accounts/lib/index.js -o ./build/graph-accounts/bin/
Enter fullscreen mode Exit fullscreen mode
// binary.gulp.non-esbuil.js

const { spawnSync } = require("child_process");

const result = spawnSync(
  "npx",
  [
    "@zeit/ncc",
    "build",
    `./packages/${projectName}/lib/index.js`,
    "-o",
    `./build/${projectName}/bin/`,
  ],
  { stdio: "inherit" }
);
Enter fullscreen mode Exit fullscreen mode

The total time spent in the compile and binary tasks were around 21.07sec.

Bundling with Esbuild

With the esbuild, we were able to reduce time on the compile and binary tasks to a stunning 313ms that is a 20.7sec πŸš€ reduction.

Below are the stats for the two tasks, but before we go into details let's see how our esbuild is setups.

[19:53:10] Starting 'graph-accounts:compile'...
[19:53:10] Finished 'graph-accounts:compile' after 289 ms
[19:53:10] Starting 'graph-accounts:binary'...
[19:53:10] Finished 'graph-accounts:binary' after 24 ms
Enter fullscreen mode Exit fullscreen mode

Esbuild Setup

First, let start by installing esbuild as a dev dependency:

yarn add -D esbuild 
Enter fullscreen mode Exit fullscreen mode

Below is a sample of our Monorepo folder structure:

graphql-services
β”œβ”€β”€ packages
β”‚   β”œβ”€β”€ graph-accounts
β”‚   β”‚   β”œβ”€β”€ ...
β”‚   β”‚   └── esbuild.config.server.js
β”‚   └── graph-gateway
β”‚       β”œβ”€β”€ ...
β”‚       └── esbuild.config.server.js
β”œβ”€β”€ scripts
β”‚   β”œβ”€β”€ gulp
β”‚   └── esbuild.config.base.js // base configs for esbuild
β”œβ”€β”€ gulpfile.js
β”œβ”€β”€ package.json
β”œβ”€β”€ package.json
└── tsconfig.settings.json
Enter fullscreen mode Exit fullscreen mode

Let dive into the esbuild.config.base.js configs. These are the default base config that we want esbuild to build off. We want to set the format of our build to commonjs and the platform to node . The external property can come in handy when you want to exclude a file or package from the build.

// esbuild.config.base.js

module.exports = {
  external: ['express', 'newrelic'],
  platform: 'node',
  target: 'node16.13.0',
  bundle: true,
  minify: true,
  format: 'cjs'
}
Enter fullscreen mode Exit fullscreen mode

Now that we have the base config that we can extend. Let go over the esbuild.config file for each of the underlying services. One key thing here is how we look up the env variables that we want to send over with the esbuild bundle.

// esbuild.config.server.js 

const path = require('path')
const baseConfig = require('../../scripts/esbuild.config.base')

const define = {}
// lookup all the env in process.env, to be sent to the esbuild bundle
const keys = Object.assign({}, process.env)
for (const k in keys) {
  define[`process.env.${k}`] = JSON.stringify(keys[k])
}

const config = Object.assign({}, baseConfig, {
  entryPoints: [path.resolve(__dirname, 'src/index.ts')],
  outfile: path.resolve(__dirname, 'lib', 'index.js'),
  define,
  // TSConfig, normally esbuild automatically discovers tsconfig.json, but we can specified here
})

module.exports = config;
Enter fullscreen mode Exit fullscreen mode

Our compile gulp task reads the underlying service esbuild.config.server.js to compile the code.

// compile.task.js 

{
  ...
  compile: (projectName) => {
    return new Promise(async (resolve, reject) => {
      const esbuildConfig = require(`../../packages/${projectName}/esbuild.config.server.js`)
      try {
        esbuild.buildSync(esbuildConfig)
      } catch (error) {
        reject()
      }
      resolve()
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we can run npx gulp graph-accounts:compile and πŸŽ‰ πŸŽ‰

[19:53:10] Starting 'graph-accounts:compile'...
[19:53:10] Finished 'graph-accounts:compile' after 289 ms
Enter fullscreen mode Exit fullscreen mode
// bundle.esbuild.js

spawnSync(
  'cp',
  [
    '-a',
    `./packages/${projectName}/lib/.`,
    `./build/${projectName}/bin/`,
  ], { stdio: 'inherit' },
)
Enter fullscreen mode Exit fullscreen mode

Summary

Setting esbuild was very easy and the developer experience that we were able to get was stunning, without adding many dependencies. It saved us a tremendous amount of development time trying to build the apps, so give it a try!

Follow-up; Doing a comparison with Webpack and investigating devspace and telepresence for hot reloading experience between our local K8.

We’re hiring!

Top comments (0)

Need a better mental model for async/await?

Check out this classic DEV post on the subject.

β­οΈπŸŽ€ JavaScript Visualized: Promises & Async/Await

async await