How to Speed Up Frontend Build Times with Native Pre‑Compilation and Caching

By leveraging native code tools like esbuild, caching strategies, and a custom pre‑compilation workflow that fakes Webpack's DllPlugin output, this article shows how Mars framework teams dramatically cut compilation times—up to 40% faster—while handling module resolution, resource handling, and cross‑platform challenges.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
How to Speed Up Frontend Build Times with Native Pre‑Compilation and Caching

Introduction

Improving compilation efficiency is an unavoidable topic in frontend infrastructure. Since the Mars middle‑backend framework was introduced at Guming, the team has been actively exploring effective compilation acceleration solutions.

Exploration Directions

Generally, frontend compilation speed can be improved from three angles, known as the “three axes of compilation acceleration”:

Cache : cache compilation results (memory or persistent) so subsequent builds can skip recompilation. Babel‑loader and webpack both support caching.

Native code : use compiled languages such as C/C++, Rust, or Go for the compiler itself (e.g., swc, esbuild, rspack) to achieve higher CPU efficiency.

Less compilation : adopt on‑demand compilation strategies, exemplified by Vite.

This article focuses on the exploration of pre‑compiling dependencies using native code.

Pre‑compilation Scheme

The implementation draws inspiration from Taro's dependency pre‑compilation and UmiJS MFSU, with additional optimizations for the Mars scenario.

1. Scan Dependencies

Use esbuild to compile the project source and a custom esbuild plugin to collect all modules under node_modules. Example plugin code:

import esbuild from 'esbuild';

const deps = new Set<string>();

// Scan plugin
export function getScanImportsPlugin() {
  return {
    name: 'ScanImportsPlugin',
    setup(build) {
      // Resolve npm package paths
      build.onResolve({ filter: /^[^./\\][^:]/ }, async (args) => {
        const { path: id, resolveDir } = args;
        const filePath = require.resolve(id, { paths: [resolveDir] });
        if (filePath.includes('node_modules')) {
          deps.add(id);
          return { external: true };
        }
        return { path: filePath };
      });

      // Handle static assets
      build.onResolve({ filter: /\.(css|less|sass|scss|png|jpg|jpeg|gif|svg|json)$/i }, () => {
        return { external: true };
      });
    },
  };
}

The simple require.resolve() approach faces issues when dealing with custom alias configurations or fields like module, main, browser, exports in a package’s package.json. To align with webpack’s resolution, the team uses enhanced-resolve.

2. Compile Dependencies

After scanning, esbuild compiles the collected dependencies. The key challenge is merging pre‑compiled output with normal webpack builds. Three possible approaches were evaluated:

Use webpack externals to exclude pre‑compiled modules and expose them on window. This is intrusive and pollutes the global scope.

Bridge with webpack 5 Module Federation (as Taro does). This requires a custom federation plugin and an extra webpack build of the pre‑compiled bundle, reducing efficiency.

Bridge with DllReferencePlugin and DllPlugin. The team chose this but avoided using DllPlugin to build the bundle; instead, esbuild directly produces the output, dramatically improving build speed.

3. Fake DllPlugin Output

By mimicking the files generated by DllPlugin —a dll.js bundle and a manifest.json —the workflow can reuse webpack’s DllReference mechanism without the extra build step.

Example dll.js (simplified) and manifest.json structures are shown, where the manifest maps module paths to IDs.

4. Pre‑compilation Cache

The only factor influencing the pre‑compiled artifact is the set of scanned dependencies. A cache‑hash strategy was introduced:

Sort scanned modules to eliminate order‑dependent differences.

Append each module’s name and version (or file content if unavailable) to the hash source.

Include other variables such as environment, compiler version, and config files.

Generate an MD5 digest as the cache key and store it in manifest.json. If the hash matches on a subsequent run, the pre‑compiled bundle can be reused.

5. Adapt to Webpack Build

Two steps are required:

Configure DllReferencePlugin with the generated manifest.json to exclude pre‑compiled modules from webpack’s compilation.

Emit the generated vendor.js to the output directory and ensure it loads before other scripts (e.g., via add-asset-html-webpack-plugin).

Q&A Highlights

How to handle CSS, images, and other assets in dependencies?

Since the pre‑compiled output only contains vendor.js, resources are recorded as externals in manifest.json and loaded via a global host object injected by webpack.

How to support custom webpack externals configuration?

The solution only supports global‑variable externals, with a sample plugin that generates runtime code mapping to the global variable.

How to avoid duplicate @babel/runtime bundles?

In development, duplication is ignored; in production, the runtime is treated as a resource module and processed by webpack.

How to debug modules under node_modules ?

On macOS, a chokidar watcher re‑triggers pre‑compilation on changes; on Windows, a manual trigger via console input is used to avoid high memory consumption.

Optimization Results

Benchmarks on a 2018 MacBook Pro (i7) show significant speedups:

Small project (18 pages): ordinary compile 26.5 s → with cache 5.9 s; with pre‑compilation 14.9 s → 3.1 s.

Large project (108 pages): ordinary compile 95.8 s → with cache 9.7 s; with pre‑compilation 60.3 s → 5.6 s.

Note: The post‑pre‑compilation time equals pre‑compilation time plus webpack compilation time.

Enabling cache yields a massive boost, and pre‑compilation improves overall compile speed by roughly 40%, with the pre‑compilation step itself taking only 2–4 seconds.

Conclusion

The article presents Guming’s pre‑compilation solution for the Mars framework, acknowledging inspiration from Taro. Combined with route‑level on‑demand compilation, first‑time compile time for large projects dropped from 60 seconds to about 20 seconds.

frontendbuild optimizationWebpackesbuildprecompilation
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.