web的基本性能优化
本文通过介绍从输入网址到网页加载完成的过程中,浏览器的各类行为,分析各个步骤的关键因素来优化网页的性能
在了解web性能优化前,我们需要知道一些基本的概念:
前端性能优化有一个指标,叫做“关键路径渲染优化”,指的是从DOM树构建到绘制到屏幕上的整个过程中的优化因素
关于首屏和白屏时间:
- 白屏时间是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。
- 首屏时间是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。
- 白屏时间 = 地址栏输入网址后回车 - 浏览器出现第一个元素
- 首屏时间 = 地址栏输入网址后回车 - 浏览器第一屏渲染完成 -影响白屏时间的因素:网络,服务端性能,前端页面结构设计。
- 影响首屏时间的因素:白屏时间,资源下载执行时间。
DOMContentLoaded
是指页面元素加载完毕($(document).ready就是监听该方法),此时的一些静态资源如图片还不可见,但是页面是可以交互的;load
是值所有资源都加载完毕($(document).load)监听的就是该事件
浏览器的结构
1.用户界面
2.浏览器引擎
在用户界面和渲染引擎之间传输指令
3.渲染引擎
负责显示请求的内容,解析获取到的资源将其呈现在屏幕上。chrome用的是Blink,firefox用的是Gecko。。。
4.网络
5.用户界面后端
绘制浏览器的基本组件,如窗口,由系统调用
6.JavaScript解释器
解析和执行js
7.数据存储
在硬盘上存储各类数据,如cookie、localstorage
基本的工作流程
1.加载内容
当我们在浏览器地址栏中输入网址后,浏览器会提交URL,DNS服务器进行域名解析后,再向对应的服务器发起请求,接收文件(包括了html、css、js、静态资源等等)
2.解析资源
浏览器在收到服务端发来的资源后,会进行解析,建立对应的结构树(如html的DOM树,CSSOM树)
HTML的解析
一般的,浏览器引擎从网络获取到文档后,会将文档分成8kb大小的分分块进行传输。
HTML解析器对HTML文档进行解析,生成解析树(以DOM元素以及属性作为节点的树)
浏览器对于html的解析具有一定的容错机制,会纠正错误无效的内容,尽管如此我们也应该避免。
解析树构建完成后,浏览器开始加载网页的外部资源,首先进行css样式文件的解析,每个css文件都会被解析成一个样式表对象。
css的样式匹配是从左往右进行的,如果换成从右往左,那么就要找到最顶层的元素再依次向下寻找,如果都不匹配就回到最顶层元素,通过另外一条路径进行匹配,这样就大大增加了成本。所以在书写css的时候应该尽可能高效:
- 不要在ID选择器前使用标签名
- 尽量不要在class选择器前使用标签名
- 尽量减少嵌套层级
- 避免使用通配符
- 避免使用后代选择符,尽量使用子选择符,子元素匹配符的概率要大于后代元素匹配符
- 减少内联css
3.渲染
将前面解析到的DOM和CSSOM合并成为一个渲染树(render),构建渲染树时,对每个元素进行位置、样式的计算,生成布局(Layout),然后将内容绘制在屏幕上(Paint)
阻塞解析和阻塞渲染的因素
渲染树是有DOM树和CSSOM树构建而成,所以,DOM和CSSOM的构建会影响到后续的渲染。其中,js的解析会阻塞DOM树的渲染(因为js的操作可能引起回流,且浏览器无法预估它会有什么具体的操作,并且js是加载后马上执行的),而css不会阻塞DOM的解析但会阻塞DOM的渲染(前提是没有js对DOM和样式进行处理)。
并且,CSSOM的构建会影响到js的执行,如果在浏览器未完成CSSOM的下载和构建时,执行js,那么浏览器会延迟js的执行和DOM的构建,直到完成CSSOM的下载和构建。我们可以理解为,CSSOM构建完成时间是早于js的执行的,二者是早于DOMContentLoaded(DOM构建彻底完成)的。
正因为如此,我们也可以很好理解,为什么<script>
标签通常是放在html文档下面,而<link>
放在顶部。
为了防止js阻塞DOM的解析,我们应该尽可能合并脚本,并且将script放在页面body闭合标签前,也可以在<script>
标签内增加defer
和async
属性,或者通过js动态插入脚本。
defer
和async
的区别:
- async在加载和渲染后续文档的过程中将和js的加载与执行并行进行
- defer在加载后续文档的过程中将和js的解析并行进行,但是js的执行要在所有的元素解析完成后,DOMContentLoaded事件触发之前完成。使用
defer
的js执行不一定按照顺序进行,谁先加载完成就先执行,所以显得不是很严谨
用js来动态加载:
function loadScript(url, callback){
var script = document.createElement('script');
script.type = 'text/javascript';
if(script.readyState){ //IE下
script.onreadystatechange = function(){
if(script.readyState == 'loaded' || script.readyState == 'complete'){
script.onreadystatechange = null;
callback();
}
}
}else{ //其他浏览器
script.onload = function(){
callback();
}
}
script.src = url;
document.getElementsByTagName('head')[0].appendChild(script);
}
CSSOM构建的优化:
在渲染树的关键渲染路径中,CSSOM也扮演着很重要的角色,我们应该想办法尽可能精简它,除此,我们还可以使用媒体类型和媒体查询来接触对渲染的阻塞:
<link href="index.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">
- 第一个资源会加载并阻塞。
- 第二个资源设置了媒体类型,会加载但不会阻塞,print 声明只在打印网页时使用。
- 第三个资源提供了媒体查询,会在符合条件时阻塞渲染。
至于一些图片之类的静态资源,实际上,它们并不会阻塞首屏的渲染(所以有些网页的结构已经加载完成了,但是图片还没加载完),但是我们也应该考虑用户的体验优化图片的加载,雪碧图和懒加载是我们常用的一些方法。
简单概括一下阻塞解析和阻塞渲染的因素:
javascript阻止DOM构建(DOMCommentLoaded触发被延迟),css的下载和完成阻止javascript的执行。在没有javascript或者只含有异步javascript的页面中,DOM的构建和CSSOM的构建互不影响。
网页首次加载完成后的性能优化
网页加载完成后,我们还是可能会对DOM进行操作,这个过程同样会涉及到网页的性能优化。
重绘(Reflow)和回流(Repaint)
它们的关系是:回流会导致重绘,重绘不一定会回流
重绘:当页面中元素样式的改变不影响它在文档流中的位置时,比如颜色、背景色、透明度等,浏览器会根据样式的变化重新对变化的元素进行绘制
回流:当文档中的元素尺寸、结构等属性发生变化时,浏览器会重新渲染部分或全部的文档的过程 会触发回流的的操作:
页面首次渲染
浏览器窗口尺寸改变
元素尺寸或位置改变(position为absolute和fixed时的位置变化不触发)
元素内容变化
添加或删除可见的DOM元素
激活css伪类
通过js获取某些属性或方法,比如clientWidth、getComputedStyle、getBoundingClientRect......
如何避免回流:
CSS的操作:
- 避免使用
<table>
- 尽可能在DOM树末端改变class
- 避免设置多层内联样式
- 将动画应用到position为absolute或fixed的元素上
- 避免使用css表达式如calc
js的操作:
- 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
- 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
- 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
- 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
其他的性能优化
关于动画
最普通的动画,我们可能会使用setTimeout
来实现,但是由于不同显示器的刷新率可能不一样,可能会导致动画掉帧的现象,所以,我们可以用css3动画或者requestAnimationframe
来优化
关于DOM的监听
在js中,我们可能会对DOM做长时间的监听,如果事件触发很频繁,但是对用户体验却毫无意义,我们可以使用防抖和节流来优化
实验性的功能
preload
preload 提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),在需要执行的时候再执行。它给我们带来的好处:
- 将加载和执行分离,防止阻塞渲染
- 优化用户体验,不会出现网页结构加载完成而部分静态资源还没加载出来
哪些类型的内容可以被预加载?
参考:MDN
怎么用?
一个基本的例子:
<link rel="preload" href="style.css" as="style">
<link rel="preload" href="main.js" as="script">
注意,要使用 as
来指定将要预加载的内容的类型
如果涉及到跨域获取资源,我们可以设置crossorigin
属性:
<link rel="preload" href="fonts/cicle_fina-webfont.svg" as="font" type="image/svg+xml" crossorigin="anonymous">
如果是需要响应式开发,可以使用媒体查询,像下面这样:
<link rel="preload" href="bg-image-narrow.png" as="image" media="(max-width: 600px)">
<link rel="preload" href="bg-image-wide.png" as="image" media="(min-width: 601px)">
预加载的资源的优先级全部会提高,所以一些不确定的资源也是会被预先加载的,如果盲目使用预加载,势必增加移动端用户的流量
预加载机制的兼容性较差,目前IE都无法兼容,所以谨慎使用
prefetch
prefetch允许浏览器在后台(空闲时)获取将来可能用得到的资源,并且将他们存储在浏览器的缓存中
使用类似preload:<link rel="prefetch" href="/uploads/images/pic.png">
preload 和 prefetch 混用的话,并不会复用资源,而是会重复加载。
移动端的一些性能优化
- 不要给非static的定位元素增加css动画,比如relative、absolute
- 尽可能设置硬件加速,比如在渲染图片时可以使用canvas绘图。或者给元素设置
transform: translate3d(0,0,0)
,这样元素本身没有外观上的变化,但是它的渲染会触发GPU加速