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;Β
- Monorepo setup using Lerna
- Using Typescript, Node, and Express
- Gulp
- Apollo Federation
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
Stats of the build, without esBuild:
[19:46:41] Starting 'graph-accounts:compile'...
[19:46:45] Finished 'graph-accounts:compile' after 4.07s
// 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`));
[19:46:45] Starting 'graph-accounts:binary'...
[19:47:02] Finished 'graph-accounts:binary' after 17s
npx @vercel/ncc build ./packages/graph-accounts/lib/index.js -o ./build/graph-accounts/bin/
// 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" }
);
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
Esbuild Setup
First, let start by installing esbuild
as a dev dependency:
yarn add -D esbuild
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
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'
}
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;
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()
})
}
}
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
// bundle.esbuild.js
spawnSync(
'cp',
[
'-a',
`./packages/${projectName}/lib/.`,
`./build/${projectName}/bin/`,
], { stdio: 'inherit' },
)
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.
Top comments (0)