浏览器的进程模型
我们首先来看下浏览器的进程模型,我们以 chrome 为例。
Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。
(图来自 https://zhuanlan.zhihu.com/p/47407398)
这也是为什么 chrome 明明只打开了一个 tab,却出现了 4 个进程的原因。
这部分不是我们本节的主要内容,大家了解这么多就够了,接下来我们看下今天的主角 - 渲染进程。
渲染进程几乎负责 Tab 内的所有事情,渲染进程的核心目的在于转换 HTML CSS JS 为用户可交互的 web 页面。
渲染进程由以下四个线程组成:主线程 Main thread , 工作线程 Worker thread,光栅线程 Raster thread 和排版线程 Compositor thread。
我们的今天的主角是主线程 Main thread 和 工作线程 Worker thread。
主线程负责:
可以看出主线程非常繁忙,需要做很多事情。主线程很容易成为应用的性能瓶颈。
当然除了主线程, 我们的其他进程和线程也可能成为我们的性能瓶颈,比如网络进程,解决网络进程瓶颈的方法有很多,可以使用浏览器本身缓存,也可以使用 ServiceWorker,还可以通过资源本身的优化等。这个不是我们本篇文章的讨论重点,这里只是让你有一个新的视角而已,因此不赘述。
工作线程能够分担主线程的计算压力,进而主线程可以获得更多的空闲时间,从而更快地响应用户行为。
工作线程主要有 Web Woker 和 Service Worker 两种。
以下摘自MDN
Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,他们可以使用 XMLHttpRequest 执行 I/O (尽管 responseXML 和 channel 属性总是为空)。一旦创建, 一个 worker 可以将消息发送到创建它的 JavaScript 代码,
以下摘自MDN
Service workers 本质上充当 Web 应用程序与浏览器之间的代理服务器, 也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验, 拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步 API。
工作线程尤其是Web Worker的出现一部分原因就是为了分担主线程的压力。
整个过程就像主线程发布命令,然后工作线程执行,执行完毕将执行结果通过消息的形式传递给主线程。
我们以包工头包工程,然后将工作交给各个单位去做的角度来看的话,大概是这样的:
实际上工作工作进程,尤其是WebWorker已经出现很长时间了。但是很多时候我们并没有充分使用,甚至连使用都没使用。
下面以Web Worker为例, 我们来深度挖掘一下工作线程的潜力。
前面的文章,我们谈了很多前端领域的算法,有框架层面的也有应用层面的。
前面提到了React的调和算法,这部分代码耗时其实还是蛮大的,React16重构了 整个调和算法,但是总体的计算成本还是没有减少,甚至是增加的。
关于调和算法可以参考我的另外一篇文章前端领域的数据结构与算法解读 - fiber
我们有没有可能将这部分内容抽离出主线程,交给工作进程,就像上面的图展示的那样呢?我觉得可以, 另外我前面系列文章提到的所有东西,都可以放到工作线程中执行。比如状态机,时光机,自动完成,差异比对算法等等。
如果将这些抽离出我们主线程的话,我们的应用大概会是这样的:
这样做主线程只负责UI展示,以及事件分发处理等工作,这样就大大减轻了主线程的负担,我们就可以更快速地响应用户了。然后在计算结果完成之后,我们只需要通知主线程,主线程做出响应即可。可以看出,在项目复杂到一定程度,这种优化带来的效果是非常大的。
我们来开一下脑洞, 假如流行的前端框架比如React内置了这种线程分离的功能, 即将调和算法交给WebWorker来处理,会给前端带来怎么样的变化?
假如我们可以涉及一个算法,智能地根据当前系统的硬件条件和网络状态, 自动判断应该将哪部分交给工作线程,哪部分代码交给主线程,会是怎么样的场景?
这其实就是传说中的启发式算法, 大家有兴趣可以研究一下
上述描述的场景非常美好,但是同样地也会有一些挑战。
第一个挑战就是操作繁琐,比如webworker只支持单独文件引入,再比如不支持函数序列化,以及反复序列化带来的性能问题, 还有和webworker通信是异步的等等。
但是这些问题都有很成熟的解决方案,比如对于操作比较繁琐这个问题我们就可以通过使用一些封装好web worker操作的库。comlink 就是一个非常不错的web worker的封装工具库。
对于不支持单文件引入,我们其实可以用 Blob
, createObjectURL
的方式模拟, 当然社区中其实也有了成熟的解决方案,如果你使用webpack构建的话,有一个 worker-loader
可以直接用。
对于函数序列化这个问题,我们无法传递函数给工作线程,其实上面提到的 Comlink, 就很好地解决了这个问题,即使用Comlink提供的 proxy
, 你可以将一个代理传递到工作线程。
对于反复序列化带来的性能问题,我们其实可以使用一种叫 对象转移(TransferableObjects)
的技术,幸运的是这个特性的浏览器兼容性也不错。
对于异步的问题,我们可以采取一定的取舍。即我们 本地每次保存一份最近一份的结果拷贝,我们只需要每次返回这个拷贝, 然后在webworker计算结果返回的时候更新拷贝即可。
这篇文章的主要目的是让大家以新的视角来思考当前的前端应用,我们站在进程和线程的角度来看现在的前端应用,或许会有更多的不一样的理解和思考。
本文先是讲了浏览器的进程模型,然后讲了浏览器的渲染进程中的 线程模型。我们知道了渲染进程主要有四个线程组成, 分别是主线程 Main thread , 工作线程 Worker thread,光栅线程 Raster thread 和排版线程 Compositor thread。
然后详细介绍了主线程和工作线程,并以webworker为例,讲述了如何利用工作线程为我们的主线程分担负担。为了消化这部分知识,建议你自己动手实践一下。
虽然我们的愿望很好,但是这其中在应用的过程之中还是有一些坑的,我这里列觉了一些常见的坑,并给出了解决方案。
我相信工作线程的潜力还没有被充分发挥出来,希望可以看到前端应用真正的挖掘各个进程和线程潜力的时候吧,这不但需要前端工程师的努力,也需要浏览器的配合支持,甚至需要标准化组织去推送一些东西。