为什么要进行性能优化
57%的⽤户更在乎⽹⻚在3秒内是否完成加载
52%的在线⽤户认为⽹⻚打开速度影响到他们对⽹站的忠实度
每慢1秒造成⻚⾯ PV 降低11%,⽤户满意度也随之降低降低16%
近半数移动⽤户因为在10秒内仍未打开⻚⾯从⽽放弃。
性能优化学徒工
雅⻁军规践⾏
html数量控制
能尽量用CSS解决的就用CSS解决。(阴影,渐变)压缩,合并,MD5,CDN
接下来请大家思考一个问题,为什么CDN对于前端这么重要?
-
还有一个很重要的点就是离线缓存
打开谷歌控制台的application
localStorage存储本地数据
//需求是什么?
//假设我们需要请求a.xx3322.js
//在本地存储localstorage存储key为a.js,对应的value是a.xx3322.js
//key为a.xx3322.js,对应的value就是我们的目标js代码
//所以就不需要<script src = "a.xx3322.js">
//可以用下面代码来实现
//webpack打包出来
var res = {
"a.js":"a.xx3322.js"
}
function activePage(){
for(let item of res){
const js = localStorage[item.key]
//如果本地没有发请求,再发一次请求缓存
//本地存在的话,就要判断一下当前的版本号
//更新我们的资源
if(js == item.value){
eval(js)
}else{
fetch(item.value).then(function(res){
localStorage['a.js'] = "a.xx3322.js";
localStorage['a.xx3322.js'] = res;
})
}
}
}
activePage();
现在我们可以使用basket.js来实现上述代码的功能
https://github.com/addyosmani/basket.js
http://www.wenjiangs.com/article/basket-js.html
使用basket.js这个库就能轻松管理了。
简单来说 Basket.js 是一个脚本缓存器,使用本地储存 localStorage 缓存 JavaScript 文件,如果脚本以前在本地缓存过,那么他将会被快速的加载到页面中,如果没有缓存过,那么就使用 XHR 异步加载到页面中
HTML5 规范建议存储限额为 5MB 的本地存储,但浏览器可以实现他们自己的配额,如果他们希望。如果超出了配额,浏览器可能无法在缓存中存储项目。如果发生这种情况,Bask.js 将从最旧的缓存中删除条目,然后重试。有些像 Opera 这样的浏览器会要求用户在超过设定阈值时增加配额。
配合使用前端离线缓存方案 localForage
缓存策略
缓存的优先级
cache-control > expire > etag > last-modified
他们的关系面试一定会问到。
回去需要用nginx体现一下
网站协议
HTTP2协议
HTTP2协议的特点:
使用二进制格式传输,更高效、更紧凑。
TTP 2.0 中所有加强性能的核心点在于此。在之前的 HTTP 版本中,我们是通过文本的方式传输数据。在 HTTP 2.0 中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制格式编码。-
对报头压缩,降低开销。
在 HTTP 1.X 中,我们使用文本的形式传输 header,在 header 携带 cookie 的情况下,可能每次都需要重复传输几百到几千的字节。在 HTTP 2.0 中,使用了 HPACK 压缩格式对传输的 header 进行编码,减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header ,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。
-
多路复用,一个网络连接实现并行请求。
在 HTTP 2.0 中,有两个非常重要的概念,分别是帧(frame)和流(stream)。帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。
多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。
HTTP/2对同⼀域名下所有请求都是基于流,也就是说同⼀域名不管访问多少⽂件,也只建⽴⼀路连接。同样Apache的最⼤连接数为300,因为有了这个新特性,最⼤的并发就可以提升到300,⽐原来提升了6倍!(本http1.x的话每个用户可能就会占据5-6个请求)
-
服务器主动推送,减少请求的延迟 。
在 HTTP 2.0 中,服务端可以在客户端某个请求后,主动推送其他资源。可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用 prefetch 。
默认使用加密。
小字为先
性能优化其实就可以用这四个字来概括,“小字为先” ,也就是将大的东西变小
渲染中性能优化
重绘
先来了解一下开发者工具下中的隐藏技能
控制台下面有个rendering的选项(如果没有的话可以在上面的performance右边的工具栏选项选择more tools中的rendering进行添加)
Paint Flashing
高亮显示网页中需要被重绘的部分。
Layer Borders
显示Layer边界。
FPS Meter
每一秒的帧细节,帧速率的分布信息和GPU的内存使用情况。
Scrolling Performance Issues
分析鼠标滚动时的性能问题,会显示使屏幕滚动变慢的区域。
Emulate CSS Media
仿真CSS媒体类型,查看不同的设备上CSS样式效果,可能的媒体类型选项有print、screen。
将paint flashing选项打勾之后点击刷新的时候可以看到页面绿了一下,正是因为这是网页中需要被重绘的部分。
上个代码(重点部分)
<div class="container">
<div class="ball" id="ball">
</div>
</div>
<script>
var ball = document.getElementById('ball');
ball.classList.add('ball');
ball.classList.add('ball-running');
</script>
<style>
.container{
position: relative;
min-height: 400px;
}
.ball{
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 100px;
background-color: blueviolet;
border-radius: 50%;
box-shadow: 0 0 5px rgba(245, 172, 172, 0.75)
}
.ball-running{
animation: run-around 4s infinite;
}
@keyframes run-around {
0%{
top: 0;
left: 0;
}
25%{
top: 0;
left: 200px;
}
50%{
top: 200px;
left: 200px;
}
75%{
top: 200px;
left: 0;
}
}
</style>
上面主要就是实现一个小球运动的效果,当我们已经在rendering里面选中了paint flashing的时候,我们可以看到小球运动起来的效果是外面包裹着一层绿色,说明这是要进行重绘的区域
使用Chrome DevTools的performance面板可以记录和分析页面在运行时的所有活动。
https://www.cnblogs.com/xiaohuochai/p/9182710.html
loading
:加载时间scripting
:脚本执行时间rendering
:重排时间painting
:重绘时间idle
:空闲时间,网站性能越好,空闲时间越长
网站的渲染流程
将上面的图从summary切换到event log
event log按照时间先后来排序
可以看到网站的渲染流程是这样的:
- 获取DOM进行分层
- 对每个图层节点进行样式的计算 Recalculate Style
- 为每个对应的节点图形和位置 重排Layout
- 对每个节点进行绘制并添加到图层位图中 Paint
(并不是每个图层都会GPU进行参与)
只有Composite Layers才会让GPU参与 - 将这个位图上传至GPU 旋转、缩放、偏移、修改透明
所以渲染过程总的来说是这样的:Layout -》 Paint -》 Composite Layers
我们说DOM会进行分层,那么什么元素会独立成层呢?
根元素、position、transfrom、半透明元素、CSS滤镜、Video 、Overflow
我们说GPU跑起来会比CPU快,那么哪些元素属性会让GPU参与进来呢?
CSS3D、Video、Webgl(https://github.com/lgwebdream/gpu.js)、CSS滤镜、transfrom
CPU和GPU到底有什么区别呢?
https://www.zhihu.com/question/19903344
CPU即中央处理器,GPU即图形处理器。其次,要解释两者的区别,要先明白两者的相同之处:两者都有总线和外界联系,有自己的缓存体系,以及数字和逻辑运算单元。一句话,两者都为了完成计算任务而设计。
总结一下:
相同之处:总线和外界联系、缓存体系、数字和逻辑与预算单元、计算而生
不同之处:CPU主要负责和操作系统应用程序,GPU显示数据相关
http://www.sohu.com/a/200435336_463987
还要推一推gpu.js这个库
https://github.com/gpujs/gpu.js
GPU.js is a JavaScript Acceleration library for GPGPU (General purpose computing on GPUs) in JavaScript. GPU.js will automatically compile simple JavaScript functions into shader language and run them on the GPU. In case a GPU is not available, the functions will still run in regular JavaScript.
也就是说GPU.js会自动的将简单的js函数翻译成shader 语言并放在GPU上面运行他们。
像素管道
像素管道是网页性能优化的灵魂,让我们来看看什么是像素管道
上图就是像素管道,通常我们会使用JS修改一些样式,随后浏览器会进行样式计算,然后进行布局,绘制,最后将各个图层合并在一起完成整个渲染的流程,这期间的每一步都有可能导致页面卡顿。
注意,并不是所有的样式改动都需要经历这五个步骤。举例来说:如果在JS中修改了元素的几何属性(宽度、高度等),那么浏览器需要需要将这五个步骤都走一遍。但如果您只是修改了文字的颜色,则布局(Layout)是可以跳过去的。
除了最后的合成,前面四个步骤在不同的场景下都可以被跳过。例如:CSS动画就可以跳过JS运算,它不需要执行JS。
通过录制performance我们可以看到主线程的任务。
我们可以放大主线程从而精准的看到每一帧浏览器都执行了哪些任务以及每个任务耗费了多长时间。如下图所示:
我们如何改进上面的代码呢,减少重绘呢?
使用transform来代替top和left
我们可以看到https://csstriggers.com/transform上面对于transform的描述是
Changing transform does not trigger any geometry changes or painting, which is very good. This means that the operation can likely be carried out by the compositor thread with the help of the GPU.
也就是说改变transform并不会触发几何图形的更改或者重绘,transform的操作可以合成器线程在GPU的帮助下执行。
css-triggers给出了不同的CSS属性被更改后会触发像素管道的那些步骤。
简单来说,像素管道经历的步骤越多,渲染时间就越长,单个步骤内也可能因为某个原因而变得耗时很长。
将上面代码的keyframes部分更改成:
0%{
transform: translate(0)
}
25%{
transform: translate(200px,0)
}
50%{
transform: translate(200px,200px)
}
75%{
transform: translate(0,200px)
}
我们此时看到小球在运动,但是已经没有绿色了,已经没有重排了!!
神奇!!!
我们再来看看录制的summary
我的天呐!真神奇。换了transforms每次运动就不用重绘重排了,都是合成层和GPU在操作了,而且只要GPU开启,速度就会快很多。
https://csstriggers.com/这个网站拿好不送!!!
总结
CSS动画我们可以通过降低绘制区域并且使transform属性来完成动画,同时我们需要管理好图层,因为绘制和图层管理都需要成本,通常我们需要根据具体情况进行权衡并做出最好的选择。
重排
什么会引起重排?
- 添加或者删除元素的时候
- 元素的位置发生改变
- 元素的-webkit-box-sizing: border-box;不会让我们的盒子发生太多的变化
如果用标准盒子模型的话,盒子越来越大 - 页面初始化
- 内容变化(没有撑开盒)
- js 读取一下几个值 offset、scroll、width、getComputerStyle
为什么js读取的时候会引起重排?
var ele = document.getElementById('myDiv');
ele.style.borderLeft = '1px';
ele.style.borderRight = '2px';
ele.style.padding = '5px';
乍一想,元素的样式改变了三次,每次改变都会引起重排和重绘,所以总共有三次重排重绘过程,但是浏览器并不会这么笨,它会把三次修改“保存”起来(大多数浏览器通过队列化修改并批量执行来优化重排过程),一次完成!但是,有些时候你可能会(经常是不知不觉)强制刷新队列并要求计划任务立即执行。获取布局信息的操作会导致队列刷新,比如:
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
-
getComputedStyle() (currentStyle in IE)
将上面的代码稍加修改,var ele = document.getElementById('myDiv'); ele.style.borderLeft = '1px'; ele.style.borderRight = '2px'; // here use offsetHeight // ... ele.style.padding = '5px';
因为offsetHeight属性需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的“待处理变化”并触发重排以返回正确的值(即使队列中改变的样式属性和想要获取的属性值并没有什么关系),所以上面的代码,前两次的操作会缓存在渲染队列中待处理,但是一旦offsetHeight属性被请求了,队列就会立即执行,所以总共有两次重排与重绘。所以尽量不要在布局信息改变时做查询。
我们可以使用requestAnimationFrame,拆开来写,就给了浏览器优化的机会了。
var ele = document.getElementById('myDiv');
// here use offsetHeight
// ...
requestAnimationFrame(function(){
ele.style.padding = '5px';
ele.style.borderLeft = '1px';
ele.style.borderRight = '2px';
})
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
requestAnimationFrame告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
页面加载性能优化
必须知道的概念
TTFB(Time To First Byte ):⾸字节时间
FP(First Paint ):⾸次绘制,仅有⼀个div根节点。
FCP(First Contentful Paint): ⾸次有内容的绘制,包含⻚⾯的基本框架,但没有数据内容。
FMP(First Meaningful Paint):⾸次有意义的绘制,包含⻚⾯所有元素及数据
TTI(Time To Interactive):可交互时间
Long tasks:超过了 50ms 的任务
SSR&&CSR:服务端渲染和客户端渲染
Isomorphic JavaScript:同构化
类比于VUE
created 类比于 FP(首次绘制,只有一个app空节点)
mounted 类比于 FMP(页面基本框架绘制完成)
Performance — 前端性能监控利器
Performance是一个做前端性能监控离不开的API,最好在页面完全加载完成之后再使用,因为很多值必须在页面完全加载之后才能得到。最简单的办法是在window.onload事件中读取各种数据。
https://www.cnblogs.com/bldxh/p/6857324.html
https://cloud.tencent.com/developer/news/301840
从输入url到用户可以使用页面的全过程时间统计,会返回一个PerformanceTiming对象,单位均为毫秒。
每一个performance.timing属性都表示一个页面事件(例如页面发送了请求)或者页面加载(例如当DOM开始加载),测量以毫秒的形式从1970年1月1日的午夜开始。结果为0表示该事件未发生(例如redirectEnd或者redirectStart等)
其中有个方法叫getEntries()
获取所有资源请求的时间数据,这个函数返回一个按startTime排序的对象数组,数组成员除了会自动根据所请求资源的变化而改变以外,还可以用mark(),measure()方法自定义添加,该对象的属性中除了包含资源加载时间还有以下五个属性。
name:资源名称,是资源的绝对路径或调用mark方法自定义的名称
startTime:开始时间
duration:加载时间
entryType:资源类型,entryType类型不同数组中的对象结构也不同!具体见下
initiatorType:谁发起的请求,具体见下
话不多说,咱们来写写代码。
<style>
body{
background-color: greenyellow;
}
</style>
const obsever = new PerformanceObserver((list)=>{
for(const entry of list.getEntries()){
console.log(entry.entryType);
console.log(entry.startTime);
console.log(entry.duration);
}
})
obsever.observe({entryTypes:['paint']});
结果如下:
或者可以直接通过window.performance.getEntriesByType("paint")就可以取得FP和FCP的值
FMP主要用来给页面打点。
五分钟撸一个前端性能监控工具
http://web.jobbole.com/94938/
聊聊performance中的long task
什么是 long task
?
https://www.itcodemonkey.com/article/10654.html
简单而言,任何在浏览器中执行超过 50 ms 的任务,都是 long task
。
那么 long task
这个时间是怎么得来的?
因为浏览器是单线程,这意味着同一时间主线程只能处理一个任务,如果一个任务执行时间太长,浏览器就无法执行其他任务,用户就会感觉浏览器被卡死,因为他的输入得不到任何响应。
为了100ms内能给出相应,将空闲周期执行的任务限制在50ms意味着,即使用户的输入行为发生在任务刚执行时。浏览器仍有50ms来相应用户的输入。
long task 会长时间占据主线程资源,进而阻碍了其他关键任务的执行/响应,造成页面卡顿。
常见场景如:
不断计算 DOM 元素的大小、位置,并且根据结果对页面进行 relayout;
一次性生成十分庞大的 DOM 元素,如大型表单;
1000000次的循环计算;
long task
的基本属性
Long Tasks API 定义了 PerformanceLongTaskTiming
接口,用于描述 long task
。
一般而言,name + attribution 就可以基本定位出 long task 的来源:
name
:告诉我们来源是 <script/> 还是 <iframe/> ?self -> <script/>;same-origin-xxx + cross-origin-xxx -> <iframe/>
attribution
:到底是哪个 <iframe/>?
如何使用?
const obsever = new PerformanceObserver((list)=>{
for(const entry of list.getEntries()){
console.log(entry.entryType);
console.log(entry.startTime);
console.log(entry.duration);
console.log(JSON.stringify(entry.attribution))
}
})
obsever.observe({entryTypes:['longtask']});
还发现了一篇不错的文章,前端性能优化标准https://yq.aliyun.com/articles/598162
CSR SSR 预渲染 同构的优点和缺点
NodeJs性能优化
什么是内存泄漏?
不再用到的变量/内存,没有及时释放,就叫做内存泄漏。
内存泄漏的表现
随着内存泄漏的增长,V8对垃圾收集器越来越具有攻击性,这会使你的应用运行速度变慢。
内存泄漏可能触发其他类型的失败,可能会耗尽文件描述符,还可能会突然不能建立新的数据库连接。
压力测试寻找内存泄漏
https://www.cnblogs.com/ycyzharry/p/8372168.html
https://github.com/wg/wrk
wrk支持大多数类UNIX系统,不支持windows。
还有更专业的JMeter
查找node内存泄漏工具
memwatch + heapdump
如果不发生特别大的内存泄漏问题,这两个工具是不会跳出来的。
memwatch.on('leak',function(info){
var file = './tmp/heapsnapshot';
heapdump.writeSnapshot(file,function(err){
if(err)console.log(err);
else console.error('Wrote snapshot',file);
})
})
//通过Diff的方式找到真正的元凶
var hd = new memwatch.HeapDiff();
var diff = hd.end()
//一个状态时间发射器
memwatch.on('stats',function(stats){
//数据包括
usage_trend(使用趋势)
current_base(当前基数)
estimated_base(预期基数)
num_full_gc(完整的垃圾回收次数)
num_inc_gc(增长的垃圾回收次数)
heap_compactions(内存压缩次数)
min(最小)
max(最大)
})
http://www.linkdata.se/sourcecode/memwatch/ memwatch的源代码下载地址
Nodejs编码规范
慎用内存缓存
函数内的变量是可以随着函数执行被回收的,但是全局不行。所以避免使用对象作为缓存,可以移步到Redis等。
Redis
是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。
Redis
与其他 key - value 缓存产品有以下三个特点:
Redis
支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
Redis
不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
Redis
支持数据的备份,即master-slave模式的数据备份。
http://www.runoob.com/redis/redis-intro.html
关于队列消费不及时
比如我们用log4来收集日志,如果日志的产生速度大于文件写入的速度。就容易产生内存泄漏。访问已经结束了,服务器的log4日志还在不停的写。
解决方式:
监控队列的长度一旦堆积就报警或者拒绝新的要求。
所以的异步调用都有超时回调,一旦达到时间调用未得到结果就报警。
关于闭包
node如果有闭包从而产生内存泄露服务器就很容易挂,相对于在浏览器的js的闭包,node的闭包的处理显得更加重要。
解决方式:
- weakmap可以立即回收某个变量。
let b = new Object()
let wm = new Weakmap()
wm.set(b,new Array(5*1024*1024))
b = null
- perf_hooks(性能钩子)
是nodejs中的一个api。
The Performance Timing API provides an implementation of the W3C Performance Timeline specification. The purpose of the API is to support collection of high resolution performance metrics. This is the same Performance API as implemented in modern Web browsers.
提供的功能就类似于JS中的那个new PerformanceObserver
const obs = new PerformanceObserver((items) => {
console.log(items.getEntries()[0].duration);
performance.clearMarks();
});
obs.observe({ entryTypes: ['measure'] });
performance.mark('A');
doSomeLongRunningProcess(() => {
performance.mark('B');
performance.measure('A to B', 'A', 'B');
});
详细内容参照http://nodejs.cn/api/perf_hooks.html
总结
对于nodejs应用的测试
- node --inspect app.js
- chrome://inspect/#devices
- 没经过压力测试的代码只完成10%
- 准确计算QPS未雨绸缪
- 合理利用压力测试工具
- 注意缓存队列及其他耗时较长的代码
- 开发健壮的NodeJs应用