Module A-18·11 min read

Compiling Node.js systems to SEA, v8.startupSnapshot for near-zero cold starts, and secure asset bundling via sea.getAsset().

Module 17 — Distribution & Cold Starts: Single Executable Applications & V8 Snapshots

What this module covers: Distributing a Node.js application traditionally requires the target machine to have Node.js installed, the correct version, and all dependencies. Single Executable Applications (SEA) solve this by compiling your entire application — runtime, modules, and assets — into one self-contained binary. V8 startup snapshots solve the remaining latency: by pre-serializing the initialized V8 heap at build time, a snapshot eliminates module loading time entirely and starts your application in microseconds. This module covers both techniques, their production use cases, and the security implications of bundling assets in an executable.


Single Executable Applications (SEA)

Available in Node.js 20+, SEA bundles the Node.js runtime and your application into a single executable binary. No Node.js installation required on the target machine.

Building a SEA

bash
# Step 1: Build your application tsc --outDir dist # dist/app.js — your bundled application # Step 2: Create SEA configuration cat > sea-config.json << 'EOF' { "main": "dist/app.js", "output": "sea-prep.blob", "disableExperimentalSEAWarning": true, "assets": { "schema.json": "dist/schema.json", "config.yaml": "dist/config.yaml" } } EOF # Step 3: Generate the SEA blob node --experimental-sea-config sea-config.json # Step 4: Copy the Node.js binary and inject the blob cp $(which node) blockchain-indexer # or .exe on Windows # Linux/macOS: inject using postject npx postject blockchain-indexer NODE_SEA_BLOB sea-prep.blob \ --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 # Step 5: Sign (macOS only — required for execution) codesign --sign - blockchain-indexer # self-sign for development # Result: single executable, no Node.js dependency ls -lh blockchain-indexer # -rwxr-xr-x 1 user staff 102M blockchain-indexer

Reading Bundled Assets in a SEA

javascript
import { getAsset, getRawAsset } from 'node:sea'; import { isSea } from 'node:sea'; // Read a bundled config file function loadConfig() { if (isSea()) { // Running as SEA — read from bundled asset const configBytes = getRawAsset('config.yaml'); // returns ArrayBuffer return parseYaml(Buffer.from(configBytes).toString('utf8')); } else { // Running as normal Node.js — read from filesystem return parseYaml(fs.readFileSync('dist/config.yaml', 'utf8')); } } // Read JSON schema for validation function loadSchema() { if (isSea()) { return getAsset('schema.json', 'utf8'); // returns string directly } return fs.readFileSync('dist/schema.json', 'utf8'); }

SEA Use Cases

Blockchain node operator distribution: deploy your indexer to node operators who don't have Node.js installed. Ship one binary, one config file. No npm install, no version mismatch, no node_modules folder.

CI/CD artifact size reduction: a single 100MB binary is faster to push/pull from container registries than a Docker image with Node.js + application + node_modules.

Air-gapped environments: financial infrastructure that cannot access the internet during deployment. Ship a single signed binary.


V8 Startup Snapshots: Near-Zero Cold Start

When Node.js starts, it initializes the V8 engine and loads your modules. For a large application with dozens of imports, this initialization takes 100–400ms. A V8 startup snapshot pre-serializes the initialized heap state at build time. At startup, V8 deserializes the snapshot instead of re-executing initialization code — typically 10–50× faster.

How Snapshots Work

Build time:
  1. Run initialization code (import modules, set up schemas, etc.)
  2. V8 serializes the current heap state to a binary blob
  3. The blob is embedded in the SEA binary

Runtime:
  1. V8 deserializes the pre-built heap (microseconds)
  2. Your request handler runs immediately
  3. All pre-initialized objects are available without re-execution

Creating a Startup Snapshot

javascript
// snapshot-entry.js — runs at BUILD TIME to initialize the heap import { setDeserializeMainFunction } from 'v8'; import { compileFunction } from 'node:vm'; // Initialize expensive resources at BUILD TIME import Ajv from 'ajv'; const ajv = new Ajv({ allErrors: true }); // Pre-compile all schemas at build time import transactionSchema from './schemas/transaction.json' assert { type: 'json' }; import paymentSchema from './schemas/payment.json' assert { type: 'json' }; const validateTransaction = ajv.compile(transactionSchema); const validatePayment = ajv.compile(paymentSchema); // This function runs at RUNTIME when the snapshot is deserialized setDeserializeMainFunction(async () => { // validateTransaction and validatePayment are already compiled — // available immediately from the deserialized heap const { default: Fastify } = await import('fastify'); const fastify = Fastify({ logger: true }); fastify.post('/api/v2/payments', async (request, reply) => { // Uses pre-compiled validator from snapshot — no compilation at request time if (!validatePayment(request.body)) { return reply.code(400).send({ errors: validatePayment.errors }); } return await processPayment(request.body); }); await fastify.listen({ port: 3000 }); });
bash
# Build the snapshot node --snapshot-blob snapshot.blob --build-snapshot snapshot-entry.js # Run with snapshot (skips all initialization) node --snapshot-blob snapshot.blob # Application ready in <10ms instead of 400ms

What Can and Cannot Be Snapshotted

Can be snapshotted (serializable V8 heap objects):

  • Compiled ajv validator functions
  • Pre-parsed JSON schemas and configurations
  • Lookup tables and pre-computed maps
  • Module-level initialized classes

Cannot be snapshotted:

  • File descriptors (TCP sockets, file handles) — OS resources don't survive serialization
  • Promises in progress — async state cannot be serialized
  • Native addon instances — C++ objects outside V8 heap
  • Crypto keys created at runtime — security-sensitive, not serializable

SEA + Snapshot: The Complete Pipeline

For a blockchain indexer, combine both techniques:

javascript
// build-snapshot.js — runs at build time import { setDeserializeMainFunction } from 'v8'; import Ajv from 'ajv'; import blockSchema from './schemas/block.json' assert { type: 'json' }; import txSchema from './schemas/transaction.json' assert { type: 'json' }; // Compile validators AT BUILD TIME const ajv = new Ajv({ allErrors: true }); const validateBlock = ajv.compile(blockSchema); const validateTransaction = ajv.compile(txSchema); // Pre-build route Radix tree for Fastify import Fastify from 'fastify'; const fastify = Fastify({ logger: false }); // Register routes at build time — Radix tree built once fastify.post('/api/v1/ingest', { schema: { body: txSchema } }, txHandler); fastify.post('/api/v1/blocks', { schema: { body: blockSchema } }, blockHandler); setDeserializeMainFunction(async () => { // At startup: validators ready, routes compiled, Radix tree built // Connect to database (cannot snapshot TCP connections) await pool.connect(); await fastify.listen({ port: 3000 }); console.log('Ready in < 20ms'); });
Traditional startup timeline:
  0ms   → Node.js runtime starts
  50ms  → Import Fastify, Ajv, pg, kafkajs...
  150ms → Compile JSON schemas
  250ms → Build Radix tree from route definitions
  320ms → Connect to database
  400ms → Server ready

With SEA + snapshot:
  0ms  → Deserialize snapshot (V8 heap, compiled validators, Radix tree)
  15ms → Connect to database (not snapshotted)
  20ms → Server ready

Summary

ConceptKey Takeaway
SEASingle binary with Node.js runtime embedded. No Node.js required on target. ~100MB binary.
getAsset() / getRawAsset()Read files bundled into SEA. Returns string or ArrayBuffer.
isSea()Check if running as SEA. Use different file-loading paths for SEA vs development.
V8 startup snapshotPre-serializes initialized heap at build time. Deserializes at startup in microseconds.
setDeserializeMainFunctionDefines what runs at startup after snapshot deserialization.
What's snapshottedCompiled validators, pre-parsed configs, lookup tables. Not: sockets, promises, native addons.
Build pipelinenode --build-snapshot → blob embedded in SEA → 20ms startup vs 400ms.

© 2026 Jatin Jain Saraf (JJS). All rights reserved.