博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
PWA系列 - Service Workers 启动性能
阅读量:5905 次
发布时间:2019-06-19

本文共 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进程/线程的过程中,有非常多的线程转换,IO --> UI --> IO --> UI --> IO,这个过程如果UI线程非常繁忙,耗时会非常大,甚至可以超过200ms。
  • 加载serviceworker.js,首次加载需要创建https连接和等待服务器响应,耗时可以超过700ms,但在非首次的场景下,可以从缓存读取,一般能在50ms以内完成。
  • 启动ServiceWorker线程,包括创建ServiceWorkerGlobalScope,初始化上下文(WorkerScriptController::initializeContextIfNeeded), 和执行JS代码(WorkerScriptController::evaluate), 这些过程一般能在50ms内完成。
  • 手机锁屏开屏的场景下,浏览器大部分内存都会被清除,会极大的影响缓存读取以及对象创建的时间,比如创建v8 isolate,一般能在10ms完成,但锁屏之后要80ms才能完成。

启动优化

 提到, ServiceWorker的启动时间与用户设备条件有关,在PC上一般为50ms,手机上大概为250ms。在极端的场景下,比如在低端手机且CPU压力较大时,可能会超出500ms。Chromium浏览器已尝试使用多种方式来减少ServiceWorker的启动时间, 比如, 

  • 使用V8 Code Cache()。
  • 在没有注册监听fetch事件的页面允许先发网络请求()。
  • 在特定情境下(比如,mouse/touch事件), 预先启动ServiceWorker()。
  • 使用, 允许fetch请求在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/

你可能感兴趣的文章
Redis for Windows(C#缓存)配置文件详解
查看>>
回忆2013年的点点滴滴(各个方面)
查看>>
ASP.NET MVC 4使用PagedList.Mvc分页
查看>>
HDOJ 2066 floyed优化算法
查看>>
window.onscroll
查看>>
开发常用动画收集
查看>>
nginx js、css多个请求合并为一个请求(concat模块)
查看>>
mybatis实战教程(mybatis in action)之五:与spring3集成
查看>>
解决浏览器Adobe Flash Player不是最新版本问题
查看>>
SQLite 约束
查看>>
Python爬虫学习——使用Cookie登录新浪微博
查看>>
linux配置网络
查看>>
vsftp 500 OOPS: cannot change directory:/home/xyp
查看>>
MVC ---- EF的安装于卸载
查看>>
WebRTC 学习之 概念总结
查看>>
Java对ad操作
查看>>
unity与android交互总结
查看>>
h5 微场景
查看>>
linux下uboot kernel操作cpu寄存器
查看>>
(转)PaperWeekly 第二十二期---Image Caption任务综述
查看>>