Web性能
# 性能优化涉及到的分类
- 网络层面
- 构建层面
- 浏览器渲染层面
- 服务端层面
# 涉及到的功能点
- 资源的合并与压缩
- 图片编解码原理和类型选择
- 浏览器渲染机制
- 懒加载预加载
- 浏览器存储
- 缓存机制
PWA
Vue-SSR
# 资源合并与压缩
# http
请求的过程及潜在的性能优化点
- 理解
减少http请求数量
和减少请求资源大小
两个优化要点 - 掌握
压缩
与合并
的原理 - 掌握通过
在线网站
和fis3
两种实现压缩与合并的方法
# 浏览器的一个请求从发送到返回都经历了什么
动态的加载静态的资源
dns
是否可以通过缓存减少dns
查询时间- 网络请求的过程走最近的网络环境
- 相同的静态资源是否可以缓存
- 能否减少
http
请求大小 - 能否减少
http
请求数量 - 服务端渲染
# 资源的合并与压缩设计到的性能点
- 减少
http
请求的数量 - 减少请求的大小
# html
压缩
HTML
代码压缩就是压缩这些在文本文件中有意义,但是在HTML
中不显示的字符,包括空格
,制表符
,换行符
等,还有一些其他意义的字符,如HTML
注释也可以被压缩
意义
- 大型网站意义比较大
# 如何进行html
的压缩
- 使用在线网站进行压缩(走构建工具多,公司级在线网站手动压缩小)
node.js
提供了html-minifier
工具- 后端
模板引擎渲染压缩
# css
及js
压缩
# css
的压缩
- 无效代码删除
- 注释、无效字符
css
语义合并
压缩的方式
- 使用在线网站进行压缩
- webpack使用
optimize-css-assets-webpack-plugin
HtmlWebpackPlugin
- 使用
html-minifier
对html
中的css
进行压缩 - 使用
clean-css
对css
进行压缩
# js
的压缩与混乱
- 无效字符的删除
- 空格、注释、回车等
- webpack 自动压缩
- 剔除注释
- 代码语意的缩减和优化
- 变量名缩短(
a
,b
)等
- 变量名缩短(
- 代码保护
- 前端代码是透明的,客户端代码用户是可以直接看到的,可以轻易被窥探到逻辑和漏洞
# js
压缩的方式
- 使用在线网站进行压缩
- 使用
html-minifier
对html
中的js
进行压缩 - 使用
uglifyjs2
对js
进行压缩
# 不合并文件可能存在的问题
- 文件与文件有插入之间的上行请求,又增加了
N-1
个网络延迟 - 受丢包问题影响更严重
- 经过代理服务器时可能会被断开
# 文件合并缺点
- 首屏渲染问题
- 文件合并之后的
js
变大,如果首页的渲染依赖这个js
的话,整个页面的渲染要等js
请求完才能执行 - 如果首屏只依赖
a.js
,只要等a.js
完成后就可执行 - 没有通过服务器端渲染,现在框架都需要等合并完的文件请求完才能执行,基本都需要等文件合并后的
js
- 文件合并之后的
- 缓存失效问题
- 标记
js
md5
戳 - 合并之后的
js
,任何一个改动都会导致大面积的缓存失效
- 标记
# 文件合并对应缺点的处理
- 公共库合并
- 不同页面的合并
- 不同页面
js
单独打包
- 不同页面
- 见机行事,随机应变
# 文件合并对应方法
- 使用在线网站进行合并
- 构建阶段,使用
nodejs
进行文件合并
# CDN减少打包体积
使用 cdn
文件来减少工程到打包体积,也可以按需加载。
在 /public/index.html 中引入需要的js和css文件
去掉 package.json 中对于 vue、element-ui 等相关资源的依赖
src/main.js ,去掉 vue、element-ui 等相关资源的 import 和 vue.use 这些语句
配置externals。由于使用 Vue Cli 3 默认配置,新建出来的项目没有了 build 目录,首先得在项目根目录下,新建 vue.config.js 文件,里面添加以下代码:
|
|
# 去除 SourceMap
由于打包后的文件经过了压缩、合并、混淆、babel编译后的代码不利于定位分析bug。
|
|
# gzip 压缩
gzip
压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右。
|
|
# 图片相关优化
# 一张JPG
的解析过程
jpg
有损压缩:虽然损失一些信息,但是肉眼可见影响并不大
# png8
/png24
/png32
之间的区别
png8
—-256色
+ 支持透明png24
—-2^24
+ 不支持透明png32
—2^24
+支持透明
|
|
png32
是在png24
上支持了透明,针对不同的业务场景选择不同的图片格式很重要
# 不同的格式图片常用的业务场景
# 不同格式图片的特点
jpg
有损压缩,压缩率高,不支持透明png
支持透明,浏览器兼容性好webp
压缩程度更好,在ios webview
中有兼容性问题svg
矢量图,代码内嵌,相对较小,图片样式相对简单的场景(尽量使用,绘制能力有限,图片简单用的比较多)
# 不同格式图片的使用场景
jpg
:大部分不需要透明图片的业务场景png
:大部分需要透明图片的业务场景webp
:android
全部(解码速度和压缩率高于jpg
和png
,但是ios
safari
还没支持)svg
:图片样式相对简单的业务场景
# 图片压缩的几种情况
- 针对真实图片情况,舍弃一些相对无关紧要的色彩信息
CSS雪碧图
:把你的网站用到的一些图片整合到一张单独的图片中- 优点:减少
HTTP
请求的数量(通过backgroundPosition
定位所需图片) - 缺点:整合图片比较大时,加载比较慢(如果这张图片没有加载成功,整个页面会失去图片信息)
facebook
官网任然在用,主要pc
用的比较多,相对性能比较强
- 优点:减少
Image-inline
:将图片的内容嵌到html
中(减少网站的HTTP
请求)base64信息
,减少网站的HTTP请求,如果图片比较小比较多,时间损耗主要在请求的骨干网络
使用矢量图
- 使用
SVG
进行矢量图的绘制 - 使用
icon-font
解决icon
问题
- 使用
在android下使用webp
webp
的优势主要体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;- 同时具备了无损和有损的压缩模式、
Alpha
透明以及动画的特性,在JPEG
和PNG
上的转化效果都非常优秀、稳定和统一
# css和js的装载与执行
# HTML页面加载渲染的过程
# 一个网站在浏览器端是如何进行渲染的
# HTML渲染过程中的一些特点
- 顺序执行,并发加载
- 词法分析:从上到下依次解析
- 通过
HTML
生成Token对象
(当前节点的所有子节点生成后,才会通过next token
获取到当前节点的兄弟节点),最终生成Dom Tree
- 通过
- 并发加载:资源请求是并发请求的
- 并发上限
- 浏览器中可以支持并发请求,不同浏览器所支持的并发数量不同(以域名划分),以
Chrome
为例,并发上限为6个 - 优化点: 把CDN资源分布在多个域名下
- 浏览器中可以支持并发请求,不同浏览器所支持的并发数量不同(以域名划分),以
- 词法分析:从上到下依次解析
- 是否阻塞
css阻塞
- css 在head中通过link引入会阻塞页面的渲染
- 如果我们把
css
代码放在head
中去引入的话,那么我们整个页面的渲染实际上就会等待head
中css
加载并生成css树
,最终和DOM
整合生成RanderTree
之后才会进行渲染
- 如果我们把
- 为了浏览器的渲染,能让页面显示的时候视觉上更好。 避免某些情况,如:假设你放在页面最底部,用户打开页面时,有可能出现,页面先是显示一大堆文字或图片,自上而下,丝毫没有排版和样式可言。最后,页面又恢复所要的效果
- css 在head中通过link引入会阻塞页面的渲染
css
不阻塞js
的加载,但阻塞js
的执行css
不阻塞外部脚步的加载(webkit preloader 预资源加载器
)
js阻塞
- 直接通过<script src>引入会阻塞后面节点的渲染
html parse
认为js
会动态修改文档结构(document.write
等方式),没有进行后面文档的变化- async defer(async放弃了依赖关系)
defer
属性() (这是延迟执行引入的`js`脚本(即脚本加载是不会导致解析停止,等到`document`全部解析完毕后,`defer-script`也加载完毕后,在执行所有的`defer-script`加载的`js`代码,再触发`Domcontentloaded`)
- async属性(<script src="" async></script>)
- 这是异步执行引入的
js
脚本文件 - 与
defer
的区别是async
会在加载完成后就执行,但是不会影响阻塞到解析和渲染。但是还是会阻塞load
事件,所以async-script
会可能在DOMcontentloaded
触发前或后执行,但是一定会在load
事件前触发。
- 这是异步执行引入的
- 直接通过<script src>引入会阻塞后面节点的渲染
# 懒加载与预加载
# 懒加载
- 图片进入可视区域之后请求图片资源
- 对于电商等图片很多,页面很长的业务场景适用
- 减少无效资源的加载
- 并发加载的资源过多会会阻塞js的加载,影响网站的正常使用
img src
被设置之后,webkit
解析到之后才去请求这个资源。所以我们希望图片到达可视区域之后,img src
才会被设置进来,没有到达可视区域前并不现实真正的src
,而是类似一个1px
的占位符。
|
|
# 预加载
- 图片等静态资源在使用之前的提前请求
- 资源使用到时能从缓存中加载,提升用户体验
- 页面展示的依赖关系维护
|
|
# 懒加载原生js
和zepto.lazyload
1
原理
先将img
标签中的src
链接设为同一张图片(空白图片),将其真正的图片地址存储再img
标签的自定义属性中(比如data-src
)。当js
监听到该图片元素进入可视窗口时,即将自定义属性中的地址存储到src
属性中,达到懒加载的效果。
注意问题:
- 关注首屏处理,因为还没滑动
- 占位,图片大小首先需要预设高度,如果没有设置的话,会全部显示出来
|
|
# 预加载原生js
和preloadJS
实现
# 预加载实现的几种方式
- 第一种方式:直接请求下来
|
|
- 第二种方式:
image
对象
|
|
第三种方式:
1
xmlhttprequest
- 缺点:存在跨域问题
- 优点:好控制
|
|
|
|
- 本质:权衡浏览器加载能力,让它尽可能饱和利用起来
# 重绘与回流
# css
性能让javascript
变慢
要把css
相关的外部文件引入放进head
中,加载css
时,整个页面的渲染是阻塞的,同样的执行javascript
代码的时候也是阻塞的,例如javascript
死循环。
|
|
这两个线程是互斥的,当UI
渲染的时候,javascript
的代码被终止。当javascript
代码执行,UI
线程被冻结。所以css
的性能让javascript
变慢。
|
|
# 什么是重绘和回流
# 回流
- 当
render tree
中的一部分(或全部)因为元素的规模尺寸
,布局
,隐藏
等改变而需要重新构建
。这就成为回流(reflow
) - 当页面布局和几何属性改变时,就需要
回流
。
# 重绘
- 当
render tree
中的一些元素需要更新属性,而这些属性只是影响元素的外观
,风格
,而不影响布局,比如background-color
。就称重绘。
# 关系
用到chrome
分析 performance
|
|
# 避免重绘、回流的两种方法
# 触发页面重布局的一些css属性
- 盒子模型相关属性会触发重布局
width
height
padding
margin
display
border-width
border
min-height
- 定位属性及浮动也会触发重布局
top
bottom
left
right
position
float
clear
- 改变节点内部文字结构也会触发重布局
text-align
overflow-y
font-weight
overflow
font-family
line-height
vertical-align
white-space
font-size
|
|
# 只触发重绘不触发回流
color
border-style
、border-radius
visibility
text-decoration
background
、background-image
、background-position
、background-repeat
、background-size
outline
、outline-color
、outline-style
、outline-width
box-shadow
# 新建DOM的过程
- 获取
DOM
后分割为多个图层 - 对每个图层的节点计算样式结果(
Recalculate style
样式重计算) - 为每个节点生成图形和位置(
Layout
回流和重布局) - 将每个节点绘制填充到图层位图中(
Paint Setup
和Paint
重绘
) - 图层作为纹理上传至
gpu
- 符合多个图层到页面上生成最终屏幕图像(
Composite Layers
图层重组)
# 浏览器绘制DOM
的过程是这样子的:
- 获取 DOM 并将其分割为多个层(
layer
),将每个层独立地绘制进位图(bitmap
)中 - 将层作为纹理(
texture
)上传至GPU
,复合(composite
)多个层来生成最终的屏幕图像 left/top/margin
之类的属性会影响到元素在文档中的布局,当对布局(layout
)进行动画时,该元素的布局改变可能会影响到其他元素在文档中的位置,就导致了所有被影响到的元素都要进行重新布局,浏览器需要为整个层进行重绘并重新上传到GPU
,造成了极大的性能开销。transform
属于合成属性(composite property
),对合成属性进行transition/animation
动画将会创建一个合成层(composite layer
),这使得被动画元素在一个独立的层中进行动画。- 通常情况下,浏览器会将一个层的内容先绘制进一个位图中,然后再作为纹理(
texture
)上传到GPU
,只要该层的内容不发生改变,就没必要进行重绘(repaint
),浏览器会通过重新复合(recomposite
)来形成一个新的帧。
# chrome
创建图层的条件
|
|
3D
或透视变换CSS
属性使用加速视频解码的<video>
元素- 拥有
3D
(WebGL
) 上下文或加速的2D
上下文的<canvas>
元素 - 复合插件(如
Flash
) - 进行
opacity/transform
动画的元素拥有加速 CSS filters
的元素元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)- 元素有一个
z-index
较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
总结:对布局属性进行动画,浏览器需要为每一帧进行重绘并上传到
GPU
中对合成属性进行动画,浏览器会为元素创建一个独立的复合层,当元素内容没有发生改变,该层就不会被重绘,浏览器会通过重新复合来创建动画帧
|
|
# 总结
- 尽量避免使用触发
回流
、重绘
的CSS
属性 - 将
重绘
、回流
的影响范围限制在单独的图层(layers
)之内 - 图层合成过程中消耗很大页面性能,这时候需要平衡考虑重绘回流的性能消耗
# 实战优化点总结
- 用
translate
替代top
属性top
会触发layout
,但translate
不会
- 用
opacity
代替visibility
opacity
不会触发重绘也不会触发回流,只是改变图层alpha
值,但是必须要将这个图片独立出一个图层visibility
会触发重绘
- 不要一条一条的修改
DOM
的样式,预先定义好class
,然后修改DOM
的className
把DOM
离线后修改,比如:先把DOM
给display:none
(有一次reflow
),然后你修改100次,然后再把它显示出来- 不要把
DOM
节点的属性值放在一个循环里当成循环的变量offsetHeight
、offsetWidth
每次都要刷新缓冲区,缓冲机制被破坏- 先用变量存储下来
- 不要使用
table
布局,可能很小的一个小改动会造成整个table
的重新布局div
只会影响后续样式的布局
- 动画实现的速度的选择
- 选择合适的动画速度
- 根据
performance
量化性能优化
- 对于动画新建图层
- 启用
gpu
硬件加速(并行运算),gpu加速
意味着数据需要从cpu
走总线到gpu
传输,需要考虑传输损耗.
- 启用
transform:translateZ(0)
transform:translate3D(0)
https://lavas.baidu.com/guide/v1/foundation/lavas-start)
# 缓存
缓存算是很重要了,面试经常问到,故整理。
总体感知一下它的匹配流程,如下:
- 浏览器发送请求前,根据请求头的expires和cache-control判断是否命中(包括是否过期)强缓存策略,如果命中,直接从缓存获取资源,并不会发送请求。如果没有命中,则进入下一步。
- 没有命中强缓存规则,浏览器会发送请求,根据请求头的last-modified和etag判断是否命中协商缓存,如果命中,直接从缓存获取资源。如果没有命中,则进入下一步。
- 如果前两步都没有命中,则直接从服务端获取资源。
# 强缓存
强缓存:不会向服务器发送请求,直接从缓存中读取资源。
# 强缓存原理
强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程,强制缓存的情况主要有三种(暂不分析协商缓存过程),如下:
- 第一次请求,不存在缓存结果和缓存标识,直接向服务器发送请求
- 存在缓存标识和缓存结果,但是已经失效,强制缓存是啊比,则使用协商缓存(暂不分析)
- 存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果
那么强制缓存的缓存规则是什么? 当浏览器向服务器发起请求时,服务器会将缓存规则放入HTTP响应报文的HTTP头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是Expires
和Cache-Control
,其中Cache-Control
优先级比Expires
高。
# Expires
缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间
,需要和Last-modified
结合使用。Expires
是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
# Cache-Control
在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存,主要取值为: - public:所有内容都将被缓存(客户端和代理服务器都可缓存) - private:所有内容只有客户端可以缓存,Cache-Control
的默认取值 - no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定 - no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存 - max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效
需要注意的是,
no-cache
这个名字有一点误导。设置了no-cache
之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致,也就是协商缓存。而no-store
才表示不会被缓存,即不使用强制缓存,也不使用协商缓存
# nginx设置
强缓存需要服务端设置expires
和cache-control
。 nginx
代码参考,设置了一年的缓存时间:
|
|
浏览器的缓存存放在哪里,如何在浏览器中判断强制缓存是否生效?这就是下面我们要讲到的from disk cache
和from memory cache
。
# from disk cache和from memory cache
细心地同学在开发的时候应该注意到了Chrome的网络请求的Size会出现三种情况from disk cache(磁盘缓存)
、from memory cache(内存缓存)
、以及资源大小数值。
浏览器读取缓存的顺序为memory –> disk。 以访问https://github.com/xiangxingchen/blog
为例 我们第一次访问时https://github.com/xiangxingchen/blog
关闭标签页,再此打开https://github.com/xiangxingchen/blog
时
F5刷新时
简单的对比一下
# 协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:
- 协商缓存生效,返回304和Not Modified
- 协商缓存失效,返回200和请求结果
# Last-Modified和If-Modified-Since
- 浏览器首先发送一个请求,让服务端在
response header
中返回请求的资源上次更新时间,就是last-modified
,浏览器会缓存下这个时间。 - 然后浏览器再下次请求中,
request header
中带上if-modified-since
:[保存的last-modified的值]
。根据浏览器发送的修改时间和服务端的修改时间进行比对,一致的话代表资源没有改变,服务端返回正文为空的响应,让浏览器中缓存中读取资源,这就大大减小了请求的消耗。
由于last-modified依赖的是保存的绝对时间,还是会出现误差的情况:
- 保存的时间是以秒为单位的,1秒内多次修改是无法捕捉到的;
- 各机器读取到的时间不一致,就有出现误差的可能性。为了改善这个问题,提出了使用etag。
# ETag和If-None-Match
etag
是http
协议提供的若干机制中的一种Web
缓存验证机制,并且允许客户端进行缓存协商。生成etag常用的方法包括对资源内容使用抗碰撞散列函数,使用最近修改的时间戳的哈希值,甚至只是一个版本号。 和last-modified
一样. - 浏览器会先发送一个请求得到etag
的值,然后再下一次请求在request header
中带上if-none-match
:[保存的etag的值]
。 - 通过发送的etag
的值和服务端重新生成的etag
的值进行比对,如果一致代表资源没有改变,服务端返回正文为空的响应,告诉浏览器从缓存中读取资源。
etag能够解决last-modified的一些缺点,但是etag每次服务端生成都需要进行读写操作,而last-modified只需要读取操作,从这方面来看,etag的消耗是更大的。
二者对比 - 精确度上:Etag
要优于Last-Modified
。 - 优先级上:服务器校验优先考虑Etag
。 - 性能上:Etag
要逊于Last-Modified
# 用户行为对浏览器缓存的影响
- 打开网页,地址栏输入地址: 查找
disk cache
中是否有匹配。如有则使用;如没有则发送网络请求。 - 普通刷新 (F5):因为 TAB 并没有关闭,因此
memory cache
是可用的,会被优先使用(如果匹配的话)。其次才是disk cache
。 - 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有
Cache-control:no-cache
(为了兼容,还带了Pragma:no-cache
),服务器直接返回 200 和最新内容。
# 总结
期望大规模数据能自动化缓存,而不是手动进行缓存,需要浏览器端和服务器端协商一种缓存机制
- Cache-Control所控制的缓存策略
- last-modified 和 etage以及整个服务端浏览器端的缓存流程
- 基于node实践以上缓存方式
# httpheader
# 可缓存性
public
:表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存。private
:表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。no-cache
:强制所有缓存了该响应的缓存用户,在使用已存储的缓存数据前,发送带验证器的请求到原始服务器only-if-cached
:表明如果缓存存在,只使用缓存,无论原始服务器数据是否有更新
# 到期
max-age=
:设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires
相反,时间是相对于请求的时间。s-maxage=
:覆盖max-age
或者Expires
头,但是仅适用于共享缓存(比如各个代理),并且私有缓存中它被忽略。cdn
缓存max-stale[=]
表明客户端愿意接收一个已经过期的资源。 可选的设置一个时间(单位秒),表示响应不能超过的过时时间。min-fresh=
表示客户端希望在指定的时间内获取最新的响应。
# 重新验证和重新加载
重新验证
must-revalidate
:缓存必须在使用之前验证旧资源的状态,并且不可使用过期资源。proxy-revalidate
:与must-revalidate
作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。immutable
:表示响应正文不会随时间而改变。资源(如果未过期)在服务器上不发生改变,因此客户端不应发送重新验证请求头(例如If-None-Match
或If-Modified-Since
)来检查更新,即使用户显式地刷新页面。在Firefox
中,immutable
只能被用在https:// transactions
.
重新加载
no-store
:缓存不应存储有关客户端请求或服务器响应的任何内容。no-transform
:不得对资源进行转换或转变。Content-Encoding
,Content-Range
,Content-Type
等HTTP
头不能由代理修改。例如,非透明代理可以对图像格式进行转换,以便节省缓存空间或者减少缓慢链路上的流量。no-transform
指令不允许这样做。
# Expires
- 缓存过期时间,用来指定资源到期的时间,是服务器端的时间点
- 告诉浏览器在过期时间前浏览器可以直接从浏览器缓存中存取数据,而无需再次请求
expires
是http1.0
的时候的http1.1
时候,我们希望cache
的管理统一进行,max-age
优先级高于expires
,当有max-age
在的时候expires
可能就会被忽略。- 如果没有设置
cache-control
时候会使用expires
# Last-modified和If-Modified-since
- 基于客户端和服务器端协商的缓存机制
last-modified
–>response header
if-modified-since
–>request header
- 需要与
cache-control
共同使用
last-modified
有什么缺点?
- 某些服务端不能获取精确的修改时间
- 文件修改时间改了,但文件的内容却没有变
# Etag和 If-none-match
- 文件内容的hash值
etag
–>reponse header
if-none-match
–>request header
- 需要与
cache-control
共同使用
好处:
- 比
if-modified-since
更加准确 - 优先级比
etage
更高
# Service Worker
ServiceWorker
是运行在浏览器后台进程里的一段 JS,它可以做许多事情,比如拦截客户端的请求、向客户端发送消息、向服务器发起请求等等,其中最重要的作用之一就是离线资源缓存。
ServiceWorker
拥有对缓存流程丰富灵活的控制能力,当页面请求到 ServiceWorker
时,ServiceWorker
同时请求缓存和网络,把缓存的内容直接给用户,而后覆盖缓存。
注意:需要HTTPS才可以使用 ServiceWorker
# 缓存流程图
# 服务端性能优化
|
|
vue
渲染遇到的问题vue-ssr
和原理和引用
# vue渲染面临的问题
|
|
以前没有前端框架时,
- 用
jsp/php
在服务端进行数据的填充,发送给客户端就是已经
填充好数据`的html - 使用
jQuery
异步加载数据 - 使用
React
和Vue
前端框架- 代价:需要框架全部加载完,才能把页面渲染出来,页面的首屏性能不好
# 多层次的优化方案
- 构建层的模板编译。
runtime
,compile
拆开,构建层做模板编译工作。webpack
构建时候,统一,直接编译成runtime
可以执行的代码 - 数据无关的
prerender
的方式 - 服务端渲染
# Web性能监控
google 开发者提出了一种 RAIL 模型来衡量应用性能,即:Response、Animation、Idle、Load,分别代表着 web 应用生命周期的四个不同方面。并指出最好的性能指标是:100ms 内响应用户输入;动画或者滚动需在 10ms 内产生下一帧;最大化空闲时间;页面加载时长不超过 5 秒。
我们可转化为三个方面来看:响应速度、页面稳定性、外部服务调用
- 响应速度:页面初始访问速度 + 交互响应速度
- 页面稳定性:页面出错率
- 外部服务调用:网络请求访问速度
# 1. 页面访问速度:白屏、首屏时间、可交互时间
我们来看看 google 开发者针对用户体验,提出的几个性能指标
这几个指标其实都是根据用户体验,提炼出对应的性能指标
# 1)first paint (FP) and first contentful paint (FCP)
首次渲染、首次有内容的渲染
这两个指标浏览器已经标准化了,从 performance 的 The Paint Timing API 可以获取到,一般来说两个时间相同,但也有情况下两者不同。
# 2)First meaningful paint and hero element timing
首次有意义的渲染、页面关键元素
我们假设当一个网页的 DOM 结构发生剧烈的变化的时候,就是这个网页主要内容出现的时候,那么在这样的一个时间点上,就是首次有意义的渲染。这个指标浏览器还没有规范,毕竟很难统一一个标准来定义网站的主体内容。
google lighthouse 定义的 first meaningful paint:
https://docs.google.com/document/d/1BR94tJdZLsin5poeet0XoTW60M0SjvOJQttKT-JK8HI/view
# 3)Time to interactive
可交互时间
# 4)长任务
浏览器是单线程的,如果长任务过多,那必然会影响着用户响应时长。好的应用需要最大化空闲时间,以保证能最快响应用户的输入。
# 2. 页面稳定性:页面出错情况
资源加载错误
JS 执行报错
# 3. 外部服务调用
CGI 耗时
CGI 成功率
CDN 资源耗时
# 监控的分类?
web 性能监控可分为两类,一类是合成监控(Synthetic Monitoring,SYN),另一类是真实用户监控(Real User Monitoring,RUM)
# 1.合成监控
合成监控是采用 web 浏览器模拟器来加载网页,通过模拟终端用户可能的操作来采集对应的性能指标,最后输出一个网站性能报告。例如:Lighthouse、PageSpeed、WebPageTest、Pingdom、PhantomJS 等。
# 1. Lighthouse
Lighthouse 是 google 一个开源的自动化工具,运行 Lighthouse 的方式有两种:一种是作为 Chrome 扩展程序运行;另一种作为命令行工具运行。 Chrome 扩展程序提供了一个对用户更友好的界面,方便读取报告。通过命令行工具可以将 Lighthouse 集成到持续集成系统。
展示了白屏、首屏、可交互时间等性能指标和 SEO、PWA 等。
腾讯文档移动端官网首页测速结果:
# 2. PageSpeed
https://developers.google.com/speed/pagespeed/insights/
不仅展示了一些主要的性能指标数据,还给出了部分性能优化建议。
腾讯文档移动端首页测速结果和性能优化建议:
# 3. WebPageTest
WebPageTest
给出性能测速结果和资源加载的瀑布图。
# 4. Pingdom
https://www.pingdom.com/
注意:Pingdom 不仅提供合成监控,也提供真实用户监控。
合成监控方式的优缺点:
优点:
无侵入性。
简单快捷。
缺点:
不是真实的用户访问情况,只是模拟的。
没法考虑到登录的情况,对于需要登录的页面就无法监控到。
# 2.真实用户监控
真实用户监控是一种被动监控技术,是一种应用服务,被监控的 web 应用通过 sdk 等方式接入该服务,将真实的用户访问、交互等性能指标数据收集上报、通过数据清洗加工后形成性能分析报表。例如 FrontJs、oneapm、Datadog 等。
# 1. oneapm
https://www.oneapm.com/bi/feature.html
功能包括:大盘数据、特征统计、慢加载追踪、访问页面、脚本错误、AJAX、组合分析、报表、告警等。
# 2. Datadog
https://www.datadoghq.com/rum/
# 3. FrontJs
https://www.frontjs.com/
功能包括:访问性能、异常监控、报表、趋势等。
这种监控方式的优缺点:
优点:
是真实用户访问情况。
可以观察历史性能趋势。
有一些额外的功能:报表推送、监控告警等等。
缺点:
有侵入性,会一定程度上响应 web 性能。
# performance 分析
在讲如何监控之前,先来看看浏览器提供的 performance api,这也是性能监控数据的主要来源。
performance 提供高精度的时间戳,精度可达纳秒级别,且不会随操作系统时间设置的影响。
目前市场上的支持情况:主流浏览器都支持,大可放心使用。
# 基本属性
performance.navigation: 页面是加载还是刷新、发生了多少次重定向
performance.timing: 页面加载的各阶段时长
各阶段的含义:
performance.memory: 基本内存使用情况,Chrome 添加的一个非标准扩展
performance.timeorigin: 性能测量开始时的时间的高精度时间戳
# 基本方法
performance.getEntries()
通过这个方法可以获取到所有的 performance 实体对象,通过 getEntriesByName 和 getEntriesByType 方法可对所有的 performance 实体对象 进行过滤,返回特定类型的实体。
mark 方法 和 measure 方法的结合可打点计时,获取某个函数执行耗时等。
performance.getEntriesByName()
performance.getEntriesByType()
performance.mark()
performance.clearMarks()
performance.measure()
performance.clearMeasures()
performance.now()
…
# 提供的 API
performance 也提供了多种 API,不同的 API 之间可能会有重叠的部分。
# 1. PerformanceObserver API
用于检测性能的事件,这个 API 利用了观察者模式。
获取资源信息
监测 TTI
监测 长任务
# 2. Navigation Timing API
https://www.w3.org/TR/navigation-timing-2/
performance.getEntriesByType(“navigation”);
不同阶段之间是连续的吗? —— 不连续
每个阶段都一定会发生吗?—— 不一定
重定向次数:performance.navigation.redirectCount
重定向耗时: redirectEnd - redirectStart
DNS 解析耗时: domainLookupEnd - domainLookupStart
TCP 连接耗时: connectEnd - connectStart
SSL 安全连接耗时: connectEnd - secureConnectionStart
网络请求耗时 (TTFB): responseStart - requestStart
数据传输耗时: responseEnd - responseStart
DOM 解析耗时: domInteractive - responseEnd
资源加载耗时: loadEventStart - domContentLoadedEventEnd
首包时间: responseStart - domainLookupStart
白屏时间: responseEnd - fetchStart
首次可交互时间: domInteractive - fetchStart
DOM Ready 时间: domContentLoadEventEnd - fetchStart
页面完全加载时间: loadEventStart - fetchStart
http 头部大小: transferSize - encodedBodySize
# 3. Resource Timing API
https://w3c.github.io/resource-timing/
performance.getEntriesByType(“resource”);
123456 | // 某类资源的加载时间,可测量图片、js、css、XHRresourceListEntries.forEach(resource => { if (resource.initiatorType == ‘img’) { console.info(Time taken to load ${resource.name}: , resource.responseEnd - resource.startTime); }}); |
---|---|
这个数据和 chrome 调式工具里 network 的瀑布图数据是一样的。
# 4. paint Timing API
https://w3c.github.io/paint-timing/
首屏渲染时间、首次有内容渲染时间
# 5. User Timing API
https://www.w3.org/TR/user-timing-2/#introduction
主要是利用 mark 和 measure 方法去打点计算某个阶段的耗时,例如某个函数的耗时等。
# 6. High Resolution Time API
https://w3c.github.io/hr-time/#dom-performance-timeorigin
主要包括 now() 方法和 timeOrigin 属性。
# 7. Performance Timeline API
https://www.w3.org/TR/performance-timeline-2/#introduction
# 总结
基于 performance 我们可以测量如下几个方面:
mark、measure、navigation、resource、paint、frame。
let p = window.performance.getEntries();
重定向次数:performance.navigation.redirectCount
JS 资源数量:p.filter(ele => ele.initiatorType === “script”).length
CSS 资源数量:p.filter(ele => ele.initiatorType === “css”).length
AJAX 请求数量:p.filter(ele => ele.initiatorType === “xmlhttprequest”).length
IMG 资源数量:p.filter(ele => ele.initiatorType === “img”).length
总资源数量: window.performance.getEntriesByType(“resource”).length
不重复的耗时时段区分:
重定向耗时: redirectEnd - redirectStart
DNS 解析耗时: domainLookupEnd - domainLookupStart
TCP 连接耗时: connectEnd - connectStart
SSL 安全连接耗时: connectEnd - secureConnectionStart
网络请求耗时 (TTFB): responseStart - requestStart
HTML 下载耗时:responseEnd - responseStart
DOM 解析耗时: domInteractive - responseEnd
资源加载耗时: loadEventStart - domContentLoadedEventEnd
其他组合分析:
白屏时间: domLoading - fetchStart
粗略首屏时间: loadEventEnd - fetchStart 或者 domInteractive - fetchStart
DOM Ready 时间: domContentLoadEventEnd - fetchStart
页面完全加载时间: loadEventStart - fetchStart
JS 总加载耗时:
123 | const p = window.performance.getEntries();let cssR = p.filter(ele => ele.initiatorType === “script”);Math.max(…cssR.map((ele) => ele.responseEnd)) - Math.min(…cssR.map((ele) => ele.startTime)); |
---|---|
CSS 总加载耗时:
123 | const p = window.performance.getEntries();let cssR = p.filter(ele => ele.initiatorType === “css”);Math.max(…cssR.map((ele) => ele.responseEnd)) - Math.min(…cssR.map((ele) => ele.startTime)); |
---|---|
# 如何监控?
在了解了 performance 之后,我们来看看,具体是如何监控的?
总体流程:性能指标收集与数据上报—数据存储—数据聚合—分析展示—告警、报表推送
这里主要讲述如何收集性能数据。
性能指标收集注意项:1)保证数据的准确性 2)尽量不影响应用的性能
# 1. 基本性能上报
采集数据:将 performance navagation timing 中的所有点都上报,其余的上报内容可参考 performance 分析一节中截取部分上报。例如:白屏时间,JS 和 CSS 总数,以及加载总时长。
其余可参考的上报:是否有缓存?是否启用 gzip 压缩、页面加载方式。
在收集好性能数据后,即可将数据上报。
那选择什么时机上报?
google 开发者推荐的上报方式:
# 2. 首屏时间计算
我们知道首屏时间是一项重要指标,但是又很难从 performance 中拿到,来看下首屏时间计算主要有哪些方式?
https://web.dev/first-meaningful-paint/
1)用户自定义打点—最准确的方式(只有用户自己最清楚,什么样的时间才算是首屏加载完成)
2)lighthouse 中使用的是 chrome 渲染过程中记录的 trace event
3)可利用
Chrome DevTools Protocol 拿到页面布局节点数目。思想是:获取到当页面具有最大布局变化的时间点
4)aegis 的方法:利用 MutationObserver 接口,监听 document 对象的节点变化。
检查这些变化的节点是否显示在首屏中,若这些节点在首屏中,那当前的时间点即为首屏渲染时间。但是还有首屏内图片的加载时间需要考虑,遍历 performance.getEntries() 拿到的所有图片实体对象,根据图片的初始加载时间和加载完成时间去更新首屏渲染时间。
5)利用 MutationObserver 接口提供了监视对 DOM 树所做更改的能力,是 DOM3 Events 规范的一部分。
方法:在首屏内容模块插入一个 div,利用 Mutation Observer API 监听该 div 的 dom 事件,判断该 div 的高度是否大于 0 或者大于指定值,如果大于了,就表示主要内容已经渲染出来,可计算首屏时间。
6)某个专利:在 loading 状态下循环判断当前页面高度是否大于屏幕高度,若大于,则获取到当前页面的屏幕图像,通过逐像素对比来判断页面渲染是否已满屏。https://patentimages.storage.googleapis.com/bd/83/3d/f65775c31c7120/CN103324521A.pdf
# 3. 异常上报
1)js error
监听 window.onerror 事件
2)promise reject 的异常
监听 unhandledrejection 事件
|
|
3)资源加载失败
window.addEventListener(’error’)
4)网络请求失败
重写 window.XMLHttpRequest 和 window.fetch 捕获请求错误
5)iframe 异常
window.frames[0].onerror
6)window.console.error
# 4. CGI 上报
大致原理:拦截 ajax 请求
数据存储与聚合
一个用户访问,可能会上报几十条数据,每条数据都是多维度的。即:当前访问时间、平台、网络、ip 等。这些一条条的数据都会被存储到数据库中,然后通过数据分析与聚合,提炼出有意义的数据。例如:某日所有用户的平均访问时长、pv 等。
数据统计分析的方法:平均值统计法、百分位数统计法、样本分布统计法。