Node.js recently introduced an experimental feature to generate run-time user-land (user scripts) snapshots in v18.8.0. In this post, we’ll look at the importance of this feature, and some of the options it provides. We’ll also compare this snapshot feature to other packaging solutions, such as pkg.
To jump ahead:
- Understanding Node.js startup
- Node.js flags
- The
--build-snapshot
flag - The
--snapshot-blob
flag
- The
- Building a snapshot
- Running a snapshot blob
- An alternative to using a separate entry script
--snapshot-blob
and--build-snapshot
vs.--node-snapshot-main
- Node.js snapshot feature vs. other packaging solutions
Understanding Node.js startup
To understand the need for generating run-time user-land snapshots, we need to understand the way Node.js starts up.
Node.js builds a v8::Isolate
, v8::context
, and node::Environment
at startup. It then constructs a process
object and launches bootstrap Node.js to prepare the environment. Node.js only executes user script once all of this is complete.
The new snapshot flags feature enables the Node executable to create a single binary that contains both Node.js and an embedded snapshot without building Node.js from the source. This means that the binary file already contains Node, so there is no need for another initialization (such as creating the v8::Isolate
, v8::context
, and all the other processes usually required to start a user script), which would have increased the start-up time of the scripts.
Node.js flags
To enable the Node.js executable to achieve this feature, a couple of new flags were introduced: the --snapshot-blob
and --build-snapshot
flags. In this section, we’ll see how to use these new flags.
The --build-snapshot
flag
The --build-snapshot
flag tells Node.js to build a snapshot of the file supplied as an argument to the flag:
--build-snapshot snapshot.js
Snapshot.js serves as the entry point script.
The --snapshot-blob
flag
The --snapshot-blob
flag allows us to tell the Node.js executable file what to save the snapshot blob to. If the snapshot blob file exists, Node simply overrides its content with the new blob, and if it is non-existent, Node creates a new blob file and saves it to the disk in the current working directory:
--snapshot-blob snapshot.blob
snapshot.blob
serves as the name of the binary file where the generated blob is saved.
Now that the function of these flags is understood, we can attempt to build a snapshot of our own.
Building a snapshot
- Open up a terminal and create a
snapshot_test
folder - Open the
snapshot_test
folder in your favorite code editor and initialize npm usingnpm init -y
- Create a file named
snapshot.js
- Copy the following lines of code into
snapshot.js
:const path = require('path') console.log(process.cwd()) globalThis.path = process.cwd() globalThis.file = __dirname const name = 'I am geezy' console.log(process.argv) globalThis.firstArg = process.argv[2] globalThis.secondArg = process.argv[3]
This is a simple script that sets a couple of global
variables using the globalThis
. The globalThis
provides us with a way to access global variables(global object)
.
To build a snapshot of this script along with its current Node.js run-time environment, run the following command:
node --snapshot-blob snapshot.blob --build-snapshot snapshot.js name home
The extra name
and home
arguments given to the commands are available to us through process.argv
.
This is the output:
/home/phantom/Documents/node_js_projects/node_testing [ '/home/phantom/.nvm/versions/node/v18.9.1/bin/node', '/home/phantom/Documents/node_js_projects/node_testing/snapshot.js', 'name', 'home' ]
Node.js executes Snapshot.js as usual, then it creates a snapshot of the script’s state.
Inspecting the current working directory, we find the snapshot.blob
file that Node.js generated. When we open up the file, we see gibberish:
This prompts the question: How do we execute the generated blob?
Running a snapshot blob
This new feature makes it easy to run a snapshot blob — all we need is to create an entry file for our snapshot.blob
file. This file will attempt to read from the Global
Object
.
Create an index.js
file in the snapshot_test
directory, and add the following lines of code to it:
console.log('current working directory', globalThis.path) console.log('First Arg', globalThis.firstArg) console.log('Second Argument', globalThis.secondArg) console.log('current process Argv', process.argv) console.log('Global Object', globalThis)
Then, run the following command:
node --snapshot-blob snapshot.blob index.js
This is the output:
current working directory /home/phantom/Documents/node_js_projects/node_testing First Arg name Second Argument home current process Argv [ '/home/phantom/.nvm/versions/node/v18.9.1/bin/node', '/home/phantom/Documents/node_js_projects/node_testing/index.js' ] Global Object <ref *1> Object [global] { global: [Circular *1], queueMicrotask: [Function: queueMicrotask], clearImmediate: [Function: clearImmediate], setImmediate: [Function: setImmediate] { [Symbol(nodejs.util.promisify.custom)]: [Getter] }, structuredClone: [Function: structuredClone], clearInterval: [Function: clearInterval], clearTimeout: [Function: clearTimeout], setInterval: [Function: setInterval], setTimeout: [Function: setTimeout] { [Symbol(nodejs.util.promisify.custom)]: [Getter] }, atob: [Function: atob], btoa: [Function: btoa], performance: Performance { nodeTiming: PerformanceNodeTiming { name: 'node', entryType: 'node', startTime: 0, duration: 99.07323400303721, nodeStart: 3.987049002200365, v8Start: 31.41652700304985, bootstrapComplete: 89.83720200136304, environment: 66.75902900099754, loopStart: -808954.4995539971, loopExit: -808949.1088810004, idleTime: 0 }, timeOrigin: 1665479964732.965 }, fetch: [AsyncFunction: fetch], path: '/home/phantom/Documents/node_js_projects/node_testing', file: '/home/phantom/Documents/node_js_projects/node_testing', firstArg: 'name', secondArg: 'home' }
Although we didn’t run the snapshot.js
file, by running the blob file with an entry point, the globalThis.path
, globalThis.firstArg
, and globalThis.secondArg
variables are assigned values as though we ran the snapshot.js
file. This goes to prove that the state of our application is captured in the snapshot.blob
file.
To know if the snapshot.blob
file is being run, we can attempt to run the index.js
file without specifying a blob.
Run the following command:
node index.js
This is the output:
current working directory undefined First Arg undefined Second Argument undefined current process Argv [ '/home/phantom/.nvm/versions/node/v18.9.1/bin/node', '/home/phantom/Documents/node_js_projects/node_testing/index.js' ] Global Object <ref *1> Object [global] { global: [Circular *1], queueMicrotask: [Function: queueMicrotask], clearImmediate: [Function: clearImmediate], setImmediate: [Function: setImmediate] { [Symbol(nodejs.util.promisify.custom)]: [Getter] }, structuredClone: [Function: structuredClone], clearInterval: [Function: clearInterval], clearTimeout: [Function: clearTimeout], setInterval: [Function: setInterval], setTimeout: [Function: setTimeout] { [Symbol(nodejs.util.promisify.custom)]: [Getter] }, atob: [Function: atob], btoa: [Function: btoa], performance: Performance { nodeTiming: PerformanceNodeTiming { name: 'node', entryType: 'node', startTime: 0, duration: 104.0962289981544, nodeStart: 15.742054000496864, v8Start: 21.813469998538494, bootstrapComplete: 88.35036600008607, environment: 66.38047299906611, loopStart: -1, loopExit: -1, idleTime: 0 }, timeOrigin: 1665480382060.152 }, fetch: [AsyncFunction: fetch] }
Inspecting both outputs, we notice some differences:
- In the second instance, the
globalThis.path
,globalThis.firstArg
, andglobalThis.secondArg
values are undefined, because those values are only set whensnapshot.js
is run - The
Global
Object
does not contain the extra key-value pairs that we initialized in thesnapshot.js
file
Alternative to using a separate entry script
We can restore our application state without the use of an entry script by using the v8.startupSnapshot
API to specify an entry point as the snapshot is being built.
In the current directory, create a second_snapshot.js
file and add the following lines of code:
const path = require('path') console.log(process.cwd()) globalThis.path = process.cwd() globalThis.file = __dirname const name = 'I am geezy' console.log(process.argv) globalThis.firstArg = process.argv[2] globalThis.secondArg = process.argv[3] require('v8').startupSnapshot.setDeserializeMainFunction(() => { console.log('firstArg', this.firstArg) console.log('secondArg', this.secondArg) console.log('I am from the second snapshot') })
Build the snapshot blob using this command:
node --snapshot-blob second_snapshot.blob --build-snapshot second_snapshot.js name home
To restore the script state from second_snapshot.blob
, run the following command:
node --snapshot-blob second_snapshot.blob
This is the output:
firstArg name secondArg home I am from the second snapshot
Notice how we didn’t have to specify an entry script when trying to restore our application state.
--snapshot-blob
and --build-snapshot
vs. --node-snapshot-main
These new flags allow for run-time snapshots, but the ability to take snapshots has existed since node v18.0.0 by using the --node-snapshot-main
flag. However, this flag only supports build-time snapshots. It also requires building Node from the source, which is not user friendly and takes a considerable amount of time depending on the host machine.
To understand the difference in performance when running run-time snapshots and build-time snapshots, let’s look at the metrics from the author of both features:
Looking at the metrics above, it’s easy to see that the run-time snapshot (--snapshot-blob
) version, which performs 19 runs, outperforms the build-time snapshot (11 runs) while taking much less time.
Support | --snapshot-blob and --build-snapshot |
--node-snapshot-main |
---|---|---|
Run-time snapshots | Yes | No |
Uses configure script | No | Yes |
User-land modules | No | No |
Requires separate startup script | Not necessary | Yes |
Building Node from source | No | Yes |
Build-time snapshots | No | Yes |
Node.js snapshot feature vs. other packaging solutions
Using packaging solutions (such as pkg), the app source can be bundled into a binary. But in order to launch the app once the binary is loaded, you still need to parse the source.
On the other hand, using the Node.js snapshot, the heap state that was initialized by the code is included in the binary, negating the requirement to run the initialization code during load time.
Conclusion
The Node.js snapshot feature is highly experimental and limited at the time of writing this article, but more features will be added as time goes on. The feature is a promising prospect for the Node.js community and hopefully you have a better understanding of the topic after reading this article.
The post Introduction to Snapshot flags in Node.js v18.8.0 appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/6GdfsLV
Gain $200 in a week
via Read more