从代码优化到网站的性能优化
我们都知道前端性能很重要,并尝试从各个方面优化它,但你是否真的知道你网站的性能瓶颈在哪呢?是你的代码执行效率低下,还是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">
- a标签的href在各大主流浏览器中会自动
-
HttpDns防劫持 DNS劫持是运营商的DNS服务在解析后返回了不正确的主机IP,将用户导入到其他网页的现象,可以使用HttpDns(某云有提供此服务)。
使用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预渲染: 浏览器不仅会下载资源,还会分配部分资源对其渲染,以到达用户下个页面响应速度更快,达到秒开的目的。
- preconnet预连接:浏览器对于要请求的资源都需要进行DNS查询,TCP连接,TSL认证过程,使用
-
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 - 使用高性能动画属性:如
transform、opacity等,减少使用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选择器声明样式
-
事件
- 防抖和节流
- 事件委托:相比于为每个元素绑定事件,事件委托可以减少网站内存消耗,否则在一些低端机型上可能成为性能瓶颈。
说在最后
本次的优化指南从代码的执行到网站的加载,从构建的优化到资源传输,罗列了一组优化清单,你可以通过逐条对比的方式找到自己网站的优化空间。由于篇幅问题,其中多数内容都只是总结输出一笔带过,并不涉及具体如何优化细节,可以自行通过自己搜索或超链接阅读。
性能的优化应该始于一个完善的检测系统,并且有健全的度量过程,且必须要清楚,对于网站的一次优化重构,具体带来了怎样的性能提升?整个优化过程最好是一个有计划且有目标的,你需要清楚此次优化的目的,和目标性能的提升,这不是盲目的。
权衡你的站点,权衡时间和收益,首先列出优先级最高的优化清单,然后就去做吧!
最后给大家推荐一网站,热点检索专用,站长高产似那啥,你想看的站点都有。戳我查看