1.为什么要做性能优化?
随着互联网的飞速发展,用户对网页响应速度的要求越来越高,移动端则更是如此。性能的好坏直接影响用户的体验。对于一个前端工程师,如何能够让页面加载更快,提高用户体验,如何减少请求所占带宽,降低服务器压力,是我们工作中必须要思考的。
2.从输入URL到页面加载
首先我们有必要了解下,在浏览器中输入url,到页面加载完成的整个链路。
1. DNS(域名系统)解析
输入URL后,需要找到这个URL对应的服务器,从而加载 HTML。为了查询服务器地址,第一步需要通过DNS将域名解析成IP地址。在解析过程中,浏览器首先依次查询自身缓存,系统缓存和路由器缓存的记录,如果没有则查询本地host文件,直到拿到ip地址,再没有就向DNS服务器发送域名解析请求。
2. TCP连接
建立连接时,两端主机必须同步双方的初始序号,并且发送确认的ACK。此过程就是三次握手。 简单图解一下就是这么回事。
sequenceDiagram
客户端->>服务器: 嘿,我想建立连接,我的初始序列号送你
服务器-->>客户端: 好的呀,礼尚往来,我的序列号也送你,确认要连接嘛?
客户端-)服务器: 确认,那我们开始聊天吧!
构建TCP请求会增加大量的网络时延。
3. HTTP 请求
客户端将要发送的内容构建成HTTP请求报文并封装在TCP包中,通过TCP协议发送到服务器指定端口。
4. 服务端响应请求
服务器端接收到客户端的HTTP请求后,查找客户端请求的资源,返回相应的HTML文件。
5. 页面渲染
- HTML文档被解析成一棵以document为根的DOM树,解析过程中如果遇到JavaScript,则会暂停解析并下载相应的文件造成阻塞。
- 浏览器解析CSS,构建CSSOM树。
- DOM树和CSSOM树融合成渲染树,然后浏览器确认页面各元素的位置。
- 浏览器根据布局结果进行页面绘制和优化。
3.思路
了解了上述整个过程,就可以清晰看到,性能优化可以从两方面入手,一是http层的优化,二是渲染层的优化。现在已经有很多成熟可靠的工具和方法可以帮助进行优化,另外也要注意实际开发中,应优先考虑有助于性能的写法和实现方式。下面来具体介绍一下吧。
4.http层优化
1.DNS预解析
DNS预解析技术,就是让具有此属性的域名不需要用户点击链接就在后台解析缓存,由于域名解析和内容载入是串行的操作,这样用户在点击链接时就不用DNS解析了,减少用户的等待时间,提升用户体验。
// 用meta信息来告知浏览器, 当前页面要做DNS预解析
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="www.baidu.com" />
// 只有部分浏览器支持
复制代码
2.使用HTTP/2
- HTTP/2 采用二进制格式传输数据,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效。
- HTTP/2的多路复用代替原来的串行化单线程和阻塞机制,所有请求的都是通过一个 TCP 连接并发完成。 HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制
- HTTP/2可以在发送页面HTML时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应
- HTTP/2对消息头进行压缩传输,能够节省消息头占用的网络流量。而HTTP/1.x每次请求,都会携带大量冗余头信息,浪费带宽资源。
根据上图的对比就可以发现HTTP/2的优势,资源同时加载,后面加载的资源不需要进行排队
3.减少http请求次数
下面看一个HTTP请求的例子。
从上图看出,真正下载数据的时间占比为 2.24 / 138.45 = 1.62%,文件越大,这个比例越大,如果将多个小文件合并为一个大文件,从而减少 HTTP 请求次数,那么将大大减少HTTP开销。
webpack 可以使用如下插件进行文件合并、打包和压缩,基本上能压缩50%以上。
- JavaScript:UglifyPlugin
- CSS :MiniCssExtractPlugin
- HTML:HtmlWebpackPlugin
4. gzip压缩文件
压缩文件可以减少文件下载时间,让用户体验性更好。上面介绍了webpack几个插件压缩,其实还可以做得更好,就是gzip。gzip 是目前最有效的压缩方法。举个例子,使用Vue开发的项目构建后生成的 app.js 文件大小为 1.4MB,使用 gzip 压缩后只有 573KB,体积减少了将近 60%。
启用gzip需要客户端和服务端的支持。请求头中 accept-encoding:gzip 表示来标识客户端对gzip压缩的支持。在http响应头中 content-encoding:gzip,表示服务端使用了gzip的压缩方式。
前端配置:
// 安装
npm install compression-webpack-plugin --save-dev
// webpack配置
const CompressionWebpackPlugin = require('compression-webpack-plugin');
plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',// 目标文件名
algorithm: 'gzip',// 使用gzip压缩
test: new RegExp(
'\\.(js|css)$' // 压缩 js 与 css
),
threshold: 10240,// 资源文件大于10240B=10kB时会被压缩
minRatio: 0.8 // 最小压缩比达到0.8时才会被压缩
})
);
复制代码
5.图片优化
使用字体图标iconfont代替图片,字体图标是矢量图,不会失真,且生成的文件特别小。
6.浏览器缓存
浏览器缓存分为强缓存和协商缓存。
1.强缓存
强缓存就是发现有缓存直接用,利用Expires (HTTP/1.0) 和Cache-Control (HTTP/1.1) 这两个请求头字段,使请求的内容缓存下来,避免了必要的HTTP请求
Expires头的内容是一个时间值,表示资源在本地的过期时间。在过期时间内,就直接使用缓存的资源,不会发送HTTP请求。Cache-Control(用的比较多)的作用也是类似的。
2.协商缓存
协商缓存就是先询问服务器缓存是否可以用,再判断是否用缓存
- 浏览器第一次请求,服务器在respone的header加上Last-Modified(最后修改时间)
- 浏览器再次请求,request的header上会加上If-Modified-Since,该值为缓存之前返回的Last-Modify
- 服务端拿到这两个值对比,如果相等的话,则命中缓存,返回304 Not Modified,但是不会返回资源内容,否则, 如果 Last-Modify > if-Modify-Since, 则会给出200响应,从服务器加载资源,并且更新Last-Modify为新的值。
5.浏览器渲染层优化
1.避免js阻塞
由于JS会阻塞浏览器渲染,一般将js放在在<body>
标签的底部,css放在<head>
标签内,防止出现白屏现象。
2.减少重绘和回流
什么操作会导致回流?
- 添加或者删除可见的DOM元素
- 元素位置改变
- 元素尺寸改变(边距、填充、边框、宽度和高度)
- 内容改变(比如文本改变或者图片大小改变)而引起的计算值宽度和高度改变
- 页面渲染初始化
- 浏览器窗口尺寸改变——resize事件 回流需要花费大量的时间进行样式计算和节点重绘与渲染,所以应当尽量减少回流次数
什么操作会导致重绘?
- 元素的属性或者样式发生变化,但不影响布局
如何减少重绘回流?
- 不要使用table布局,因为一个小改动可能会造成整个table重新布局。而且table渲染比较慢。
- 元素定义高度或最小高度,否则元素的动态内容载入时,会导致页面位置改变,造成回流。
- 用 Js 修改样式时,不要直接写样式,而是切换 class 来批量改变样式。
- 使用DocumentFragment合并DOM插入。
- 缓存DOM对象
//没有缓存dom
for (let i = 0; i < document.getElementsByTagName('p').length; i++) {
//每一次循环都会去查找tagName为p的元素,效率自然非常低
...
}
//缓存dom
var p = document.getElementsByTagName('p');
for (let i = 0; i < p.length; i++) {
// 提高查找效率
}
复制代码
3.图片懒加载
对于图片很多的网站来说,一次性加载全部图片,会对用户体验影响很大,使用图片懒加载,不仅提升用户体验,还能节省用户流量。
创建一个自定义属性data-src存放真正需要显示的图片路径,而img自带的src放一张大小为1 * 1px的图片路径,当图片滚动到可视区域内,用js取到该图片的data-src的值赋给src。
判断是否在可视区域可以用getBoundingClientRect,也可以用offsetTop-scroolTop<=clientHeight
判断
//html
<img src="img/loading.gif" alt="1" data-src="img/g1.jpg">
<img src="img/loading.gif" alt="2" data-src="img/g2.jpg">
<img src="img/loading.gif" alt="3" data-src="img/g3.jpg">
//js
<script>
var imgs = document.querySelectorAll('img');
//判断是否在可视区域内
function isInner(el) {
var bound = el.getBoundingClientRect();
var clientHeight = window.innerHeight;
return bound.top <= clientHeight;
}
function check() {
imgs.forEach(function(el){
if(!el.dataset.isLoaded && isInner(el)){
//在可视区域,且没下载过,下载图像
loadImg(el);
}
})
}
function loadImg(el) {
var source = el.dataset.src;
el.src = source;
el.dataset.isLoaded = 1
}
window.onload = window.onscroll = function () {
// 滚动触发
check();
}
</script>
复制代码
4.缩略图
对于一些很大的又不常用的图片,可以用缩略图的方式展示,当用户鼠标悬浮在上面才展示全图。
5.响应式图片
浏览器根据不同分辨率显示不同大小图片,既保证显示效果,又能节省带宽,提高加载速度
<picture>
<source srcset="banner_w1000.jpg" media="(min-width: 801px)">
<source srcset="banner_w800.jpg" media="(max-width: 800px)">
<img src="banner_w800.jpg" alt="">
</picture>
复制代码
6.事件代理
事件代理是指将事件监听器注册在父级元素上,由于子元素事件会通过事件冒泡向上传播到父节点,因此可以由父节点的监听函数统一处理多个子元素的事件。利用事件代理,可以减少内存使用,提高性能及降低代码复杂度。
7.事件节流
使用函数节流(throttle)或函数防抖(debounce),限制事件频繁触发。
- 函数节流: 函数节流的应用场景一般是onrize,onscroll等这些频繁触发的函数,比如你想获取滚动条的位置,然后执行下一步动作,如果监听后执行的是Dom操作,这样的频繁触发执行,可能会影响到浏览器性能,甚至会将浏览器卡崩。所以我们可以规定多少秒执行一次,这种方法叫函数节流
//限制500ms执行一次
// 方式1
var type = false;
window.onscroll = function(){
// 执行了一次后,500ms内再疯狂操作也没用
// 直到500ms之后才能执行下一次
if(type === true) return;
type = true;
setTimeout(()=>{
console.log("要执行的事");
type = false;
},500)
}
//方式2
var time = null;
window.onscroll = function(){
let curTime = new Date();
if(time==null){
time = curTime;
}
// 判断两次操作的时间差是否小于500ms
if((curTime-time)>500){
console.log("要执行的事");
}
}
复制代码
- 函数防抖 在特定的时间内没有触发执行条件,最后才执行一次就是函数防抖,应用场景:频繁操作点赞和取消点赞,高频查询list等,需要获取最后一次操作结果并发送给服务器
var timer = null;
function click(){
// 如果500ms内频繁操作,每次都会清除定时器再重新创建一个
// 直到最后一次操作,500ms后执行
clearTimeout(timer);
timer = setTimeout(()=>{
ajax(...);
},500)
}
复制代码
8.分页加载
对于数据量较大的列表,使用分页加载,减少服务器压力。
9.模块按需引用
在SPA等业务逻辑比较复杂的系统中,需要根据路由来加载当前页面需要的业务模块 ,按需引用,是一种很好的优化网页的方式。只在需要用到的时候进行加载相关模块,这种方式提高⾸屏加载速度,减轻了应用总体积。
webpack 提供了两种方法,优先选择import()语法,或使用require.ensure。
10.及时清理环境
消除对象引用,防止内存泄漏,清除定时器等。
6.检查性能
网站性能分为加载性能和运行性能
- 加载性能主要看白屏时间和首屏时间
- 白屏时间:指从输入网址,到页面开始显示内容的时间。将以下脚本放在
</head>
前面就能获取白屏时间。
<script>
new Date() - performance.timing.navigationStart
</script>
复制代码
- 首屏时间:指从输入网址,到页面完全渲染的时间。
在 window.onload 事件里执行
new Date() - performance.timing.navigationStart
即可获取首屏时间
- 检查运行性能 在实际应用中,需要根据项目自身情况选择合适的优化方式,配合 chrome 的开发者工具的performance,可以查看网站在运行时的性能。
点击左上角的灰色圆点,然后模仿用户使用网站,在使用完毕后,点击 stop,就能得到网站运行期间的性能报告。如果哪个处理上占用大量时间,那么可以考虑此处的性能优化了。