本文共 5712 字,大约阅读时间需要 19 分钟。
ServiceWorker给前端开发者提供了非常强大的缓存操控能力,灵活的请求拦截能力,和高效的消息推送能力。但我们在使用ServiceWorker相关能力编写PWA应用时,偶尔会发现性能并没有预期的那么好,这里面到底有什么玄机呢?
我们先来看看ServiceWorker的启动流程,把ServiceWorker线程的整个启动流程划分为五大步骤:
步骤一: 进入启动流程。
一般来说,我们在访问一个含有ServiceWorker的页面主文档时,在发起主文档请求之前,它会先派发一个Fetch事件,这个事件会触发该页面ServiceWorker的启动流程。
content::ServiceWorkerControlleeRequestHandler::MaybeCreateJob // 准备创建主文档的Job
--> content::ServiceWorkerControlleeRequestHandler::PrepareForMainResource
--> content::ServiceWorkerControlleeRequestHandler::DidLookupRegistrationForMainResource
--> content::ServiceWorkerURLRequestJob::StartRequest
--> content::ServiceWorkerFetchDispatcher::DispatchFetchEvent // 从IO线程派发一个Fetch事件
--> content::ServiceWorkerVersion::DispatchFetchEvent
--> ServiceWorkerVersion::StartWorker
--> EmbeddedWorkerInstance::Start // 触发ServiceWorker的启动流程
步骤二:分派进程(多进程模式)/ 线程(单进程模式)。
ServiceWorker启动之前,它必须先向浏览器UI线程申请分派一个线程,再回到IO线程继续执行ServiceWorker线程的启动流程。
content::EmbeddedWorkerInstance::Start
--> content::EmbeddedWorkerInstance::RunProcessAllocated
--> ServiceWorkerProcessManager::AllocateWorkerProcess // from IO thread
--> ServiceWorkerProcessManager::AllocateWorkerProcess // PostTask to UI thread
--> ServiceWorkerProcessManager::AllocateWorkerProcess // from UI thread
--> content::EmbeddedWorkerInstance::ProcessAllocated // from IO thread
--> content::EmbeddedWorkerInstance::RegisterToWorkerDevToolsManager // from IO thread
--> content::EmbeddedWorkerInstance::RegisterToWorkerDevToolsManager // PostTask to UI thread
--> content::EmbeddedWorkerInstance::RegisterToWorkerDevToolsManager // from UI thread
--> content::EmbeddedWorkerInstance::SendStartWorker // from IO thread
--> content::EmbeddedWorkerRegistry::SendStartWorker
--> content::EmbeddedWorkerDispatcher::OnStartWorker
这个过程中,我们可以看到非常多的线程转换,IO --> UI --> IO --> UI --> IO。如果能够减少这些线程转换,是否能提升性能?步骤三:加载serviceworker.js。
分派了ServiceWorker线程之后,就会继续执行serviceworker.js的加载流程。
content::EmbeddedWorkerDispatcher::OnStartWorker
--> blink::WebEmbeddedWorkerImpl::startWorkerContext
--> blink::WebEmbeddedWorkerImpl::loadShadowPage // 加载一个与serviceworker.js相同URL的空白文档
--> blink::FrameLoader::load // 触发空白文档的加载
--> ... ...
--> blink::WebEmbeddedWorkerImpl::didFinishDocumentLoad
--> blink::WebEmbeddedWorkerImpl::Loader::load // 触发真实serviceworker.js的加载
--> content::ResourceDispatcherHostImpl::BeginRequest
--> content::ServiceWorkerReadFromCacheJob::Start
--> content::ServiceWorkerReadFromCacheJob::OnReadComplete
--> ResourceLoader::didFinishLoading // 完成serviceworker.js的加载
这个过程中,它会先加载一个空白文档,再去加载serviceworker.js,即会走两次完整的加载流程。步骤四:启动ServiceWorker线程。
serviceworker.js加载完成之后,就会触发ServiceWorker线程的启动流程
blink::ResourceLoader::didFinishLoading
--> blink::WorkerScriptLoader::didFinishLoading
--> blink::WebEmbeddedWorkerImpl::onScriptLoaderFinished
--> blink::WebEmbeddedWorkerImpl::startWorkerThread
--> blink::ServiceWorkerGlobalScopeProxy::create
--> blink::ServiceWorkerThread::create
--> blink::WorkerThread::start // 启动ServiceWorker线程
这个过程中,主要包括创建ServiceWorkerGlobalScope,初始化上下文(WorkerScriptController::initializeContextIfNeeded), 和执行JS代码(WorkerScriptController::evaluate)。步骤五:回调通知ServiceWorkerVersion启动完成。
ServiceWorker线程启动完成之后,回调通知ServiceWorkerVersion,至此,ServiceWorker线程启动完成。
WebEmbeddedWorkerImpl::startWorkerThread // 启动serviceworker线程
--> new ServiceWorkerThread::ServiceWorkerThread
--> content::ServiceWorkerDispatcherHost::OnWorkerStarted
--> content::EmbeddedWorkerRegistry::OnWorkerStarted
--> content::EmbeddedWorkerInstance::OnStarted
--> content::ServiceWorkerVersion::OnStarted // 启动serviceworker线程完成
从上面可以看到,ServiceWorker的启动流程极其复杂,这么复杂的启动流程,会带来怎样的性能消耗呢?
我们在下面详细分析上述五大步骤的性能消耗(测试数据来自Chromium57内核版本):
步骤 | 覆盖安装, 首次启动 | 重启浏览器, 首次启动 | 不退出浏览器, 再次启动 | 保持SW页面不关闭, 锁屏, 开屏, 启动SW |
---|---|---|---|---|
步骤一: 进入启动流程 | 2ms | 1ms | 1ms | 1ms |
步骤二:分派进程/线程 | 265ms | 151ms | 37ms | 56ms |
步骤三:加载serviceworker.js | 757ms | 37ms | 20ms | 108ms |
步骤四:启动ServiceWorker线程 | 33ms | 29ms | 23ms | 186ms |
步骤五:回调通知启动完成 | 2ms | 2ms | 2ms | 1ms |
1059ms | 220ms | 83ms | 352ms |
说明:上面数据来自本地测试数据,并非线上数据,不能完全代表用户的实际数据,但在各阶段的耗时趋势上,还是可以参考的。
注1:覆盖安装浏览器,第一次启动SW, 分派进程/线程的耗时265ms, 其中UI线程的耗时超过180ms,即UI非常繁忙。加载serviceworker.js的耗时为757ms,主要消耗在创建https连接和等待页面服务响应。注2:重启浏览器, 第一次启动SW, 分派进程/线程的耗时151ms, 其中UI线程的耗时超过120ms,即UI非常繁忙。加载serviceworker.js的耗时为37ms,因为可以从缓存中读取。
注3:不退出浏览器,第二次启动SW, 分派进程/线程的耗时37ms, UI相对空闲。加载serviceworker.js的耗时为20ms,估计是一部分内容可从内存中读取。
注4:保持SW页面不关闭,锁屏,开屏,启动SW,分派进程/线程的耗时56ms, 加载serviceworker.js的耗时为108ms,启动SW线程的耗时为186ms。
从上述数据可以看到,
提到, ServiceWorker的启动时间与用户设备条件有关,在PC上一般为50ms,手机上大概为250ms。在极端的场景下,比如在低端手机且CPU压力较大时,可能会超出500ms。Chromium浏览器已尝试使用多种方式来减少ServiceWorker的启动时间, 比如,
从我们的测试数据来看,ServiceWorker线程的启动耗时一般在100-300ms,与Chromium官方的数据相近。所以,我们能够得出一个大概的推论,ServiceWorker线程的启动是有较大成本的,一般在100-300ms。
那么,我们有没有一些比较有效的办法,尽可能降低ServiceWorker线程的启动耗时呢?
一些可能的办法,
(1)支持Code Cache,可以降低ServiceWorker JS的解析编译时间。(Chromium M53实现)
其中,Chromium M42 支持serviceworker.js的Code Cache。 参考:
Chromium M53 支持CacheStorage的Code Cache。参考:
(2)ServiceWorker线程启动不阻塞网络请求,即可以在启动过程中,同时发送网络请求。(Chromium M57实现)
其中,Chromium M57开始支持,还在持续完善中。参考:
(3)减少ServiceWorker线程启动过程的线程抛转。
从前面可以看到,ServiceWorker线程启动的过程有较多的线程抛转,特别是抛转到UI的过程,可能会非常耗时。 值得一提的是,Chromium官方对ServiceWorker启动性能问题也非常重视,他们有了非常全面的优化计划,请参考:, 有Chromium的全力投入,我们相信问题可以得到较好的解决。转载地址:http://qdjpx.baihongyu.com/