8

前端性能优化总结

资源优化

缓存

最好的资源优化就是不加载资源。缓存也是最见效的优化手段。说实话,虽然说客户端缓存发生在浏览器端,但缓存主要还是服务端来控制,与我们前端关系并不是很大。但还是有必要了解一下。

缓存包括服务端缓存和客户端缓存,本文只谈客户端缓存。所谓客户端缓存主要是http缓存。http缓存主要分为强制缓存和协商缓存。

强制缓存
  • Expires(http1.0)

在http1.0中使用Expires来做强制缓存。Exprires的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差。

  • Cache-Control

Cache-Control有很多属性,不同的属性代表的意义也不同。

  1. private:客户端可以缓存
  2. public:客户端和代理服务器都可以缓存
  3. max-age=t:缓存内容将在t秒后失效
  4. no-cache:需要使用协商缓存来验证缓存数据
  5. no-store:所有内容都不会缓存。
协商缓存

浏览器第一次请求数据时,服务器会将缓存标识与数据一起响应给客户端,客户端将它们备份至缓存中。再次请求时,客户端会将缓存中的标识发送给服务器,服务器根据此标识判断。若未失效,返回304状态码,浏览器拿到此状态码就可以直接使用缓存数据了。

  • Last-Modified

服务器在响应请求时,会告诉浏览器资源的最后修改时间

  • if-Modified-Since

浏览器再次请求服务器的时候,请求头会包含此字段,后面跟着在缓存中获得的Last-Modified(最后修改时间)。服务端收到此请求头发现有if-Modified-Since,则与被请求资源的最后修改时间进行对比,如果大于被请求资源最后修改时间则返回304,浏览器从缓存获取资源。如果小于被请求资源最后修改时间,则返回200,并返回最新资源,浏览器从服务端获取最新的资源,并缓存。

  • Etag

由服务器生成的每个资源的唯一标识字符串

  • If-None-Match

再次请求服务器时,浏览器的请求报文头部会包含此字段,后面的值为在缓存中获取的标识。服务器接收到次报文后发现If-None-Match则与被请求资源的唯一标识进行对比。如果相同,说明资源没有被修改过,返回304,浏览器从缓存获取资源,如果不同说明资源被修改过,则返回200,并返回最新资源,浏览器从服务端获取最新资源,并缓存。

Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

如果使用前端打包工具,可以在打包文件时候在给文件添加版本号或者hash值,同样可以区分资源是否过期。

减少http请求

  • 使用CDN托管静态资源
  • 可以借助gulp、webpack等打包工具对js、css等文件合并与压缩
  • 图片懒加载、按需加载,当滚动到图片可视区域才去加载图片
  • 小图片并且基本不会改动的图片使用base64编码传输。base64不要滥用,即使小图片经过base64编码之后也会生成很长的字符串,如果滥用base64反而会适得其反
  • 雪碧图,这个也是针对基本不会更改的图片才使用雪碧图,因为如果一张图片修改,会导致整个雪碧图重新生成,如果乱用也会适得其反。

减小http请求资源体积

  • 借助webpack、gulp等工具压缩资源
  • 服务端开启gzip压缩(压缩率非常可观,一般都在30%之上)
  • 如果有用打包工具,打包优化要做好,公共资源、提取第三方代码、不需要打包的库...

渲染优化

读过前面js运行机制的应该知道,从浏览器输入url,到页面出现在屏幕上,都发生了哪些事(tcp握手、dns解析等不在认知范围)。
  • FPS 16ms 小于10ms完成最好 Google devtool 查看帧率

如果浏览器FPS到达60,就会显得比较流畅,大多数显示器的刷新频率是60Hz,浏览器会自动按照这个频率刷新动画。
按照FPS等于60来计算,平均一帧的时间为1000ms/60 = 16.7ms,所以每次渲染时间不能超过16ms,如果超过这个时间就会出现丢帧、卡顿现象。

可以在chrome浏览器开发者工具中的Timeline中查看刷新率,可以查看所有帧率耗时情况以及某一帧的执行情况。Timeline的使用教程:https://segmentfault.com/a/11...

为了保证正常的FPS,有些渲染性能优化还是有必要的。下面所介绍的都是有关渲染优化的策略。

  • 尽量使用css3来做动画

总所周知,css的性能要比js快,所以能使用css,尽量不用js来实现

  • 避免使用setTimeout或setInterval,尽量使用requestAnimationFrame来做动画或者高频Dom操作。

因为setTimeout和setInterval无法保证callback函数的执行时机,很可能在帧结束的时候执行,从而导致丢帧,但是requestAnimationFrame可以保证callback函数在每帧动画开始的时候执行
requestAnimationFrame的中文MDN地址:https://developer.mozilla.org...

  • 复杂的计算操作使用Web Workers

如果有需要复杂的数据操作,比如对一个有一个个元素的数组遍历求和,那么Web Workers在适合不过了。

Web Workers可以让JavaScript脚本运行在后台线程(类似于创建一个子线程),而后台线程不会影响到主线程中的页面。不过,使用Web Workers创建的线程是不能操作DOM树。
有关Web Workers的更多可以查看MDN详解:https://developer.mozilla.org...

  • css放在头部,js放在尾部。

读过前面js运行机制的应该知道页面渲染是怎样一个过程,不再赘述了。css放在头部会避免生成html树之后重新布局的闪屏现象,js一般对页面的影响较大,一般放在尾部最后执行。

  • 事件防抖(debounce)与节流(throttle)

针对高频触发的事件(mousemove、scroll)等事件,如果不加以控制会在短时间内触发很多事件。

函数防抖是指频繁触发的情况下,只有足够的空闲时间,才执行代码一次。场景:注册时邮箱的输入框,随着用户的输入,实时判断邮箱格式是否正确,当第一次输入事件触发,设置定时:在800ms之后执行检查。假如只过了100ms,上次的定时还没执行,此时清除定时,重新定时800ms。直到最近一次的输入,后面没有紧邻的输入了,这最近一次的输入定时计时结束,终于执行了检查代码。

const filter  = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;  
$("#email").on("keyup",checkEmail());  
function checkEmail(){  
    let timer=null;  
    return function (){  
        clearTimeout(timer);  
        timer=setTimeout(function(){  
            console.log('执行检查');  
        },800);  
    }  
}  

函数节流是指一定时间内js方法只跑一次。就是本来一秒要执行100次的变成一秒执行10次。
场景:函数节流应用的实际场景,多数在监听页面元素滚动事件的时候会用到。

var canRun = true;
document.getElementById("throttle").onscroll = function(){
    if(!canRun){
        // 判断是否已空闲,如果在执行中,则直接return
        return;
    }

    canRun = false;
    setTimeout(function(){
        console.log("函数节流");
        canRun = true;
    }, 300);
};
  • Dom操作

前端开发人员都知道Do操作是非常耗时的(曾亲测过30*30的表格遍历添加样式)。所以尽量避免频繁的Dom操作,如果避免不了就尽量对DOm操作做优化。

1.:缓存Dom查询,比如通过getElementByTagName('div')获取Dom集,而不是逐个获取。

2: 合并Dom操作,使用createDocumentFragment()
    var frag = document.createDocumentFragment()
    for (i<10) {
        var li = document.createElement('li')
        frag.appendChild(li)
    }
    document.body.appendChild(frag)
 3: 使用React、Vue等框架的虚拟dom(原理目前还不明白),可以更快的实现dom操作。

  • 尽量避免重绘(rePaint)和回流(reFlow)

如果使用js修改元素的颜色或者背景色就会触发重绘,重绘的开销还是比较昂贵的,因为浏览器会在某一个DOM元素的视觉效果改变后去check这个DOM元素内的所有节点。

如果修改元素的尺寸和位置就会发生回流,回流开销更大,它会在某一个DOM元素的位置发生改变后触发,而且它会重新计算所有元素的位置和在页面中的占有的面积,这样的话将会引起页面某一个部分甚至整个页面的重新渲染。

  • css3硬件加速

浏览器渲染时,会分为两个图层:普通图层和复合图层。

普通文档流内可以理解为一个复合图层,absolute、fixed布局虽然可以脱离普通文档流,但它仍然属于普通图层,不会启动硬件加速。上面说的重绘(rePaint)和回流(reFlow)说的就是普通图层上的重绘和回流。

复合图层会启动硬件加速。和普通图层不在同一个图层,所以复合图层不会影响普通图层,如果一个元素被提升到复合图层,再操作该元素时,就不会引起普通图层的重绘和回流,从而提升渲染性能。

如何启动硬件加速:

1.使用translate3d和translateZ

webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0);

webkit-transform: translate3d(0,0,0);
-moz-transform: translate3d(0,0,0);
-ms-transform: translate3d(0,0,0);
-o-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);

2.使用opacity
需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态

3.使用will-chang属性
这个属性比较不常用,一般配合opacity与translate使用

针对webkit浏览器,启用硬件加速有些时候可能会导致浏览器频繁闪烁或抖动,可以使用下面方法消除:

-webkit-backface-visibility:hidden;
-webkit-perspective:1000;
如果使用硬件加速,请使用z-index配合使用, 因为如果这个元素添加了硬件加速,并且index层级比较低, 那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且releative或absolute属性相同的), 会默认变为复合层渲染,如果处理不当会极大的影响性能
  • 避免强制同步布局和布局抖动

浏览器渲染过程为:js/css(javascript) > 计算样式(style) > 布局(layout) > 绘制(paint) > 渲染合并图层(Composite)

JavaScript:JavaScript实现动画效果,DOM元素操作等。
Style(计算样式):确定每个DOM元素应该应用什么CSS规则。
Layout(布局):计算每个DOM元素在最终屏幕上显示的大小和位置。
Paint(绘制):在多个层上绘制DOM元素的的文字、颜色、图像、边框和阴影等。
Composite(渲染层合并):按照合理的顺序合并图层然后显示到屏幕上。

在js中如果读取style属性的某些值就会让浏览器强行进行一次布局、计算,然后再返回值,比如:

offsetTop, offsetLeft, offsetWidth, offsetHeight

scrollTop/Left/Width/Height

clientTop/Left/Width/Height

width,height

请求了getComputedStyle(), 或者 IE的 currentStyle

所以,如果强制浏览器在执行JavaScript脚本之前先执行布局过程,这就是所谓的强制同步布局。
比如下面代码:

requestAnimationFrame(logBoxHeight);

// 先写后读,触发强制布局
function logBoxHeight() {
    // 更新box样式
    box.classList.add('super-big');

    // 为了返回box的offersetHeight值
    // 浏览器必须先应用属性修改,接着执行布局过程
    console.log(box.offsetHeight);
}

// 先读后写,避免强制布局
function logBoxHeight() {
    // 获取box.offsetHeight
    console.log(box.offsetHeight);

    // 更新box样式
    box.classList.add('super-big');
}

在JavaScript脚本运行的时候,它能获取到的元素样式属性值都是上一帧画面的,都是旧的值。因此,如果你在当前帧获取属性之前又对元素节点有改动,那就会导致浏览器必须先应用属性修改,结果执行布局过程,最后再执行JavaScript逻辑。

如果连续多次强制同步布局,就会导致布局抖动
比如下面代码:

function resizeAllParagraphsToMatchBlockWidth() {
  for (var i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  }
}

作者:SylvanasSun
链接:https://juejin.im/post/59da456951882525ed2b706d
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

我们知道浏览器是一帧一帧的刷新页面的,对于每一帧,上一帧的布局信息都是已知的。
强制布局就是使用js强制浏览器提前布局,比如下面代码:

// bed  每次循环都要去获取left ,就会发生一次回流
function logBoxHeight() {
  box.style.left += 10
  console.log(box.style.left)
}

// goog 
var width = box.offsetWidth;

function resizeAllParagraphsToMatchBlockWidth() {
  for (var i = 0; i < paragraphs.length; i++) {
    // Now write.
    paragraphs[i].style.width = width + 'px';
  }
}
  • DOMContentLoaded与Load

DOMContentLoaded 事件触发时,仅当DOM加载完成才触发DOMContentLoaded,此时样式表,图片,外部引入资源都还没加载。而load是等所有的资源加载完毕才会触发。

1. 解析HTML结构。
2. 加载外部脚本和样式表文件。
3. 解析并执行脚本代码。
4. DOM树构建完成。//DOMContentLoaded
5. 加载图片等外部文件。
页面加载完毕。//load
  • 视觉优化

等待加载时间可以合理使用loading gif动图一定程度上消除用户等待时间的烦躁感

代码性能

代码对性能的影响可大可小,但是养成一个良好的写代码习惯和高质量的代码,会潜移默化的提高性能,同时也能提高自己的水平。废话不多说,直接看我总结的部分要点(因为这一部分知识点太多,需要大家写代码的时候多多总结)。

  • 避免全局查找

访问局部变量会比访问全局变量快,因为js查找变量的时候现在局部作用局查找,找不到在逐级向上找。

// bad
function f () {
    for (...){
        console.log(window.location.href)
    }
}

//good
function f () {
    var href = window.location.href
    for (...){
        console.log(href)
    }
}
  • 循环技巧
// bed 
for(var i = 0; i < array.length; i++){
    ....
}
// good
for(var i = 0, len = array.length; i < len; i++){
    ....
}
// 不用每次查询长度
  • 不要使用for in 遍历数组

for in是最慢的,其他的都差不多,其中直接使用for循环是最快的。for in只是适合用来遍历对象。

  • 使用+''代替String()吧变量转化为字符串
var a = 12
//bad
a = String(a)

// good
var a = 12
a = a + ''

这个还有很多类似的,比如使用*1代替parseInt()等都是利用js的弱类型,其实这样对性能提升不是很大,网上有人测试过,进行十几万次变量转换,才快了零点几秒。

  • 删除dom

删除dom元素要删除注册在该节点上的事件,否则就会产生无法回收的内存,在选择removeChild和innerHTML=''二者之间尽量选择后者,据说removeChild有时候无法有效的释放节点(具体原因不明)

  • 使用事件代理处理事件

任何可以冒泡的事件都可以在节点的祖先节点上处理,这样对于子节点需要绑定相同事件的情况就不用分别给每个子节点添加事件监听,而是都提升到祖先节点处理。

  • 通过js生成的dom对象必须append到页面中

在IE下,js创建的额dom如果没有添加到页面,这部分内存是不会被回收的

  • 避免与null比较

可以使用下面方法替换与null比较
1.如果该值为引用类型,则使用instanceof检查其构造函数
2.如果该值为基本类型,使用typeof检查类型

  • 尽量使用三目运算符代替if else
if(a>b){num = a}
else{num = b}

// 可以替换为
num = a > b ? a : b
  • 当判断条件大于3中情况时,使用switch代替if

因为switch的执行速度比if要快,也别是在IE下,速度大约是if的两倍

先总结这么多,其实性能优化还有很多,像预加载、服务端渲染、css选择器优化等等。等有机会再总结

zhuqitao
498 声望80 粉丝

心有猛虎,细嗅蔷薇