前言:随着Node.js越来越强大,代码量也越来越多,这不可避免地拖慢了Node.js的启动速度。针对这个问题,Node.js社区通过V8快照技术优化了Node.js的启动。github上有很多关于这个的问题讨论。如果你有兴趣,也可以看看。通过快照加速启动是一个非常复杂的过程,需要对V8有深刻的理解。本文介绍如何在Node.js中使用快照来加速Node.js的启动。以v16.13.1为例,社区一直在优化这里的速度,不同版本的速度可能不一样。Node.js默认启用快照。编译后会生成一个node_snapshot.cc文件。它定义了几个方法并保存了快照数据,这些数据将在Node.js启动时使用。我们也可以在编译的时候关闭这个功能,具体执行./configure--without-node-snapshot。除了编译时控制是否生成快照外,还可以控制启动时是否使用快照。默认是使用它,可以通过--no-node-snapshot关闭。让我们看看效果。const{performance}=require('perf_hooks');console.log(performance.nodeTiming.bootstrapComplete-performance.nodeTiming.nodeStart);可以通过perf_hooks模块获取Node.js启动过程的时间,这里我们先不使用快照的情况下看一下时间。具体执行node--no-node-snapshottest.js,在我的电脑上需要42.165621001273394毫秒。然后看使用快照的时间。具体执行nodetest.js,我的电脑是24.800417000427842毫秒,我们看到速度有了很大的提升。接下来我们看一下这部分在Node.js中的大致实现。先看编译配置。['node_use_node_snapshot=="true"',{'dependencies':['node_mksnapshot',],'actions':[{'action_name':'node_mksnapshot','process_outputs_as_sources':1,'inputs':['<(node_mksnapshot_exec)',],'outputs':['<(SHARED_INTERMEDIATE_DIR)/node_snapshot.cc',],'action':['<@(_inputs)','<@(_outputs)',],},],},{'sources':['src/node_snapshot_stub.cc'],}],我们看到这里判断是否根据node_use_node_snapshot生成快照,这个变量是在configure.py中设置的,即--without前面提到过-node-snapshot。如果node_use_node_snapshot为false,则编译node_snapshot_stub.cc。node_snapshot_stub.cc提供了默认实现,因为这些函数在Node.jsC++代码中使用。如果node_use_node_snapshot为真则执行node_mksnapshot.cc并将快照写入文件node_snapshot.cc。接下来我们看看node_mksnapshot.cc是如何生成快照的。intmain(intargc,char*argv[]){argv=uv_setup_args(argc,argv);v8::V8::SetFlagsFromString("--random_seed=42");std::ofstream输出;out.open(argv[1],std::ios::out|std::ios::binary);node::InitializationResult结果=node::InitializeOncePerProcess(argc,argv);{std::stringsnapshot=node::SnapshotBuilder::Generate(result.args,result.exec_args);输出<<快照;关闭();节点::TearDownOncePerProcess();return0;}InitializeOncePerProcess做了一些初始化操作,重点是Generate(直接看底层的Generate)。voidSnapshotBuilder::Generate(SnapshotData*out,conststd::vectorargs,conststd::vectorexec_args){Isolate*isolate=Isolate::Allocate();isolate->SetCaptureStackTraceForUncaughtExceptions(true,10,v8::StackTrace::StackTraceOptions::kDetailed);per_process::v8_platform.Platform()->RegisterIsolate(isolate,uv_default_loop());std::unique_ptrmain_instance;std::字符串结果;{conststd::vector&external_references=NodeMainInstance::CollectExternalReferences();SnapshotCreator创建者(隔离,external_references.data());环境*环境;{main_instance=NodeMainInstance::Create(隔离,uv_default_loop(),per_process::v8_platform.Platform(),args,exec_args);HandleScope范围(隔离);creator.SetDefaultContext(Context::New(isolate));out->isolate_data_indices=main_instance->isolate_data()->Serialize(&creator);本地<上下文>上下文;{TryCatchbootstrapCatch(隔离);上下文=NewContext(隔离);}语境::范围context_scope(context);//创建环境env=newEnvironment(main_instance->isolate_data(),context,args,exec_args,nullptr,node::EnvironmentFlags::kDefaultFlags,{});//在lib/internal/bootstrap/中运行脚本{TryCatchbootstrapCatch(isolate);v8::MaybeLocal结果=env->RunBootstrapping();结果.ToLocalChecked();}//序列化原生状态out->env_info=env->Serialize(&creator);//序列化上下文size_tindex=creator.AddContext(context,{SerializeNodeContextInternalFields,env});}//必须超出HandleScopeout->blob=creator.CreateBlob(SnapshotCreator::FunctionCodeHandling::kClear);}per_process::v8_platform.Platform()->UnregisterIsolate(isolate);}可以看到模拟了Node。js启动过程,然后将相关数据写入快照,最后生成一个文件。该文件类似于之前的默认node_snapshot_stub.cc文件,具有额外的快照数据。现在您有了快照,让我们看看如何使用它。intStart(intargc,char**argv){InitializationResult结果=InitializeOncePerProcess(argc,argv);如果(result.early_return){返回结果.exit_code;}{Isolate::CreateParams参数;conststd::vector*indices=nullptr;constEnvSerializeInfo*env_info=nullptr;//是否使用快照booluse_node_snapshot=per_process::cli_options->per_isolate->node_snapshot;if(use_node_snapshot){//获取快照信息v8::StartupData*blob=NodeMainInstance::GetEmbeddedSnapshotBlob();if(blob!=nullptr){params.snapshot_blob=blob;indices=NodeMainInstance::GetIsolateDataIndices();env_info=NodeMainInstance::GetEnvSerializeInfo();}}uv_loop_configure(uv_default_loop(),UV_METRICS_IDLE_TIME);//通过快照初始化NodeMainInstancemain_instance(¶ms,uv_default_loop(),per_process::v8_platform.Platform()、result.args、result.exec_args、索引);结果.exit_code=main_instance.Run(env_info);}TearDownOncePerProcess();returnresult.exit_code;}start是Node.js启动时执行的函数,上面的代码可以看到,如果开启了快照并生成快照,则通过快照初始化,否则通过正常的初始化过程。下面是IsolateData的初始化逻辑if(indexes==nullptr){CreateProperties();}else{DeserializeProperties(indexes);}我们可以看到如果有snapshot可以反序列化初始化,否则需要被创建。下面以async_wrap_providers_为例,对比一下使用和不使用snapshot进行初始化的代码。以下是未使用的快照。voidIsolateData::CreateProperties(){async_wrap_providers_[AsyncWrap::PROVIDER_##Provider].Set(\isolate_,\String::NewFromOneByte(\isolate_,\reinterpret_cast(#Provider),\NewStringType::kInternalized,\sizeof(#Provider)-1).ToLocalChecked());NODE_ASYNC_PROVIDER_TYPES(V)}以上就是通过宏初始化async_wrap_providers_数组的逻辑。可以看到字符串是使用V8API创建的,然后设置为async_wrap_providers_。接下来看使用快照的初始化逻辑。voidIsolateData::DeserializeProperties(conststd::vector*indexes){size_ti=0;HandleScopehandle_scope(isolate_);对于(size_tj=0;jmaybe_field=isolate_->GetDataFromSnapshotOnce((*indexes)[i++]);本地字段;async_wrap_providers_[j].Set(isolate_,field);}}可以看到可以直接调用GetDataFromSnapshotOnce获取对应的快照信息数据,然后设置为async_wrap_providers_。总结:可以看到通过快照大大加快了Node.js的启动过程。快照技术的思路很简单,就是保存一份,避免每次都重新创建相同的数据,但是实现起来却很复杂。甚至有同学问是否可以随时对进程的当前状态进行快照,这样进程挂掉后可以直接恢复到之前的状态。这听起来不错,但实施起来可能非常复杂。