前端阶段性总结(二):页面渲染机制与性能优化
引言: 转前端一年了,期间工作较忙,也没时间整理一些知识体系,此系列文章是对前端基础的一些回顾与总结。本文主要介绍浏览器工作的原理以及一些优化手段。
一、浏览器渲染过程
1. 浏览器的主要结构:
2. 浏览器的多进程模型:
以chorme为例:
- Browser进程:浏览器的主进程,负责浏览器界面的显示,各个页面的管理,其他各种进程的管理;
- Renderer进程:页面的渲染进程,负责页面的渲染工作,Blink的工作主要在这个进程中完成(主要分成render主线程和合成器线程);
- NPAPI插件进程:每种类型的插件只会有一个进程,每个插件进程可以被多个Render进程共享;
- GPU进程:最多只有一个,当且仅当GPU硬件加速打开的时候才会被创建,主要用于对3D加速调用的实现;
- Pepper插件进程:同NPAPI插件进程,不同的是为Pepper插件而创建的进程
需要注意的是,NPAPI是指浏览器对系统或外部的一些程序的调用接口,比如播放视频的 flash 插件,而Pepper其实是基于NPAPI改进的插件架构。
3. 网页请求过程:
4. 浏览器渲染过程
a. 主要流程:
主流的浏览器内核主要有2种,Webkit 和 Geoko ,虽然 chorme 现在的内核更换为 blink ,但其实 blink是基于webkit的,差异不大。其渲染过程分别如下:
- webkit
- Geoko
这两个内核的渲染流程大同小异,主要的过程可以总结为下列5个:
- DomTree: 解析html构建DOM树。
- CssomTree : 解析CSS生成CSSOM规则树。
- RenderObjectTree: 将DOM树与CSSOM规则树合并在一起生成渲染对象树。
- Layout: 遍历渲染树开始布局(layout),计算每个节点的位置大小信息。
- Painting: 将渲染树每个节点绘制到屏幕。
可以看到上述流程的耗时,甚至可以统计到每一帧的耗时分布,从而对影响渲染性能的代码精确定位。其中黄色为JS,紫色为Style和Layout,绿色为Paint和Composite部分,选中每个部分会显示各自的花费时间等信息,可以看出这个图片中JS运行的时间太长。目前的显示设备一般刷新率是60FPS,所以理想中每帧的时间最好为16毫秒。
需要注意的一点是,这里的步骤执行并没有特定的顺序,为保证渲染的速度,浏览器一开始接收到html时就开始执行解析的过程,并且遇到需要重绘和重排的时候会重复执行这些步骤,下面我们详细介绍一下这5个过程。
b. 具体流程
DOM树的构建:
浏览器在接收到html文件后即开始解析和构建DOM树,在碰到js代码段时,由于js代码可能会改变dom的结构,所以为避免重复操作,浏览器会停止dom树构建,先加载并解析js代码。而对于css,图片,视频等资源,则交由资源加载器去加载,这个过程是异步的,并不会阻碍dom树的生成。这个过程需要注意的点是:
display:none
的元素、注释存在于dom树中- js会阻塞dom树的构建从而阻塞其他资源的并发加载,因此好的做法是将js放在最后加载
- 对于可异步加载的js片段加上
async
或defer
CSSOM树的构建:
浏览器在碰到<link>
和 <style>
标签时,会解析css生成cssom , 当然,link标签需要先将css文件加载完成才能解析。
需要注意的是:
- js 代码会阻塞cssom的构建,在webkit内核中有所优化,只有js访问css才会阻塞
- cssom的构建与dom树的构建是并行的
- 减少css的嵌套层级和合理的定义css选择器可以加快解析速度,可参考如何提升 CSS 选择器性能
RenderObject树的构建:
在cssom 和dom 树都构建完成后,浏览器会将他们结合,生成渲染对象树,渲染树的每一个节点,包含了可见的dom节点和节点的样式 。
需要注意的是:
- renderObject树 与 dom树不是完全对应的,不可见的元素如
display:none
是不会放入渲染树的。 visibility: hidden
的元素在Render Tree中
布局:
这一步是浏览器遍历渲染对象树,并根据设备屏幕的信息,计算出节点的布局、位置,构建出渲染布局树(render layout)。渲染布局树输出的就是我们常说的盒子模型,需要注意的是:
float
,absolute
,fixed
的元素的位置会发生偏移- 我们常说的脱离文档流,其实就是脱离布局树
绘制:
浏览器对生成的布局树进行绘制,由用户界面后端层将每个节点绘制出来。此时,Webkit内核还需要将渲染结果从Renderer进程传递到Browser进程。
4. 重绘和回流
前面讲到,js代码可以访问和修改dom节点和css,所以在解析js的过程中会导致页面重新布局和渲染,这就是重绘(repaint)和回流(reflow)。
a. 重绘:
概念:
重绘是指css样式的改变,但元素的大小和尺寸不变,而导致节点的重新绘制。
重绘的触发:
任何对元素样式,如background-color
、border-color
、visibility
等属性的改变。css 和 js 都可能引起重绘。
b. 回流
概念
回流(reflow)是指元素的大小、位置发生了改变,而导致了布局的变化,从而导致了布局树的重新构建和渲染。
回流的触发
- dom元素的位置和尺寸大小的变化
- dom元素的增加和删除
- 伪类的激活
- 窗口大小的变化
- 增加和删除class样式
- 动态计算修改css样式
当然,我们的浏览器不会每一次reflow都立刻执行,而是会积攒一批,这个过程也被成为异步reflow,或者增量异步reflow。但是有些情况浏览器是不会这么做的,比如:resize窗口,改变了页面默认的字体,等。对于这些操作,浏览器会马上进行reflow。
二、页面性能分析与测速
优化并不是无目的的,而是通过分析页面各个维度,找到亟待优化的方向或者具体到某段代码。下面就讨论一下如何对页面做性能分析和测速监控。
1.性能分析
Chorme Devtools
chorme得devtools相信所有的前端开发者都用过,它不仅提供了日常开发中极强的调试能力,同时也具备着极强的页面分析能力。
第三方分析网站
2.测速上报
测速的关键指标
一般来说,我们打开一个页面,期望的是页面的响应和呈现速度和流畅的交互体验。所以,页面的测速指标可以大致概括为: 白屏时间,首屏时间,可交互时间。
如何计算
window.performance是w3c提供的用来测量网页和Web应用程序的性能api。其中performance timing提供了延时相关的性能信息,可以高精度测量网站性能。timing的整体结构如下图所示:
- 白屏时间=页面开始展示的时间点(PerformanceTiming.domLoading)-开始请求时间点(PerformanceTiming.navigationStart)
- 首屏时间=首屏内容渲染结束时间点(视业务具体情况而定)-开始请求时间点(PerformanceTiming.navigationStart)
- 可交互时间=用户可以正常进行事件输入时间点(PerformanceTIming.domInteractive)-开始请求时间点(PerformanceTiming.navigationStart)
三、性能优化
关于性能优化,涉及的方向太广了,从网络请求到数据库,整条链路都有其可优化的地方。这里我只总结一下前端比较需要关注的一些优化点。这里从两个个维度进行讨论:
(一). 网络请求的优化
从上文可知,浏览器渲染网页的前提是下载相关的资源,html文档、css文档、图片资源等。这些资源是客户端基于HTTP协议,通过网络请求从服务器端请求下载的,大家都知道,有网络,必定有延迟,而资源加载的网络延迟,是页面缓慢的一个重要因素。所以,如何使资源更快、更合理的加载,是性能优化的必修课。
1. 静态资源
1)拼接、合并、压缩、制作雪碧图:
由于HTTP的限制,在建立一个tcp请求时需要一些耗时,所以,我们对资源进行合并、压缩,其目的是减少http请求数和减小包体积,加快传输速度。
- 拼接、合并、压缩: 在现代的前端工程化开发流程中,相信大家都有使用webpack或者gulp等打包工具对资源(js、css、图片等)进行打包、合并、去重、压缩。在这基础上,我们需要根据自身的业务,合理的对公共代码,公共库,和首屏代码进行单独的打包压缩,按需加载;
- 雪碧图:对于图片资源,我们可以制作雪碧图,即对一些页面上的icon和小图标,集成到一张图片上,css使用背景图定位来使用不同的icon,这样做可以有效的减少图片的请求数,降低网络延迟。而它的缺点也很明显,由于集成在同一张图片上,使用其中的一个图标,就需要将整张图片下载下来,所以,雪碧图不能盲目的使用。
segmentfault.com 的雪碧图图标
2)CDN资源分发:
将一些静态资源文件托管在第三方CDN服务中,一方面可以减少服务器的压力,另一方面,CDN的优势在于,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,保证资源的加载速度和稳定性。
3)缓存:
缓存的范围很广,比如协议层的DNS解析缓存、代理服务器缓存,到客户端的浏览器本地缓存,再到服务端的缓存。一个网络链路的每个环节都有被缓存的空间。缓存的目的是简化资源的请求路径,比如某些静态资源在客户端已经缓存了,再次请求这个资源,只需要使用本地的缓存,而无需走网络请求去服务端获取。
segmentfault 的主页的一些静态资源使用了缓存,上面是一些控制缓存的header首部字段
4)分片:
分片指得是将资源分布到不同的主机,这是为了突破浏览器对同一台主机建立tcp连接的数量限制,一般为6~8个。现代网站的资源数量有50~100个很常见,所以将资源分布到不同的主机上,可以建立更多的tcp请求,降低请求耗时,从而提升网页速度。
从segmentfault 的主页请求可以看出,网站将静态js文件和图片都放在了不同的子域名下。
5)升级协议:
可以升级我们的网络协议,比如使用HTTP2,quic 之类的,代替之前的http1.1,从协议层优化资源的加载。可以参考我之前的文章。
2. 业务数据
虽然做好了静态数据的加载优化,但是还是会出现一种情景,即静态数据已经加载完毕,但页面还是在转菊花,页面还没有进入可交互状态,这是因为现如今的网站开发模式,前后端分离已经成为主流,不再由php或jsp服务端渲染前端页面,而是前端先加载静态数据,再通过ajax异步获取服务器的数据,进而重新渲染页面。这就导致了异步从接口获取数据也是网页的一个性能瓶颈。响应缓慢,不稳定的接口,会导致用户交互体验极差,页面渲染速度也不理想。比如点击一个提交数据的按钮,接口速度慢,页面上菊花需要转好久才能交换完数据。
1)首屏直出
为了提升用户体验,我们认为首屏的渲染速度是极为重要的,用户进来页面,首页可见区域的加载可以由服务端渲染,保证了首屏加载速度,而不可见的部分则可以异步加载,甚至做到子路由页面的预加载。业界已经有很多同构直出的方案,比如vue的nuxt , react的beidou等。
2)接口合并
前端经常有这样的场景,完成一个功能需要先请求第一个接口获得数据,然后再根据数据请求第二个接口获取第二个数据,然后第三、第四...前端通常需要通过promise或者回调,一层一层的then下去,这样显然是很消耗性能的
通常后台接口都按一定的粒度存在的,不可能一个接口满足所有的场景。这是不可避免的,那么如何做到只发送一个请求就能实现功能呢?有一种不错的方案是,代理服务器实现请求合并,即后台的接口只需要保证健壮和分布式,而由nodejs(当然也可以使用其他语言)建设一层代理中间层,流程如下图所示:
前端只需要按找约定的规则,向代理服务器发起一次请求,由代理服务器向接口服务器发起三次请求,再将目标数据返回给客户端。这样做的好处是:一方面是代理服务器代替前端做了接口合并,减少了前端的请求数量;另一方面代理服务器可以脱离HTTP的限制,使用更高效的通信协议与服务器通信;
(二). 页面渲染性能的优化
1. 防止阻塞渲染
页面中的css 和 js 会阻塞html的解析,因为他们会影响dom树和render树。为了避免阻塞,我们可以做这些优化:
- css 放在首部,提前加载,这样做的原因是: 通常情况下 CSS 被认为是阻塞渲染的资源,在CSSOM 构建完成之前,页面不会被渲染,放在顶部让样式表能够尽早开始加载。但如果把引入样式表的 link 放在文档底部,页面虽然能立刻呈现出来,但是页面加载出来的时候会是没有样式的,是混乱的。当后来样式表加载进来后,页面会立即进行重绘,这也就是通常所说的闪烁了。
- js文件放在底部,防止阻塞解析
- 一些不改变dom和css的js 使用
defer
和async
属性告诉浏览器可以异步加载,不阻塞解析
2. 减少重绘和回流
重绘和回流在实际开发中是很难避免的,我们能做的就是尽量减少这种行为的发生。
- js尽量少访问dom节点和css 属性
- 尽可能的为产生动画的 HTML 元素使用
fixed
或absolute
的position
,那么修改他们的 CSS 是不会 Reflow 的。 - img标签要设置高宽,以减少重绘重排
- 把DOM离线后修改,如将一个dom脱离文档流,比如
display:none
,再修改属性,这里只发生一次回流。 - 尽量用
transform
来做形变和位移,不会造成回流
3. 提高代码质量
这最能体现一个前端工程师的水平了,高性能的代码能在实现功能的同时,还兼顾性能。下面是一些好的实践:
1)html:
- dom的层级尽量不要太深,否则会增加dom树构建的时间,js访问深层的dom也会造成更大的负担。
- meta标签里需要定义文档的编码,便于浏览器解析
2)css:
- 减少 CSS 嵌套层级和选择适当的选择器,可参考如何提高css选择器性能
- 对于首屏的关键css 可以使用style标签内联。可参考什么是关键css
3)js:
- 减少通过JavaScript代码修改元素样式,尽量使用修改class名方式操作样式或动画
- 访问dom节点时需要对dom节点转存,防止循环中重复访问dom节点造成性能损耗。
- 慎用 定时器 和 计时器, 使用完后需要销毁。
- 用于复杂计算的js代码可以放在worker进程中运行
- 对于一些高频的回调需要对其节流和消抖,就是
debounce
和throttle
这两个函数。比如scroll
和touch
事件
...
优化没有正确答案,优化的手段也层出不穷,这里也无法概括全面,只列举了一些我了解过的。其实除了前端,后端也有许多可优化的地方,比如接口缓存啊,数据库缓存啊等等。这个本骚年就了解的不深了。
四、思考与总结
性能一直是前端开发很重要的一个课题。性能优化也是一条不见尽头的路,任重而道远啊~
参考文章:
https://segmentfault.com/a/11...
https://sylvanassun.github.io...
https://www.html5rocks.com/zh...
https://juejin.im/post/5a966b...
https://juejin.im/post/59672f...
https://tech.meituan.com/perf...
https://segmentfault.com/a/11...
https://www.jianshu.com/p/268...