On this page
1. Cap Terser to 2 parallel workers2. SkipForkTsCheckerWebpackPlugin3. Persistent filesystem webpack cacheThe three build optimizations
All three optimizations live in packages/clinic-web/craco.config.js, inside the if (isProduction && process.env.SANDBOX_BUILD === 'true') block added by this PR (starting around line 99). craco.config.js is a config-override file: clinic-web is built with Create React App (CRA), and CRACO lets the project reach into CRA's otherwise-hidden webpack config and modify it before the build runs.
1. Cap Terser to 2 parallel workers
Business view
Every production build of the clinic-web app runs its final JavaScript through a minifier (a tool that shrinks code for faster page loads) called Terser. Minifying is CPU-intensive, so Terser normally splits the work across as many worker processes as the machine has CPU cores — on the shared sandbox build host, that's 32. Each of those 32 workers needs its own copy of the (large) app's code in memory to do its job, so spinning up all 32 at once was one of the biggest drivers of the memory spike that was crashing the build host. Limiting sandbox builds to 2 workers keeps the CPU busy without that 32-way memory multiplication — at the cost of the minify step taking roughly 20–30 seconds longer.
Technical view
webpackConfig.optimization.minimizer.forEach(plugin => {
if (plugin.constructor && plugin.constructor.name === 'TerserPlugin') {
plugin.options.parallel = 2
}
})
CRA's production webpack config already includes TerserPlugin as one of optimization.minimizer's entries. This code doesn't add a new plugin — it iterates the existing minimizer list, identifies TerserPlugin by its constructor name, and mutates its options.parallel in place from the library default (os.cpus().length) down to 2. This only affects the sandbox path; the production release scripts (build:prod, build:with-sourcemaps in packages/clinic-web/package.json) never set SANDBOX_BUILD, so they keep using Terser's default worker count.
2. Skip ForkTsCheckerWebpackPlugin
Business view
CRA's TypeScript template runs a full project-wide type check as part of every build, in a separate background process, so that type errors can fail the build the same way a syntax error would. That type-check is thorough but expensive (~500–800 MB of memory) and — for a throwaway sandbox preview whose only job is to show what a branch looks like — largely redundant, since a developer already sees type errors in their editor and local dev server before they ever push a branch. This PR removes that check from sandbox builds only. The trade-off is explicit: if a branch has a type error, a sandbox preview build could still succeed even though a real production build would have caught it. The PR description flags this and proposes a follow-up: a dedicated CI workflow that runs the type check separately, so skipping it here becomes zero-risk instead of a coverage gap.
Technical view
webpackConfig.plugins = webpackConfig.plugins.filter(plugin =>
!(plugin.constructor && plugin.constructor.name === 'ForkTsCheckerWebpackPlugin')
)
Rebuilds webpackConfig.plugins with ForkTsCheckerWebpackPlugin filtered out by constructor name, rather than modifying its options — the plugin is removed from the build entirely for sandbox builds, so it never spawns its sidecar process at all. Note this is a coverage gap, not just a performance trade: until the follow-up CI typecheck workflow mentioned in the PR exists, a sandbox preview build succeeding is not proof that tsc would also pass.
3. Persistent filesystem webpack cache
Business view
By default, webpack starts every build from scratch, re-processing every file even if almost nothing changed since the last build. This PR turns on webpack's built-in filesystem cache, pointed at a fixed directory (/tmp/webpack-cache) that the sandbox's Docker build already preserves between builds via a BuildKit cache mount. The practical effect: the first sandbox build of a given day starts cold like always, but subsequent builds — of that branch, or of a different branch that shares most of the same source files — can reuse previously-compiled work instead of redoing it, making warm rebuilds an estimated 3–5× faster.
Technical view
webpackConfig.cache = {
type: 'filesystem',
cacheDirectory: '/tmp/webpack-cache',
compression: 'gzip'
}
Overwrites webpack's cache config from CRA's default (type: 'memory', which is process-scoped and gone the instant the build process exits) to a filesystem cache at a fixed path. That path is deliberately the same across every branch — the cache is shared, not per-branch — which only works safely because webpack keys individual cache entries by a content hash, so two branches touching different files don't collide even though they write into the same directory. Since sandbox branches typically share roughly 80% of their source with main, a shared cache gets far more hits than a fresh per-branch cache would. The corresponding BuildKit mount that makes this durable across separate docker build invocations lives outside this PR, in the sandbox Dockerfile:
RUN --mount=type=cache,target=/tmp/webpack-cache,id=cra-webpack-dentolize \
yarn workspace @dentolize/clinic-web run build
(Dockerfile:298-299). Without that mount, setting cache.cacheDirectory alone would still write to /tmp/webpack-cache inside the build container, but the directory — and every byte of caching benefit — would be discarded the moment the container that produced it was removed.