转自IMWeb社区,做者:jerryOnlyZRJ,原文连接前端
本文是对前文:imweb.io/topic/5b6fd… 相关知识的补充,文中的“前文”一词同此。git
特以此文向《WebKit技术内幕》做者朱永盛前辈致敬。github
自上次发布了《网站性能优化实战——从12.67s到1.06s的故事》一文后,发现本身对页面渲染性能这个版块介绍的内容还不够完善,为了更清晰的梳理浏览器渲染页面的机制,以让读者更为全面了解渲染性能优化的深层次原理,笔者在课余时间从新研读了一遍《WebKit技术内幕》一书,将本身的总结经验分享予论坛同僚。文章更新可能以后不会实时同步在论坛上,欢迎你们关注个人Github,我会把最新的文章更新在对应的项目里,让咱们一块儿在代码的海洋里策马奔腾:github.com/jerryOnlyZR… 。web
让咱们用本身的双手,创造出极致的页面渲染性能。面试
由于本文是基于前文的基础上拓展了相关内容,因此可能会有部分文字重复,但愿你们不要介意。算法
仍是献上前文的那张浏览器渲染引擎、HTML解释器、JS解释器关系图:chrome
咱们平时打开浏览器所看到的界面,就是图里的User Interface,咱们常说的浏览器内核,指的就是咱们的渲染引擎——Rendering engine,最著名的还属Chrome的前任、Safari的搭档WebKit,咱们使用的大多数移动端产品(Android、IOS 等等)都是使用的它,也就是说咱们能够在手机上实现咱们的CSS动画、JS交互等等效果,这也是咱们的前端开发人员可以开发出Web和Hybrid APP的缘由,包括如今的Blink,其实也应该算是Webkit的一个变种,它是从WebKit衍生来的,可是Google在和WebKit分手后便在Blink里使用了声名远播的V8引擎,打出了一场漂亮的翻身战。还有IE的Trident,火狐的Gecko浏览器内核,平时咱们须要为部分CSS样式添加兼容性前缀,正是由于不一样的浏览器使用了不一样的渲染引擎,产生了不一样的渲染机制。npm
渲染引擎内包括了咱们的HTML解释器,CSS样式解释器和JS解释器,不过如今咱们会经常听到人们说V8引擎,咱们常常接触的Node.js也是用的它,这是由于JS的做用愈来愈重要,工做愈来愈繁杂,因此JS解释器也渐渐独立出来,成为了单独的JS引擎。编程
在你深刻探知浏览器内部机理以前,你必须知道,浏览器是多进程、多线程模型,这里咱们以基于Blink内核的Chromium浏览器为例,讲讲在Chromium浏览器中,几个常见的进程:浏览器
刚刚咱们提到的全部进程,他们都具备以下特征:
为了能让你们更为直观的理解Chromium多进程模型,笔者附上一张Chrome浏览器在Windows上的多进程示例图:(打开任务管理器,将进程按照“命令行”排序,找到“Google Chrome”相关内容)
从进程的type参数中,咱们能够区分出不一样类型的进程,而那个不带type参数的进程,指的就是咱们的Browser浏览器主进程。
每一个进程的内部,都有不少的线程,多线程的主要目的就是为了保持用户界面的高响应度,保证UI线程(Browser进程中的主线程)不会被被其余费事的操做阻碍从而影响了对用户操做的响应。就像咱们平时所说的JS脚本解释执行,都是在独立的线程中的,这也是JS这门编程语言特立独行的地方,它是单线程脚本。
在这里作一些简单的拓展,你们看看下面这段代码:
setTimeout(function(){
console.log("我能被输出吗?")
}, 0)
while(true){
var a = 1;
}
复制代码
你们确定都知道由于线程阻塞,定时器里的console并不会被输出,这就是由于咱们的JS解释执行是单线程的,因此在执行过程当中须要将同步和异步的两类代码分别压入同步堆栈和异步队列中,经过Event Loop实现异步操做:
也就说咱们的JS定时器其实并非彻底准确的,还须要考虑同步堆栈中代码执行产生的延迟。
不过如今有不少技术可让咱们的JS代码模拟多线程执行,包括以前一位日本大牛编写的一个名为Concurrent.Thread.js
的插件,还有HTML5标准中提出的Web Worker,这些工具都能让咱们实现多线程执行JS代码的效果。
在了解浏览器渲染机制以前你必须理解浏览器的层级结构,或许你知道浏览器的渲染频率是60fps,知道浏览器的页面呈现就如同电影般是逐帧渲染的效果,但并不表明页面就像胶片同样,从头到尾都是单层的。页面所经历的,是从一个像千层面同样的东西一步步合成的过程,中间经历了软硬件渲染等等过程,最后造成一个完整的合成层才被渲染出来。千层面的效果大概就像Firefox的3D View插件所呈现出的那般:
有人可能会说刨得这么深咱们实际开发中用获得吗?若是我这么和你说“性能优化不是讲究减小重排重绘嘛,我如今手上有一套方案,能让你的页面动画直接跳太重排重绘的环节”,你是否会对此产生一点兴趣?不过不着急,在咱们尚未把其中原理理清以前,我是不会草率地放出解决方案的,否则很容易就会让你们的思想偏离正轨,由于我就是经历了那样一个惨痛的过程过来的。
若是要验证我上述所言非凭空捏造,你们能够打开chrome开发者工具中的performance版块,录制一小段页面渲染,并将输出结果切换至Event Log版块,你们就能够清晰地看见网站渲染经历的过程:
在Activity字段中咱们能够看到,咱们的页面经历了从新计算样式→更新Layer树→绘制→合成合成层的过程,结合咱们的Summary版块中的环形图,咱们能够大体把页面渲染分为三个阶段:
若是你学过计算机网络,或者数字电子技术,那么你必定知道,资源在网路中传输的形式是字节流。咱们每次请求一个页面,都通过了字节流→HTML文档→DOM Tree的过程,其中细节我已在前一篇文章中的navigation timing版块做了详细介绍,今天咱们只谈DOM树构建以后浏览器的相关工做。
DOM树的根是document,也就是咱们常常在浏览器审查元素时能看到的HTMLDocument,HTML文档中的一个个标签也被转化成了一个个元素节点。
既然说到了DOM树,就不得不说起浏览器的事件处理机制。事件处理最重要的两个部分即是事件捕获(Event Capture)和事件冒泡(Event Bubbling)。事件捕获是自顶向下的,也就是说事件是从document节点发起,而后一路到达目标节点,反之,事件冒泡的过程则是自下而上的顺序。
咱们常使用addEventListener()
方法来监听事件,它包含三个参数,前两个你们都太熟悉,咱们来聊聊第三个参数,MDN上将它称做useCapture,类型为Boolean。它的取值显而易见,即是true和false(默认),若是设置为true,表示在捕获阶段执行回调,而false则是在冒泡阶段执行,它决定了父子节点的事件绑定函数的执行顺序。
在DOM树之中,某些节点是用户不可见的,也就是说这些只是起一些其余方面而不是显示内容的做用。例如head节点、script节点,咱们能够称之为“非可视化节点”。而另外的节点就是用来展现页面内容的,包括咱们的body节点、div节点等等。对于这些“可视节点”,由于WebKit须要将它们的内容渲染到最终的页面呈现中,因此WebKit会为他们创建相应的RenderObject对象。一个RenderObject对象保存了为了绘制DOM节点所须要的各类信息,其中包括样式布局信息等等。
可是构建的过程并无就此结束了,由于WebKit要对每个可视节点都生成一个RenderObject对象,若是当即对全部的对象进行渲染,假设咱们的页面有上百个可视化元素,那将会是多么复杂的一项工程啊。为了减少网页结构的复杂程度,并在不少状况下可以减小从新渲染的开销,WebKit会依据节点的样式为网页的层次建立响应的RenderLayer对象。
当某些类型的RenderObject节点或者具备某些CSS样式的RenderObject节点出现的时候,WebKit就会为这些节点建立RenderLayer对象。RenderLayer节点和RenderObject节点不是一一对应关系,而是一对多的关系,其中生成RenderLayer的基本规则主要包括:
RenderLayer节点的使用能够有效地减少网页结构的复杂程度,并在不少状况下可以减少从新渲染的开销。通过梳理,RenderObject和RenderLayer的构建大概就是下图这样一个过程:
最后的构建结果将会以具体代码的形式在WebKit中存储起来:
“layer at (x, x)”表示的是不一样的RenderLayer节点,下面的全部的RenderObject对象均属于该RenderLayer对象。
这一板块的内容你们只须要了解就好,有兴趣能够深究。
浏览器的渲染方式,主要分为两种,第一种是软件渲染,第二种是硬件渲染。若是绘制工做只是由CPU完成,那么称之为软件渲染,若是绘制工做由GPU完成,则称之为硬件渲染。软件渲染与硬件渲染有不一样的缓存机制,只要咱们合理利用,就能发挥出最好的效果。
在软件渲染中,一般的结果就是一个位图(Bitmap)。若是在页面的某一元素发生了更新,WebKit只是首先计算须要更新的区域,而后只绘制同这些区域有交集的RenderObject节点。也就是说,若是更新区域跟某个Render-Layer节点有交集,WebKit就会继续查找RenderLayer树中包含的RenderObject子树中的特定的一个或一些节点(这话好拗口,说的我都喘不过气了),而不是去从新绘制整个RenderLayer对应的RenderObject子树。以上内容,咱们也能够称之为CPU缓存机制。
而硬件渲染的相关内容,咱们将在下一模块以一个单独的模块进行介绍,由于相关的理论和优化的知识太多了。
终于到了咱们的重头戏了,若是你能参透硬件渲染机制并物尽其用,那么基本上能够说你在浏览器渲染性能上的造诣已经快登峰造极了。咱们刚刚已经说过,浏览器还有一种名为硬件渲染的渲染方式,它是使用GPU的硬件能力来帮助渲染页面。那么,硬件渲染又是怎样的一个过程呢?
WebKit会依据指定条件决定将那些RenderLayer对象组合在一块儿造成一个新层并缓存在GPU,这一新层不久后会用于以后的合成,这些新层咱们统称为合成层(Compositing Layer)。对于一个RenderLayer对象,若是他不会造成一个合成层,那么就会使用它的父亲所使用的合成层,最后追溯到document。最后,由合成器(Compositor)将全部的合成层合成起来,造成网页的最终可视化结果,实际上同软件渲染的位图同样,也是一张图片。
同触发RenderLayer条件类似,知足必定条件或CSS样式的RenderLayer会生成一个合成层:
Compositing due to association with a element thay may overlap other composited elements
,意思就是你这个RenderLayer盖在别的合成层至上啦,因此我浏览器要把你强制变成一个合成层)若是你们想要更直观地了解合成层到底是一个什么样的形式,Chrome开发者工具为咱们提供了十分好用的工具。即是开发者工具中的Layers功能模块(具体的添加及使用流程已在前文中作了详细介绍,若有须要还望读者移步):
版块的左侧的列表里将会列出页面里存在哪些渲染层,右侧的Details将会显示这些渲染层的详细信息。包括渲染层的大小、造成缘由等等,从图中咱们能够清楚知道,百度首页只存在一个合成层document(由于百度首页自己没有过多的动画须要大量重排重绘,因此一个合成层足够了),这个合成成的造成缘由是由于它是一个根Layer(Root Layer),和咱们说的造成合成层的第一个条件别无二致。
你们能够试着在开发者工具里根据咱们刚刚提出的几条规则试着去修改元素的CSS样式,尝试一下看看是否会生成一个新的Compositing Layer。↖(^ω^)↗
不过这时候问题来了,为何咱们已经对RenderObject合成了一次RenderLayer,以后还须要再合成一次Compositing Layer呢,这难道不是画蛇添足吗?其实缘由是,首先咱们再一次对页面的层级进行了一次合成,这样能够减小内存的使用量;其二是在合并以后,GPU会尽可能减小合并后元素发生更新带来的重排重绘性能和处理上的困难。
上面的两个缘由你们听起来可能还云里雾里,到底是什么意思呢?
咱们都知道,提高渲染性能的第一要义是减小重排重绘,咱们以前也说过,在软件渲染的过程当中,若是发生元素更新,CPU须要找到更新到RenderObject进行从新绘制,其中过程包括了重排和重绘。但若是页面只是某个合成层发生了位置的偏移、缩放、透明度变化等操做,那么GPU会取代CPU去处理从新绘制的工做,由于GPU要作的知识把更新的合成层进行相应的变换并送入Compositor从新合成便可。
PS:你们能够尝试的本身写一个动画,好比某个div从left: 0
变化到 left: 200px
,若是触发了合成层它是不会发生重排和重绘。(观察元素是否发生了重排重绘的方法已在前文进行了详细介绍)
综上所述,浏览器的渲染方式大概是下面这样一个流程:
笔者本身画的流程图可能比较简陋,但愿你们见谅啊。也就是说,网页加载后,每当从新绘制新的一帧的时候,须要经历三个阶段,就是流程图中的布局、绘制和合成三个阶段。而且,layout和paint每每占用了大量的时间,因此咱们想要提升性能,就必须尽量减小布局和绘制的时间,最佳的解决方案固然是在从新渲染时触发硬件加速而直接跳太重排和重绘的过程。
自从前文发布后,就有小伙伴向我提到了JS阻塞性能这部份内容介绍的较少,今天就为此做些许补充。你们都知道JS代码会阻塞咱们的页面渲染,并且相对于另外两部分性能优化而言(前文提到过的网络传输性能优化与页面渲染性能优化),JS性能调优是一项很大的工程,由于做为一门编程语言,其中涉及到的算法、时间复杂度等知识对于大多数CS专业的学生而言应该是很熟悉的名词了吧,这也是大厂笔试面试必考的知识点。举个最简单的例子,学过C的小伙伴确定熟悉这么一个梗,请输出给定范围(N)内全部的素数,你可能会想到使用两个for循环去实现,的确,这样输出的值没有一点问题,可是没有做任何优化,作过这道题的人都知道能够在内层的for循环里将区间限制在j<=(int)sqrt(i)
这句简单的代码有什么效果呢,给你举个简单的例子,若是N的取值是100,它能帮你省去内层循环最多90次的执行,具体原理你们就自行去研究吧。
若是你对这些计算机基础知识还不是特别了解,或者以前没有传统编程语言的基础,我推荐你们去翻阅这样一篇文章,可以快速地带你了解关于代码执行性能的重要指标——时间复杂度的相关知识。传送门:mp.weixin.qq.com/s?__biz=MzA…
而这个模块的内容,不会给你们去介绍JS经常使用的算法或者是下降复杂度的技巧,由于若是我这么一篇简短的文章可以说得清楚的话,这些知识在大学里面就不会造成一门完整的课程了。今天主要就是为你们推荐两款很是实用的JS代码性能监测工具,供你们比较本身与他人书写的代码的性能优劣。
首先提到的即是声名远播的Benchmark.js这款插件啦,这是它的官网:benchmarkjs.com/ (图片来自官网截图)
使用方法很简单,按照官网的教程一步步走就好了:
首先如今项目里安装Benchmark:$ npm i --save benchmark
在检测文件中引入Benchmark模块:var Benchmark = require('benchmark');
实例化Benchmark下的Suite,使用实例下的add方法添加函数执行句柄
实例的on方法就是用于监听Benchmark监测代码执行抛出的事件,其中cycle会在控制台输出相似这样的执行结果:
其中,Ops/sec 测试结果以每秒钟执行测试代码的次数(Ops/sec)显示,这个数值确定是越大越好。除了这个结果外,同时会显示测试过程当中的统计偏差(百分比值)。
Benchmark的使用方法就是这么简便,它的做用就好像是咱们平时运动会短跑比赛上裁判的读秒器,而咱们的代码就像是咱们的运动员,试着去和大家的小伙伴比比看,看实现同一需求,谁的代码更有效率吧。
JsPerf和Benchmark的功能其实是如出一辙的,包括它的输出内容,只不过它是一款在线的代码执行监测工具,无需像Benchmark那样安装模块,书写本地文件,只须要简单的复制粘帖就行,传送门:jsperf.com/ (图片来自官网截图)
咱们只须要使用github登录,而后点击Tests
下的Add
连接就可疑新建一个监测项目
接下来会让咱们填一些描述信息,基本的英文你们应该都能看懂吧,这就不用我再去介绍了,只要把带星号的部分填完就没问题了:
重点就是把Code snippets to compare
这个模块里面的内容天完整就好了,顾名思义,这里面填写的就是咱们须要去监测的两个代码执行句柄。
点击save test case监测结果就是这样,具体评判标准参照Benchmark:
花了三天的时间才终于把浏览器的渲染机制这篇文章的相关内容整理完成,笔者也是创建在本身粗略的理解上将本身总结的经验分享给你们,这篇文章比前文写起来难度要高不少,由于所涉及的理论和知识太深,又只有太少的素材对这些理论展开了深刻的介绍,但在咱们实际的开发中,若是只知其然而不知其因此然,每每会在不少地方陷入迷茫,或者滥用硬件加速形成移动产品不可逆转的寿命消耗,因此笔者在研读完《WebKit技术内幕》一书以后,便马上将书中知识结合开发所学撰写成文,与广大前端爱好者分享。若是文中有歧义或者错误,欢迎你们在评论区提出意见和批评,我会第一时间回答和改正。成文不易,不喜勿喷。