PWA系列 - Service Workers 启动性能

简介: ServiceWorker线程启动有较大的成本,它会直接影响到PWA页面的实际效果。本文详细分析ServiceWorker的启动耗时和可能的解决思路。

前言

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才能完成。

启动优化

Chromium官方文档 提到, 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。 参考:Support V8 code caching for ServiceWorker script

Chromium M53 支持CacheStorage的Code Cache。参考:Support V8 code caching in CacheStorage

(2)ServiceWorker线程启动不阻塞网络请求,即可以在启动过程中,同时发送网络请求。(Chromium M57实现)

其中,Chromium M57开始支持Navigation Preloads 技术,还在持续完善中。参考:Implement Service Worker navigation preload

(3)减少ServiceWorker线程启动过程的线程抛转。
从前面可以看到,ServiceWorker线程启动的过程有较多的线程抛转,特别是抛转到UI的过程,可能会非常耗时。


值得一提的是,Chromium官方对ServiceWorker启动性能问题也非常重视,他们有了非常全面的优化计划,请参考:Service Worker: performance roadmap, 有Chromium的全力投入,我们相信问题可以得到较好的解决。

参考文档

Improve service worker startup time

Speed up Service Worker with Navigation Preloads

Service Worker: performance roadmap

目录
相关文章
|
5天前
|
存储 缓存 安全
在 Service Worker 中配置缓存策略
Service Worker 是一种可编程的网络代理,允许开发者控制网页如何加载资源。通过在 Service Worker 中配置缓存策略,可以优化应用性能,减少加载时间,提升用户体验。此策略涉及缓存的存储、更新和检索机制。
|
3月前
|
微服务 Windows
【Azure微服务 Service Fabric 】在SF节点中开启Performance Monitor及设置抓取进程的方式
【Azure微服务 Service Fabric 】在SF节点中开启Performance Monitor及设置抓取进程的方式
|
3月前
|
监控 Java 应用服务中间件
【Azure 应用服务】App Service中抓取 Web Job 的 DUMP 办法
【Azure 应用服务】App Service中抓取 Web Job 的 DUMP 办法
|
3月前
【Azure 应用服务】App Service下部署的应用报错 Out of Memory
【Azure 应用服务】App Service下部署的应用报错 Out of Memory
|
4月前
|
缓存 JavaScript 前端开发
Web Workers与Service Workers:后台处理与离线缓存
Web Workers 和 Service Workers 是两种在Web开发中处理后台任务和离线缓存的重要技术。它们在工作原理和用途上有显著区别。
63 1
|
11月前
|
存储 Web App开发 缓存
WorkBox 之底层逻辑Service Worker(二)
WorkBox 之底层逻辑Service Worker(二)
168 0
|
11月前
|
存储 缓存 前端开发
WorkBox 之底层逻辑Service Worker(一)
WorkBox 之底层逻辑Service Worker(一)
103 0
|
缓存 JavaScript 前端开发
在项目中使用Service Worker 与 PWA
在项目中使用Service Worker 与 PWA
87 1
|
存储 缓存 前端开发
Service Worker实现离线缓存和推送通知
离线缓存和推送通知在提升网页的离线访问体验方面起着重要的作用。 离线缓存允许网页将所需的资源(如 HTML、CSS、JavaScript 文件、图像等)保存在用户设备的本地存储中。这意味着即使在没有网络连接的情况下,用户仍然可以访问网页的内容和功能。离线缓存不仅提供了更好的用户体验,而且还可以减轻服务器的负担,因为客户端可以直接通过本地缓存的资源进行加载,而无需每次都向服务器发出请求。
655 0
|
Apache Windows
Apache service monitor下无服务可供启动
Apache service monitor下无服务可供启动
243 0