2024/04/07
引言: 转前端一年了,期间工作较忙,也没时间整理一些知识体系,此系列文章是对前端基础的一些回顾与总结。本文主要介绍浏览器工作的原理以及一些优化手段。
以chorme为例:
需要注意的是,NPAPI是指浏览器对系统或外部的一些程序的调用接口,比如播放视频的 flash 插件,而Pepper其实是基于NPAPI改进的插件架构。
主流的浏览器内核主要有2种,Webkit 和 Geoko ,虽然 chorme 现在的内核更换为 blink ,但其实 blink是基于webkit的,差异不大。其渲染过程分别如下:
这两个内核的渲染流程大同小异,主要的过程可以总结为下列5个:
使用chorme浏览器的开发者工具,我们很容易看到这5个过程的时间线,下面是segmentfault主页的渲染截图:
可以看到上述流程的耗时,甚至可以统计到每一帧的耗时分布,从而对影响渲染性能的代码精确定位。其中黄色为JS,紫色为Style和Layout,绿色为Paint和Composite部分,选中每个部分会显示各自的花费时间等信息,可以看出这个图片中JS运行的时间太长。目前的显示设备一般刷新率是60FPS,所以理想中每帧的时间最好为16毫秒。
需要注意的一点是,这里的步骤执行并没有特定的顺序,为保证渲染的速度,浏览器一开始接收到html时就开始执行解析的过程,并且遇到需要重绘和重排的时候会重复执行这些步骤,下面我们详细介绍一下这5个过程。
浏览器在接收到html文件后即开始解析和构建DOM树,在碰到js代码段时,由于js代码可能会改变dom的结构,所以为避免重复操作,浏览器会停止dom树构建,先加载并解析js代码。而对于css,图片,视频等资源,则交由资源加载器去加载,这个过程是异步的,并不会阻碍dom树的生成。这个过程需要注意的点是:
浏览器在碰到 和 标签时,会解析css生成cssom , 当然,link标签需要先将css文件加载完成才能解析。
需要注意的是:
在cssom 和dom 树都构建完成后,浏览器会将他们结合,生成渲染对象树,渲染树的每一个节点,包含了可见的dom节点和节点的样式 。
需要注意的是:
这一步是浏览器遍历渲染对象树,并根据设备屏幕的信息,计算出节点的布局、位置,构建出渲染布局树(render layout)。渲染布局树输出的就是我们常说的盒子模型,需要注意的是:
浏览器对生成的布局树进行绘制,由用户界面后端层将每个节点绘制出来。此时,Webkit内核还需要将渲染结果从Renderer进程传递到Browser进程。
前面讲到,js代码可以访问和修改dom节点和css,所以在解析js的过程中会导致页面重新布局和渲染,这就是重绘(repaint)和回流(reflow)。
重绘是指css样式的改变,但元素的大小和尺寸不变,而导致节点的重新绘制。
任何对元素样式,如、、 等属性的改变。css 和 js 都可能引起重绘。
回流(reflow)是指元素的大小、位置发生了改变,而导致了布局的变化,从而导致了布局树的重新构建和渲染。
当然,我们的浏览器不会每一次reflow都立刻执行,而是会积攒一批,这个过程也被成为异步reflow,或者增量异步reflow。但是有些情况浏览器是不会这么做的,比如:resize窗口,改变了页面默认的字体,等。对于这些操作,浏览器会马上进行reflow。
优化并不是无目的的,而是通过分析页面各个维度,找到亟待优化的方向或者具体到某段代码。下面就讨论一下如何对页面做性能分析和测速监控。
chorme得devtools相信所有的前端开发者都用过,它不仅提供了日常开发中极强的调试能力,同时也具备着极强的页面分析能力。
一般来说,我们打开一个页面,期望的是页面的响应和呈现速度和流畅的交互体验。所以,页面的测速指标可以大致概括为: 白屏时间,首屏时间,可交互时间。
window.performance是w3c提供的用来测量网页和Web应用程序的性能api。其中performance timing提供了延时相关的性能信息,可以高精度测量网站性能。timing的整体结构如下图所示:
关于性能优化,涉及的方向太广了,从网络请求到数据库,整条链路都有其可优化的地方。这里我只总结一下前端比较需要关注的一些优化点。这里从两个个维度进行讨论:
从上文可知,浏览器渲染网页的前提是下载相关的资源,html文档、css文档、图片资源等。这些资源是客户端基于HTTP协议,通过网络请求从服务器端请求下载的,大家都知道,有网络,必定有延迟,而资源加载的网络延迟,是页面缓慢的一个重要因素。所以,如何使资源更快、更合理的加载,是性能优化的必修课。
由于HTTP的限制,在建立一个tcp请求时需要一些耗时,所以,我们对资源进行合并、压缩,其目的是减少http请求数和减小包体积,加快传输速度。
segmentfault.com 的雪碧图图标
将一些静态资源文件托管在第三方CDN服务中,一方面可以减少服务器的压力,另一方面,CDN的优势在于,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,保证资源的加载速度和稳定性。
缓存的范围很广,比如协议层的DNS解析缓存、代理服务器缓存,到客户端的浏览器本地缓存,再到服务端的缓存。一个网络链路的每个环节都有被缓存的空间。缓存的目的是简化资源的请求路径,比如某些静态资源在客户端已经缓存了,再次请求这个资源,只需要使用本地的缓存,而无需走网络请求去服务端获取。
segmentfault 的主页的一些静态资源使用了缓存,上面是一些控制缓存的header首部字段
分片指得是将资源分布到不同的主机,这是为了突破浏览器对同一台主机建立tcp连接的数量限制,一般为6~8个。现代网站的资源数量有50~100个很常见,所以将资源分布到不同的主机上,可以建立更多的tcp请求,降低请求耗时,从而提升网页速度。
从segmentfault 的主页请求可以看出,网站将静态js文件和图片都放在了不同的子域名下。
可以升级我们的网络协议,比如使用HTTP2,quic 之类的,代替之前的http1.1,从协议层优化资源的加载。可以参考我之前的文章。
虽然做好了静态数据的加载优化,但是还是会出现一种情景,即静态数据已经加载完毕,但页面还是在转菊花,页面还没有进入可交互状态,这是因为现如今的网站开发模式,前后端分离已经成为主流,不再由php或jsp服务端渲染前端页面,而是前端先加载静态数据,再通过ajax异步获取服务器的数据,进而重新渲染页面。这就导致了异步从接口获取数据也是网页的一个性能瓶颈。响应缓慢,不稳定的接口,会导致用户交互体验极差,页面渲染速度也不理想。比如点击一个提交数据的按钮,接口速度慢,页面上菊花需要转好久才能交换完数据。
为了提升用户体验,我们认为首屏的渲染速度是极为重要的,用户进来页面,首页可见区域的加载可以由服务端渲染,保证了首屏加载速度,而不可见的部分则可以异步加载,甚至做到子路由页面的预加载。业界已经有很多同构直出的方案,比如vue的nuxt , react的beidou等。
前端经常有这样的场景,完成一个功能需要先请求第一个接口获得数据,然后再根据数据请求第二个接口获取第二个数据,然后第三、第四...前端通常需要通过promise或者回调,一层一层的then下去,这样显然是很消耗性能的
通常后台接口都按一定的粒度存在的,不可能一个接口满足所有的场景。这是不可避免的,那么如何做到只发送一个请求就能实现功能呢?有一种不错的方案是,代理服务器实现请求合并,即后台的接口只需要保证健壮和分布式,而由nodejs(当然也可以使用其他语言)建设一层代理中间层,流程如下图所示:
前端只需要按找约定的规则,向代理服务器发起一次请求,由代理服务器向接口服务器发起三次请求,再将目标数据返回给客户端。这样做的好处是:一方面是代理服务器代替前端做了接口合并,减少了前端的请求数量;另一方面代理服务器可以脱离HTTP的限制,使用更高效的通信协议与服务器通信;
页面中的css 和 js 会阻塞html的解析,因为他们会影响dom树和render树。为了避免阻塞,我们可以做这些优化:
重绘和回流在实际开发中是很难避免的,我们能做的就是尽量减少这种行为的发生。
这最能体现一个前端工程师的水平了,高性能的代码能在实现功能的同时,还兼顾性能。下面是一些好的实践:
...
优化没有正确答案,优化的手段也层出不穷,这里也无法概括全面,只列举了一些我了解过的。其实除了前端,后端也有许多可优化的地方,比如接口缓存啊,数据库缓存啊等等。这个本骚年就了解的不深了。
性能一直是前端开发很重要的一个课题。性能优化也是一条不见尽头的路,任重而道远啊~
参考文章:
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...