前端性能优化指南

316 阅读16分钟

从代码优化到网站的性能优化

原文

我们都知道前端性能很重要,并尝试从各个方面优化它,但你是否真的知道你网站的性能瓶颈在哪呢?是你的代码执行效率低下,还是javascript文件过大,是频繁的Dom操作,还是缓慢的渲染速度?最重要的是你应该知道从哪些方面提升网站的性能瓶颈,并长期观察对比各个方案的效果。

目录

  • 代码优化
    • 高性能代码
      • 数据存取
      • DOM编程
      • 算法和流程控制
      • ajax
      • 编程实践
    • 构建优化
      • webpack
      • react
      • vue
  • 网站性能
    • 传输过程
      • DNS优化
      • 资源传输优化
    • 渲染过程
      • 懒加载
      • 动画
      • 同构
    • 交互过程
      • dom优化
      • css优化
      • 事件优化

代码优化

代码优化包含两个方面,一是对于JS引擎和浏览器而言,高效率的代码,二是对于不同构建环境的代码,这部分优化针对不同的框架而言手段都不尽相同。

高性能代码 -来自高性能Javascript一书

一、数据存取

  • 使用局部变量:在函数中读取局部变量是最快的,读取全局变量是最慢的,因为通过作用域链解析标识符需要开销,并且作用域链越长,开销越大。如果在函数中需要多次引用全局变量也可优化:
var global = 0;
function() {
    // 在函数内部定义一个变量, 后续直接访问局部变量即可
    var temp = global;
    temp++;temp--;
    console.log(temp);
}
  • 缓存对象的成员值: 对象的属性获取需要一定开销,且嵌套越深开销越大,如读取location.href总是比读取window.location.href要快,对于需要频繁访问的深层对象,需要将其缓存:
var name = element.name;
for(var i = 0; i < 10000; i++) {
    name += "zhou";
    // element.name += "zhou";
}

二、DOM编程

  • 创建DOM节点:在大多数情况下,使用innerHTML要比原生DOM方法快,如createElement
  • querySelect:如querySelectorAll查询语句性能高于其它的查询语句,原因在于querySelect返回的不是动态的HTML集合
  • 离线DOM:对于批量修改DOM样式时,推荐离线DOM树,减少访问DOM布局的次数。

三、算法和流程控制

  • 循环
    • for-in循环效率低于for、while, 速度只有for循环的1/7。
    • forEach等基于函数的迭代:forEach会基于数组的每一项调用函数,这是它慢于循环的原因,在对速度有严格要求或数组较长时不推荐使用
    • for循环优化
// 原始版本
for(let i = 0; i < list.length; i++){
    fn(list[i]);
}

// 优化版本1, 缓存list.length,因为length值是不变的
for(let i = 0, len = list.length; i < len; i++){
    fn(list[i]);
}

// 优化版本2,倒序循环,把i的值作为控制条件,去掉 i < len的比较消耗
for(let i = list.length; i--){
    fn(list[i]);
}
  • 条件语句

    • 将最可能出现的条件前置以减少判断的次数
    • 将扁平化的判断转换为嵌套式的判断,二分法缩小区间,如判断1-10可转换为小于6和大于等于6
    • 通过表查找替代if/else和switch,所谓表查找是将所有情况的返回值存入数组中访问的一种方法
  • 递归

    • 循环代替递归:由于递归调用栈的限制可能会导致栈溢出错误,所有可以使用循环改写递归
    • 尾递归优化:尾递归优化不会导致栈溢出,严格模式下才有效
    • 开启缓存:在一次递归中可能相同的值会被计算多次,将结果缓存是优化递归效率的手段之一。

四、Ajax

  • 使用get获取数据可以被缓存,只有在url长度大于2048个字符时再考虑post
  • jsonp的方式加载数据是非常快的,因为相应的消息直接作为js代码执行,而不是作为字符串需要进一步处理,但需要注意的是不要使用jsonp去加载不可靠的源数据。
  • 使用图片向服务器发送信息:具体方式为,新建一个图片对象new Image(),并将其src属性设置为服务器的urlsrc = url + params,服务器会接受到数据并保存,使用这种方式的消耗是非常小的。
  • 数据格式:json作为轻量级的数据交换格式是首选,其次是特定字符分割的字符串
  • 使用类库:合理使用ajax类库可以避免在一些古怪的浏览器中遇到问题

五、编程实践

  • 使用字面量创建对象和数组: var a = [], b = {},可以在控制台中查看效率
  • with和eval:避免使用这两种语句,因为它们会造成动态作用域问题且性能较低。
  • 避免重复工作:很多次的代码重构其实都在在消除重复,尽量在开发阶段处理好,不要想着重构是再来优化
  • 通过位运算提示计算效率:如,通过对整数求模的方式为表格添加条纹,使用i % 2 === 1判断, 这个计算可转换为位运算提升效率: i & 1
  • 在进行数学运算:使用Math提供的方法要比自己写执行效率高,尽量使用原生方法
  • 使用性能分析工具:如chrome的devTools分析性能瓶颈

构建优化

一、webpack

  • 代码分离
    • 多入口分离
    • 复用组件分离 (CommonsChunkPlugin)
    • 动态导入组件分离 (import())
  • Loader
  • resolve(解析路径) & Externals(外部扩展)
  • Dll优化
  • source map
  • tree shaking
  • Split CSS(分离css)
  • webpack性能优化

二、React

  • shouldComponentUpdate:合理使用shouldComponentUpdate阻止组件更新可以有很大的性能优化(或使用PureComponent)
  • 为列表设置唯一key:设置唯一key可以让diff算法在对比同一级元素变动时有更好的表现(不用频繁的删除和添加元素,而是移动元素位置)
  • React Fragments:避免额外标记,因为每个组件需要有单一根元素,所以经常新增额外标签,使用React Fragments提供的一组空元素解决: <></>
  • 不使用内联函数:内联函数每次调用“render”函数时都会创建一个新的函数实例
// 1、使用内联函数
render() {
    return (
   	<div onClick={e => this.handleClick(e)}>test</div>
    )
}

// 2、不使用内联函数
render() {
    return (
   	<div onClick={this.handleClick}>test</div>
    )
}
  • componentWillMount:在react16更新后,render之前的生命周期有可能会执行多次(React Fiber的影响),并且在react17中也会删除此生命周期,建议不要使用。重复执行也是getDerivedStateFromProps钩子被设计为static函数的原因 参考react16的更新
  • 优化条件渲染:很多情况React不需要完全卸载一个组件,如动态切换显示和隐藏,这时我们可以考虑用样式切换来代替条件渲染(类似于vue的v-if和v-show)。
  • componentDidCatch:为组件创建错误边界,componentDidCatch钩子能捕获到组件本身render方法和子组件的生命周期方法抛出的错误,为应用提供一个错误收集的方式和兜底的设计。
  • 不要使用index作为key:在某些情况下使用index作为key会导致更糟糕的性能,如在列表头部新增一个项,这会导致列表所有项被重新添加。相同的道理,不要做出将列表尾部的项移动到列表头部等操作。

三、Vue

  • v-if / v-show: 合理使用v-if / v-show
  • 列表元素设置key(同react)
  • keep-alive组件缓存
  • data优化:对于不需要被监听的对象不要放在data中,或者使用Object.freeze()冻结对象。
  • props: 定义尽量详细的props, 避免这样使用props: ["status"]
  • v-if和v-for: v-if和v-for不要用在一起,好的做法是使用计算属性代替

网站性能优化

通常,网站的性能指标反应在网站加载时间,和后续交互体验上。网站从加载到用户可交互的时间有多个过程时间和。

传输过程

用户在访问网站过程中,会有DNS解析,TCP握手,HTTP资源传输等过程, 针对这些过程优化如下。

  • DNS预解析,对静态资源域名添加dns-prefetch

    • a标签的href在各大主流浏览器中会自动dns-prefetch,但是https域名不会。
    • 添加meta头的方式解决https域名不会自动预解析: <meta http-equiv="x-dns-prefetch-control" content="on">
    • 使用场景通常是网站中包含了大量的外部资源,<link rel="dns-prefetch" href="//wddsss.com/a.jpg">
  • HttpDns防劫持 DNS劫持是运营商的DNS服务在解析后返回了不正确的主机IP,将用户导入到其他网页的现象,可以使用HttpDns(某云有提供此服务)。

    image.png
    使用HttpDns eg:http://127.0.0.1?domain=https://www.wddsss.com

  • CDN加速资源传输

    • 物理层的硬件优化:更快的硬盘读取,更高的网络带宽
    • 网络层的寻址优化:寻找距离最近的资源,依赖于网络层的路由协议寻址算法
    • 传输层的优化:1: TCP的慢启动流量控制可以用于避免网络拥塞,2: 设置rwnd为一合理的值提升最大吞吐量(针对不同类型的流量,rwnd的值设置应该不尽相同)
    • 应用层的缓存优化:资源合理设置缓存
  • SSL加速

    • 减少中间证书:客户端通过https请求服务端,服务端返回证书信息给客户端,客户端需要验证证书是否可信(浏览器的可信CA列表验证),如果证书的颁发机构不在浏览器可信列表中,则会检查此CA的上层机构是否可信,直到找到可信CA。通过减少中间证书机构优化证书验证效率
    • OCSP Stapling:OCSP是一个TLS证书状态查询扩展,它加速上述查询CA过程,可以在服务端配置。
  • Http/2优化资源传输

    • 多路复用:一个域名维护一个TCP链接,http请求以流的方式传输,实现资源并行下载
    • 头部压缩:使用静态表和动态表压缩http的头部信息
  • 资源压缩

    • Brotli压缩算法(用于纯文本): 由Google推出的无损压缩算法。Brotli 通过变种的LZ77算法、Huffman 编码以及二阶文本建模等方式进行数据压缩,与其他压缩算法相比,它有着更高的压缩效率。对于常见的纯文本,Brotli压缩性能比gzip提高了17-25%(IE不支持)
    • gzip:gzip可以对所有常见web资源压缩,浏览器支持性也很好,几乎所有浏览器都支持
  • 图片优化

    • 响应式图片:在不同环境不同场景中使用不同size或者不同质量的图片,一般CDN图片都会提供此功能。
    • webp图片支持:对于支持的浏览器返回webp格式的图片,注意两个问题,一是向下兼容,对于不支持webp的浏览器需要返回可用格式的图片,二是服务端缓存(memcahe通过vary字段缓存多份html文档解决) 详细请查看全站webp支持
    • gif转循环视频:在浏览器中gif的表现不理想,可以使用循环播放的视频代替
    • save-date: 在http请求头中新增save-data: on,获取压缩后的图片(需要服务端支持)
  • 资源缓存

    • 强缓存:由http头信息的Expires/Cache-Control控制,当资源处于强缓存未过期状态,资源的状态码是 200(memory cache or disk cache)
    • 协商缓存: 由http头信息的 ETag/If-None-Match、last-modified/if-modified-since控制,协商缓存未过期状态码是 304(Not Modified),注意过度使用Etag计算文件hash值会增大服务器压力,增加文件相应时间,需要谨慎使用。
    • service workers:使用service workers缓存静态资源甚至整个页面

渲染过程

  • 下载js文件
    • 普通js:同步下载js文件,下载完成后立即执行
    • async:异步下载js文件,下载完毕后立即执行
    • defer:异步下载js文件,在文档加载完毕,DOMContentLoaded事件触发之前执行
    • 动态下载:在js文件中添加一个script标签,并为其添加src属性,当次标签被添加到dom时开始下载,推荐使用。
    • ajax下载:在ajax中获取js文件内容,并设置script.text = rs.jsContent,通过此方式添加的js的主要优点是你可以下载js代码但不立即执行。

async和defer都能异步下载js文件,做到了js下载的同时不阻塞文档解析,但是async由于其加载完成后立即执行的特性,导致js的执行顺序无法控制,所以实战中推荐使用defer。

  • 懒加载:
    • webpack配置路由懒加载:依赖于import()或require.ensure()
    • 组件懒加载:同样依赖于import()或require.ensure()的动态导入
    • IntersectionObserver构造函数: IntersectionObserver为开发者提供了一种可以异步监听目标元素与其祖先或视窗(viewport)交叉状态的手段,说白了就是提供了一种监听目标元素是否在可视区域(viewport)内的api,我们可以使用它实现懒加载,以图片懒加载为例:
const io = new IntersectionObserver(callback);
let imgs = document.querySelectorAll('[data-src]')
imgs.forEach((item)=>{
    io.observe(item)
})
function callback(items){
    items.forEach(item => {
        if(item.isIntersecting){ // 当前元素可见
            item.target.src = item.target.dataset.src
            io.unobserve(item.target)
        }   
    })
}

这里图片的初始src可以设置一个质量很低的图片,以达到图片加载的流畅度。

  • 预加载

    • preconnet预连接:浏览器对于要请求的资源都需要进行DNS查询,TCP连接,TSL认证过程,使用preconnet可以预先建立连接,在需要使用时直接获取资源。<link preconnet href="https://www.wddsss.com/a.js"/>
    • prefetch预加载:prefetch能够让浏览器预加载一个资源<link prefetch href="https://www.wddsss.com/a.html" />
    • prerender预渲染: 浏览器不仅会下载资源,还会分配部分资源对其渲染,以到达用户下个页面响应速度更快,达到秒开的目的。
  • css优先下载

    • 将首屏使用的css放置在head中提前下载,放置因为css下载过慢导致页面闪烁。
    • 将重要的css代码内嵌在html文档中,缺点是耦合性高,不利于缓存
    • 使用HTTP/2服务端推送传递重要的css
  • 骨架屏:骨架屏就是在页面数据尚未加载前先给用户展示出页面的大致结构,常用在比较规则的列表页面,可以自己设计也可使用已有的解决方案。

  • 动画优化

    • translate3d:开启translate3d可以让GPU参与加速动画渲染,这是一种欺骗浏览器的hack方法,让浏览器认为即将渲染3D动画,其实元素根本没有在z轴运动。
    • will-change:will-change是css3新增的属性,和translate3d类似,是一种加速动画渲染的方法,js在异步事件中触发浏览器大量绘制界面时,浏览器往往是没有准备的被动使用cpu去计算页面渲染,而will-change可以提前告知浏览器此元素的渲染需要gpu参与高速渲染。用法:will-change: transform, will-change: contents
    • 使用高性能动画属性:如transformopacity等,减少使用box-shadow等消耗较大的属性动画。
    • 减少回流的动画:元素的某些属性在发生变化后会导致浏览器大面积重绘页面,视觉上反应为动画卡顿。
    • js的执行和渲染互斥:js在执行期间将主线程中的渲染操作收集起来,并在本轮事件循环结束(微任务执行完毕)后执行所有渲染操作,但是如果在js中获取布局信息则会打乱这里过程,迫使浏览器将暂存的所有渲染操作优先执行,然后获取最新的渲染状态,如获取元素的offsetTop、clientTop等。
    • requestAnimationFrame:requestAnimationFrame是H5新增的api,传统的js实现动画在setInterval中实现,setInterval中的代码并不是严格意义上的定时执行,有可能造成性能问题,requestAnimationFrame则充分利用了屏幕的刷新机制,可以使用此API代替setInterval
  • 服务端渲染

交互过程

  • web worker: 使用web woker处理耗时操作,在执行完成后将结果通知主线程即可,web woker优化可以让用户的交互及时响应,不至于被耗时的js执行阻止界面渲染。

  • DOM

    • 缓存dom引用:将获取的dom元素赋值给变量,不用重复获取dom对象
    • 避免在循环中操作dom
    • documentFragment:合并DOM操作
    • class替换:对于元素上样式变化较多的操作可以考虑使用class替换
    • 使用框架:现在前端流行框架都有自己的dom处理方案,建议在理解其原理的同时合理的使用。
  • CSS

    • flexbox:flexbox布局代替浮动布局
    • calc:非特殊情况不使用计算属性
    • 选择器:优先使用class选择器,避免选择器嵌套过深
    • @import:避免使用@import,@import引入的css下载优先级较低,并且兼容性有一定问题。
    • 不用ID选择器:避免使用ID选择器声明样式
  • 事件

    • 防抖和节流
    • 事件委托:相比于为每个元素绑定事件,事件委托可以减少网站内存消耗,否则在一些低端机型上可能成为性能瓶颈。

说在最后

本次的优化指南从代码的执行到网站的加载,从构建的优化到资源传输,罗列了一组优化清单,你可以通过逐条对比的方式找到自己网站的优化空间。由于篇幅问题,其中多数内容都只是总结输出一笔带过,并不涉及具体如何优化细节,可以自行通过自己搜索或超链接阅读。

性能的优化应该始于一个完善的检测系统,并且有健全的度量过程,且必须要清楚,对于网站的一次优化重构,具体带来了怎样的性能提升?整个优化过程最好是一个有计划且有目标的,你需要清楚此次优化的目的,和目标性能的提升,这不是盲目的。

权衡你的站点,权衡时间和收益,首先列出优先级最高的优化清单,然后就去做吧!

最后给大家推荐一网站,热点检索专用,站长高产似那啥,你想看的站点都有。戳我查看