本篇文章主要从两个方面讲解页面渲染机制,即网络方面和渲染引擎方面。
网络
当用户访问页面时,浏览器需要获取用户请求内容,这个过程主要涉及浏览器网络模块。
- 用户在地址栏输入域名,比如,baidu.com
- DNS(又称域名解析系统,默认端口号53)协议,通过域名查找IP地址
浏览器DNS解析大多时候比较快,且会缓存常用域名的解析值,但是如果网站涉及多域名,在对每一个域名访问时都需要先解析出IP地址,而我们希望在跳转或者请求其他域名资源时尽量快,则可以开启域名预解析,浏览器会在空闲时提前解析声明需要预解析的域名。如下
即,在网页头部(head之间)增加rel属性为”dns-prefetch”的link标签,并在href中指定想要预解析的域名。
- 向该IP地址发起请求。请求过程如下
客户端:
HTTP协议生成针对目标Web服务器的HTTP请求报文;
为了方便通信,TCP协议将HTTP报文分割成报文段,把每个报文段可靠地传给对方;
ARP协议根据IP地址解析出对应的MAC地址,IP协议根据MAC地址传送报文。这期间可能会经过多个路由器,IP协议自动中转,直到找到目的MAC;
服务器端:
TCP协议从客户端接收到报文段,按序号以原来的顺序重组请求报文;
HTTP协议对Web服务器请求的内容进行处理。此时,服务器知道了客户端想要浏览 baidu.com 这个页面了;
- 浏览器获得并解析服务器的返回内容(HTTP Response)。响应过程如下
服务器端:
客户端:
- 浏览器加载HTML文件以及文件内包含的外部引用文件以及图片,多媒体等资源。
渲染搜索引擎(关键渲染路径)
渲染引擎所做的事情是将请求内容展现给我们,默认支持HTML,XML和图片类型,对于其他诸如PDF等类型的内容则需要安装响应插件,但浏览器的展示工作流程基本是一样的。
通过网络模块加载到HTML文件后,渲染引擎渲染流程如下
- 从 Head 标签开始逐行解析HTML代码,遇到 link 标签又会向服务器请求加载CSS文件,这个过程是异步加载。如果有多个CSS文件,会同时加载。
- 如果遇到 script 标签或者 js 文件,会立即执行,并且这个过程是异步的。
不同于CSS文件,js 是同步加载。即执行js文件时,浏览器不会做其他事情,只有js代码执行结束后,才会继续开始渲染页面。为了防止出现“空白页”现象,应该把 js 放到页面底部,也就是</body>标签前。
- 然后到 body 标签开始渲染页面,按照从上到下的顺序依次渲染dom节点。如果遇到 img 标签,会异步向服务器发送请求加载图片文件,浏览器会继续渲染页面,因为图片加载是异步的~
- 如果遇到了dom节点的变化,元素尺寸变化,浏览器不得不回头重新渲染这部分代码。
以上就是本篇文章介绍的浏览器页面渲染机制了,下面简单介绍下如何提升页面的渲染效率~
影响页面渲染速率
- 回流(reflow)
当浏览器发现页面某个部分发生了变化影响了布局,需要倒回去重新渲染,该过程称为回流。
- 重绘(repaint)
如果只是改变了某个元素的边框颜色、字体颜色、背景色等不影响它周围或内部布局的属性,将只会影响浏览器的重绘。
提升页面渲染速率
- CSS注意事项
用CSS动画替代js模拟动画的好处是:不占用js主线程;可以利用硬件加速;浏览器可对动画做优化。但CSS动画有时会出现卡顿现象
使用CSS3动画造成页面的不流畅和卡顿问题,其潜在原因往往还是页面的回流和重绘,减少页面动画元素对其他元素的影响是提高性能的根本方向。
1、设置动画元素 position 样式为absolute或 fixed,可避免动画的进行对页面其它元素造成影响,导致其重绘和重排的发生;
2、避免使用margin,top,left,width,height等属性执行动画,用 transform 进行替代;
下面有一个CSS动画,动画开始后,你会隐约感觉到动画不是那么流畅,即使使用电脑上的浏览器也会有些卡顿,更不要提在移动端达到 60 fps的流畅效果了。。。
.div {
animation: run-around 4s infinite;
}
@keyframes run-around {
0% {
top: 0;
left: 0;
}
25% {
top: 0;
left:200px;
}
50% {
top: 200px;
left: 200px;
}
75% {
top: 200px;
left: 0px;
}
}
为了解决这个问题,我们可以使用CSS transform中的 translate() 来代替 top 和 left。
.div {
animation: run-around 4s infinite;
}
@keyframes run-around {
0% {
transform: translate(0,0);
}
25% {
transform: translate(200px,0);
}
50% {
transform: translate(200px,200px);
}
75% {
transform: translate(200px,0);
}
}
现在动画看起来会好很多。因为transform属性不会出发浏览器的 repaint,而 top 和 left 会一直触发 repaint。为什么 transform 没有触发 repaint 呢?因为,transform 动画由GPU控制,支持硬件加速,并不需要软件方面的渲染。
GPU详细介绍,可以参考 https://www.jianshu.com/p/d1e16a2e88c1 这篇文章。
- JS注意事项
解决js同步加载问题(有下面三种方式)
1、将js文件放在页面底部,即</body>标签之前。因为html文件默认是按照顺序从上到下依次加载的,这样就可以先渲染dom节点,再加载js
2、使用 H5 的async属性,用法和特点如下
<script src = "test.js" anysc></script>
//加载脚本时不阻塞页面渲染
//使用这个属性的脚本中不能调用document.write方法
//可以只写属性名,不写属性值。写法如上
//H5新增属性
//脚本在下载后立即执行,同时会在window的load事件之前执行,所以有可能出现脚本执行顺序被打乱的情况
3、使用HTML的defer属性,用法和特点如下(前三点和anysc相同)
<script src = "test.js" defer></script>
//加载脚本时不阻塞页面渲染
//使用这个属性的脚本中不能调用document.write方法
//可以只写属性名,不写属性值。写法如上
//H4属性
//脚本在页面解析完之后,按照原本的顺序执行,同时会在document的DOMContentLoaded之前执行
避免频繁操作DOM元素
1、当需要很多的插入操作和改动,使用下面的代码会很有问题
var ul = document.getElementById("ul");
for (var i = 0;i < 20;i++){
var li = document.creatElement("li");
ul.appendChild(li);
}
由于每一次对文档的插入都会引起重新渲染(计算元素的尺寸、显示背景、内容等),所以进行多次插入操作使得浏览器发生了多次渲染,效率比较低。这是我们提倡减少页面的渲染来提高DOM操作的效率的原因。
createDocumentFragment()方法,是用来创建一个虚拟的节点对象,或者说,是用来创建文档碎片节点,它可以包含各种类型的节点,在创建之初是空的。它有一个很实用的特点,当请求把一个createDocumentFragment 节点插入文档树时,插入的不是createDocumentFragment 自身,而是它所有的子孙节点。这个特性使得createDocumentFragment 成了占位符,暂时存放那些一次插入文档的节点。
另外,当需要添加多个dom元素时,如果先将这些元素添加到createDocumentFragment 中,再统一将createDocumentFragment 添加到页面。因为文档片段存在于内存中,并不在DOM中,所以将子元素插入文档片段中不会引起回流(对元素位置和几何上的计算),因此,使用DocumentFragment可以起到性能优化的作用。可以将上面的代码改成下面的
var ul = documen.getElementById("ul");
var fragment = document.createDocumentFragment();
for (var i = 0;i < 20;i++){
var li = document.createElement("li");
li.innerHtml =" index: " + i;
fragment.appendChild(li);
}
ul.appendChild(fragment);
关于 createDocumentFragment() 方法更多介绍,请读者自行查阅~
2、设置DOM元素的display属性为none再操作该元素
var myElement = document.getElementById('myElement');
myElement.style.display = 'none';
…… //一些基于myElement的大量DOM操作
myElement.style.display = 'block';
3、复制DOM元素到内存中再对其进行操作
var old = document.getElementById('myElement');
var clone = old.cloneNode(true);
…… //一些基于clone的大量操作
old.parentNode.replaceChild(clone,old);
4、用局部变量缓存样式信息从而避免频繁获取DOM数据
比如访问一个元素的offsetWidth属性时,浏览器需要重新计算(重新布局),然后才能返回最新的值,如果这个动作发生在一个很大的循环中,那么浏览器就不得不进行多次重新布局,这可能会产生严重的性能问题。正确的做法是,先将这个值读出来,然后缓存在一个变量上(触发一次重新布局)。以便后续使用
//一般用法
for (var i = 0;i < paragraphs.length;i++) {
paragraphs.style.width = box.offsetWidth + 'px';
}
//优化性能的用法
var width = box.offsetWidth;
for (i = 0;i < paragraphs.length;i++){
paragraphs[i].style.width = width + 'px';
}
5、合并多次DOM操作
//一般用法
var left = 10;top = 10;
el.style.top = top;
el.style.left = left;
//优化性能写法
el.style.cssText += ";left: " + left + "px; top: " + top + "px;";
- 其他
除了CSS 和 JS 的改进能够提升页面渲染速率,还有其他方面的改进同样能够提升页面渲染速率。
1、资源压缩与合并。
HTML代码压缩:压缩在文本中有意义,而在HTML中不需要的字符。比如,空格、制表符、换行符,还有一些其他意义的字符,如HTML注释也可以被压缩。
CSS代码压缩:删除无效的代码和css语义合并。
JS的压缩和紊乱:使用在线网站压缩、使用html-minifier工具、使用uglifyjs2进行压缩。
文件合并:将多个js/css小文件合并为一个文件,减少网络请求次数。
注:css压缩与js的压缩和紊乱比html压缩收益要大的多,同时css代码和js代码比html代码多的多。所以,css与js代码压缩非常有必要!
2、浏览器缓存
缓存作用:对于web应用来说,缓存是提升页面性能同时减少服务器压力的利器。
强缓存:不会向服务器发送请求,直接从缓存中读取资源,在Chrome控制台的network选项中可以看到该请求的状态码是 200,但是 size 的标识为 from dist cache 或者 from memory cache
response header:response header里的过期时间,浏览器再次加载该资源时,如果在有效时间内,则使用强缓存。
Last-Modified 和 If-Modified-Since:二者都是记录页面最后修改时间的 HTTP 头信息,Last-Modified 是由服务器往客户端发送的HTTP, If-Modified-Since 是客户端往服务器端发送的头。
再次请求本地缓存的 cache 页面时,客户端会通过 If-Modified-Since 头先将服务器端发过来的 Last-Modified 最后修改时间戳发送回去,这是为了让服务器端进行验证,通过这个时间戳判断客户端的页面是否是最新的。如果不是最新的,则返回新的内容,如果是最新的,则返回 304 告诉客户端其本地 cache 的页面时最新的,于是客户端就可以直接从本地加载页面了,这样在网络上传输的数据就会大大减少,同时也减轻了服务器端的负担。而在一些ajax应用中,要求回获取的数据永云是最新的,而不是读取缓存中的数据,做这样的设置是很有必要的。
3、CDN预解析
CDN服务提供商会有全国各个省份部署节点, 将网站静态资源部署到CDN后, 用户在访问页面时, CDN静态资源会从就近的CDN节点上加载资源. 当请求至达CDN节点后, 节点会判断资源是缓存是否有效, 若有效, 直接返回给用户, 若无效, 会从CDN服务器加载最新的资源返回给用户同时将资源保存一份到该CDN节点上, 以便后续的访问用户使用. 因此, 只在该地区有一个用户先加载了资源, 在CDN中建立了缓存, 该地区的其他用户都能受益。
4、DNS预解析