How V8 Code Cache Supercharges Node.js Startup Times

This article explains the background, implementation, and practical usage of V8's code cache in browsers and Node.js, showing how serialized bytecode reduces JIT compilation overhead, speeds up startup, and benefits serverless functions.

Node Underground
Node Underground
Node Underground
How V8 Code Cache Supercharges Node.js Startup Times

Background

V8 uses JIT to parse JavaScript, but before executing code it must spend considerable time parsing and compiling, which slows overall execution. In 2015 V8 introduced a code cache solution to address this issue.

What is Code Cache

In simple terms, after the first compilation of JavaScript, the generated code is serialized and stored on disk; subsequent loads read the cached code, deserialize it, and execute it, avoiding the repeated compilation overhead. This is achieved via several V8 methods and can be invoked in Node.js through vm.Script.

v8::ScriptCompiler::kProduceCodeCache // generate code cache
v8::ScriptCompiler::Source::GetCachedData // retrieve code cache
v8::ScriptCompiler::kConsumeCodeCache // consume generated code cache

Usage Scenarios

Browser

Browsers typically cache resources in two ways: memory cache, where compiled code resides in memory and can be reused within the same V8 instance, and code cache, which persists compiled code to disk. Disk‑based code cache can be shared across multiple V8 instances but occupies disk space, so it is used mainly for scripts that are repeatedly accessed over time.

Node.js

The feature was first introduced in Node v5.7.0 and can be used via vm.Script to accelerate instance creation and reduce Node startup time. A common implementation is the v8-compile-cache package, which is used simply by requiring it.

require('v8-compile-cache');

The package checks environment variables, determines cache support, and sets up a cache directory. It creates a FileSystemBlobStore for storing cached data and installs a NativeCompileCache that overrides the module compilation process.

if (!process.env.DISABLE_V8_COMPILE_CACHE && supportsCachedData()) {
  const cacheDir = getCacheDir();
  const prefix = getParentName();
  const blobStore = new FileSystemBlobStore(cacheDir, prefix);

  const nativeCompileCache = new NativeCompileCache();
  nativeCompileCache.setCacheStore(blobStore);
  nativeCompileCache.install();

  process.once('exit', code => {
    if (blobStore.isDirty()) {
      blobStore.save();
    }
    nativeCompileCache.uninstall();
  });
}

The helper function getCacheDir() builds a cache directory path based on the user ID and V8 version, typically under the system temporary directory.

function getCacheDir() {
  const dirname = typeof process.getuid === 'function'
    ? 'v8-compile-cache-' + process.getuid()
    : 'v8-compile-cache';
  const version = typeof process.versions.v8 === 'string'
    ? process.versions.v8
    : typeof process.versions.chakracore === 'string'
      ? 'chakracore-' + process.versions.chakracore
      : 'node-' + process.version;
  const cacheDir = path.join(os.tmpdir(), dirname, version);
  return cacheDir;
}

The NativeCompileCache class overrides Module.prototype._compile, preserving the original compile method and injecting a custom require that resolves modules using the cached data when available.

class NativeCompileCache {
  install() {
    const self = this;
    const hasRequireResolvePaths = typeof require.resolve.paths === 'function';
    this._previousModuleCompile = Module.prototype._compile;
    Module.prototype._compile = function(content, filename) {
      const mod = this;
      function require(id) { return mod.require(id); }
      function resolve(request, options) { return Module._resolveFilename(request, mod, false, options); }
      if (hasRequireResolvePaths) { resolve.paths = function(request) { return Module._resolveLookupPaths(request, mod, true); }; }
      require.resolve = resolve;
      require.main = process.mainModule;
      require.extensions = Module._extensions;
      require.cache = Module._cache;
      const dirname = path.dirname(filename);
      const compiledWrapper = self._moduleCompile(filename, content);
      const args = [mod.exports, require, mod, filename, dirname, process, global];
      return compiledWrapper.apply(mod.exports, args);
    };
  }
  // ... other methods ...
}

The core method _moduleCompile creates a wrapper function, retrieves cached data from the store, creates a vm.Script with produceCachedData: true, and runs it in the current context. If cached data is produced, it is saved back to the store; otherwise, rejected data is removed.

_moduleCompile(filename, content) {
  var wrapper = Module.wrap(content);
  var invalidationKey = crypto.createHash('sha1').update(content, 'utf8').digest('hex');
  var buffer = this._cacheStore.get(filename, invalidationKey);
  var script = new vm.Script(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true,
    cachedData: buffer,
    produceCachedData: true,
  });
  if (script.cachedDataProduced) {
    this._cacheStore.set(filename, invalidationKey, script.cachedData);
  } else if (script.cachedDataRejected) {
    this._cacheStore.delete(filename);
  }
  var compiledWrapper = script.runInThisContext({
    filename: filename,
    lineOffset: 0,
    columnOffset: 0,
    displayErrors: true,
  });
  return compiledWrapper;
}

When the process exits, the cache store checks if there are dirty entries and persists them to disk.

process.once('exit', code => {
  if (blobStore.isDirty()) {
    blobStore.save();
  }
  nativeCompileCache.uninstall();
});

Images illustrate the performance difference between disabled and enabled code cache, showing a significant reduction in load data for files such as babel-core.js and rxjs-bundle.js.

Conclusion

Code cache speeds up instance creation, reducing Node.js startup time, which is crucial in FaaS scenarios where rapid scaling is required. By pre‑compiling code to bytecode during the build phase and packaging it into container images, the compilation overhead is eliminated, leading to faster function cold starts.

backend developmentNode.jsV8code cache
Node Underground
Written by

Node Underground

No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.

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.