写在前面
本篇把所见所想进行总结,持续更新
目标: 学会流行的性能优化技术提高web性能
地位: 是web的支柱: 流量》 搜索 》 转换率 》 用户体验
反例: amazon发现每100ms延迟就会有1%销量损失
性能优化的指标和工具
以淘宝为例
我们先选中这三个选项,使我们可以看到画面渲染快照
重新请求后
我们可以看到性能是相当不错的。 首先说目前首页有80个请求算是蛮多的了,但是DOMContentLoaded仅仅316ms,并且对于电商网站(需要鲜艳一点多姿一点)来讲resources仅仅1.6MB,说明对图片资源也进行了压缩等处理。
瀑布图
我要讲的瀑布图并不是通常说的瀑布流
那通过瀑布图就可以直观地看到一个资源的加载
如何分析瀑布图?
- 横向:看具体资源的加载
会有不同的颜色,说明资源的加载也并不是单一的过程,还经历了很多的环节。
- Queueing 说明资源要经过排队才能从浏览器发出去,对资源进行优先级安排
- DNS Lookup 每个资源要被翻译成域名然后解析成IP然后找到服务器,所以还有一个dns查找的过程。这个时候已经找到资源了
- Initial connection 已经找到资源,那客户端和服务器需要建立连接,即建立TCP连接。
- SSL 协议是https协议,有的网站为了保障安全性使用了SSL证书。 那SSL证书的工作原理就是: 上来要进行一个安全性的验证,我们叫做SSL协商,因此这个过程也是要耗时的。
上面我们知道,在请求发送之前会有很多闲置时间,这里共3.04ms
-
Request sent 真正发送请求需要的时间
-
Waiting (TTFB): 很重要因为网站大部分时间由他决定(后台的响应能力(查数据库,做处理等),服务器响应有多快、回路网络是否顺畅) 从发送出去请求到资源真正回来
-
Content Download 下载资源的时间,资源过大肯定不好
- 纵向: 看的是资源和资源之间的联系
- 有一些资源可以并行加载
- 看关键的时间节点,
想保存的话:
lighthouse
First Contentful Paint 这个是当有内容出现的时间 Speed Index 速度指数 现在我们以4为值(很复杂的计算公式,不必纠结)
交互响应
刚才我们讲了关于首屏加载如何分析,那交互响应也是很重要的。比如这里的二级菜单是否能快速展开;搜索显示下拉内容的时间是否够快;当我们下滑的时候,画面是否流畅;以及一些动画等。
FPS大于等于60的时候是很流畅的
那我们如何直观看到帧数呢? cmd + shift + p
- 异步请求100ms之内返回 如果不行那就尽量压缩,或者前端交互优化
rail测量模型
- 响应: 处理时间应该在50ms以内完成 有同学疑问刚才不是说的100ms吗?我们刚才说的是已经有response的时间。这里我们指的是输入浏览器处理的时间。
- 动画: 每10ms产生一帧
- 空闲: 用户那里尽可能增加空闲时间(业务相关、运算相关的应该在后台去做)
- 加载: 在5s内完成内容加载并可以交互
工欲善其事必先利其器
- Chrome DevTools开发调试、性能评测
我们可以放大分析,导致阻塞的原因,定位到哪一行代码。
在分析之前要知道我们可以调节一下是否使用缓存以及网络吞吐(模拟用户的网络状况)
- lighthouse网站之后整体质量评估
- WebPageTest 多测试地点、全面性能报告t(前提是toC而且是各地都有服务器(像淘宝这样的网站))
有的程序员最爱说的一句话: 这个功能在我电脑上是好的呀?。。。。。。 我们来看一下跨站点的性能测量工具webpagetes:
隐藏功能: (点击esc)
测量API
DNS解析耗时: domainLookupEnd - domainLookupStart
TCP连接耗时: connectEnd - connectStart
白屏事件: responseEnd - fetchStart
首次可交互事件: domInteractive - fetchStart
......
浏览器渲染优化
触发视觉变化 》 style(对样式进行重新计算) > layout > paint > composite (类似于ps,会有不同的图层,最终合成)
- 避免布局和绘制 有些动画直接回用GPU加速不会经历绘制
修改颜色等也不会经历layout
- 影响回流的操作:
添加或删除元素、操作styles、display:none、offsetLeft,scrollTop, clientWidth、移动元素位置、修改浏览器大小,字体大小
如何较小回流: 尽量使用transform
- 页面抖动(但是有的时候回流是无法避免的,当有回流的时候还有可能layout throttl) 没有fastDom
const update = () => {
for(let i = 0; i < cards.length; i++) {
//获取offsetTop,设置新的width
cards[i].style.width = ((Math.sin(cards[i].offsetTop + tiemstamp / 1000) + 1) * 500) + 'px'
}
window.requestAnimationFrame(update);
}
fastdom(对dom操作批量进行读写)
const update = () => {
for(let i = 0; i < cards.length; i++) {
fastdom.measure(() => {
let top = cards[i].offsetTop;
fastdom.mutate(() => {
cards[i].style.width = ((Math.sin(top + timestamp / 1000) + 1) * 500) + 'px';
})
})
}
}
使用之后就没有警告,并且layout也是正常的(绿色的):
-
复合和图层 把页面拆分成多个图层进行绘制,方便修改、互不影响。 我们可以利用devtools了解网页的图层拆分情况,也就是录制一个性能分析过程然后看一下composite layers
-
避免重绘
尽量使用transform ,opacity控制元素的大小、位置、透明度。
transform: rotate(360deg)
transform: scaleX(1)
- 注意防抖和节流
- 了解react调度
代码优化
- js开销和缩短解析时间
我们可以通过chrome devtools看一下evaluate script
我们知道前端代码中js的开销(编译、解析、执行)是很大的,那如何解决呢?
code splitting 代码拆分,按需加载
tree shaking 代码减重
避免长任务
避免超过1kb的行间脚本
使用raf和rac进行时间调度
- progressive bootstrapping 可见不可交互 vs 最小可交互资源集
- 配合v8编译原理进行优化
源码 》 抽象语法树 》 字节码 》 机器码
编译过程会进行优化
运行过程可能会进行反优化
v8优化机制: 脚本流(下载>解析,下载过程中也可以进行解析) 字节码缓存 懒解析
v8引擎会对参数类型进行优化,尽量不要改变参数的类型
const { performance, PerformanceObserver } = require('perf_hooks');
const add = (a, b) => a + b;
const num1 = 1;
const num2 = 2;
performance.mark("start");
for(let i = 0; i < 100000; i++) {
add(num1, num2);
}
//这个位置进行类型变化
add(num1, "s");
for(let i = 0; i < 100000; i++) {
add(num1, num2);
}
performance.mark("end");
const observer = new PerformanceObserver((list) => {
console.log(list.getEntries()[0])
})
observer.observe({ entryTypes: ['measure']});
performance.measure('测量 ','start', 'end');
- 懒解析 & 饥饿解析(函数优化) js执行默认是进行懒解析的,也就是运行的时候才会进行解析。但是我们有的时候希望进行饥饿解析
//如何进行饥饿解析呢
const add = ((a, b) => a + b);
const num1 = 1;
const num2 = 2;
add(num1, num2);
但是,js最终会进行压缩,那么通过"()"进行的饥饿解析会失效,因此我们用Optimize.js进行优化
- 对象优化,我们可以做哪些事情呢?
其实就是迎合V8引擎对代码进行解析,能够对代码进行优化
- 以相同的顺序初始化对象成员,避免隐藏类的调整
- 实例化后避免添加新属性
为什么初始化对象的顺序很重要呢?首先我们知道javascript是动态类型语言或者弱类型语言,即写时不会声明变量的类型,而编辑器最终需要知道确定的类型。那么编辑器在解析时就会推断类型(21种类型),我们称之为隐藏类型(hidden class),因此之后所做的优化都是基于hidden class来做的。下面例子
正确示范:
class Student { //HC0
constructor(name, age) {
this.name = name;//HC1
this.age = age;//HC2
}
}
const student1 = new Student("tom", 19);
const student2 = new Student("jarry", 20);
//创建一个对象时,会创建三个hidden class, 如果后续创建对象的过程中,还是这样的顺序,那么复用上面的hidden class
错误示范:
const obj = { name: 'tom' } //HC0
// 一开始就有的属性为In-object属性
obj.age = 19; //HC1
// Normal/Fast属性 存储在property store里面,需要描述数组间接查找
const obj2 = { age: 30 } // HC3
obj2.name = "jarry" //HC4
//可以看到在声明obj2的时候,底层不能复用obj声明的隐藏类型。
- 尽量使用Array属性代替arrry-like对象
- 避免读取超过数组的长度(比如for循环越界)
- 避免元素类型转换
- html优化
- 减少iframes的使用 会阻碍父文档的使用,并且在iframes中创建的元素开销会高很多。
如果一定要用的话:
<iframe id="a"></iframe>
//onload之后执行加载
document.getElementById("a").setAttribute("src", 'url');
- 压缩空白符
- 避免节点深层级嵌套
- 避免table布局
- 删除无用注释 几乎不会用了,因为只有table加载完才会加载页面、css和html没有分离
- css和ja尽量外链,首屏优化的时候也可能行间
- 删除元素默认属性
- 语义化标签
ps: 可以通过配置webpack进行压缩优化
- css优化
资源优化
构建优化
传输加载优化
比较前沿的优化方案
关于react的性能优化
- React.memo()
//memo只会对子组件的属性进行浅比较,但是如果props是函数的话就不能满足了。
const SonComponent = (props) => {
return "这个是子组件"
}
export React.memo(SonComponent)
React.memo() 第二个参数是什么?
export React.memo(SonComponent, (nextProps, preProps) => {return false})//如果返回的是true就不进行渲染了。
//但是如果我们是props中的属性是函数呢?
方法一:
//如果我们已经确切的知道我们传递进来的函数始终做一件事情的时候,我们可以绕开函数,直接通过第二个参数进行
//比较nextProps and preProps
eg:
//props: {name: 'tom', onClick: () => {}}
React.memo(SonComponent, (nextProps, prevProps) => nextProps.name === prevProps.name )