从输入url到显示页面,到底发生了什么(前端角度)
性能优化的目标,就是整体的时间变短
- 用户输入 url
- 浏览器通过DNS,把url解析为IP (ping www.baidu.com)
- 和IP地址建立TCP链接,发送HTTP请求
- 服务器接收请求(查库、读文件等),拼接好返回的HTTP响应
- 浏览器收到首屏html,开始渲染
- 解析html为dom (加载额外的css和js)
- 解析css为 css-tree
- dom + css 生成 render-tree,然后绘图(print)
所谓性能优化,就是上面的步骤加载一起,时间尽可能的短,所以基本也有两个大方向
1、少加载文件
2、少执行代码
Image(图片)
图片主要就是压缩和优化
1、大图用jpg
2、svg矢量图
3、webp兼容性(降级)
Optimize CSS Sprites(优化CSS雪碧图)
图片合并,颜色相近的合并,颜色数更少
css放在header里,选择器性能(尽量不要写超过三层或四层的样式)
Cookie
减少cookie的体积
http cookie 的使用有多种原因,比如授权和个性化。cookie的信息通过http头部在浏览器和服务器端交换。尽可能减少cookie的大小来降低响应时间。
1、消除不必要的cookie
2、尽可能减少cookie的大小
3、注意设置cookie到合适的域名级别,则其他子域名不会被影响
4、正确设置 Expires 日期,早一点的 Expires 日期或者没有尽早的删除 cookie,优化响应时间
Server(服务端)
使用CDN
用户接近你的服务器会减少响应时间。把你的内容发布到多个地理上分散的服务器可以让页面加载更快。
CDN是一群不同地点的服务器,可以更高效地分发内容到用户。一些大公司有自己的CDN。
Gzip Components(传输时用gzip等压缩组件)
http请求或响应的传输时间可以被前端工程师显著减少。终端用户的带宽,ISP,接近对等交换点等等没法被开发团队控制,但是,压缩可以通过减少 http 响应的大小来减少响应时间。
从HTTP/1.1 开始,客户端通过http请求中的 Accept-Encoding 头部来提示支持的压缩:Accept-Encoding:gzip,deflate
如果服务器看到这个头部,它可能会选用列表中的某个方法压缩响应。服务器通过 Content-Encoding 头部提示客户端:Content-Encoding:gzip
gzip 一般可减小响应的 70%。尽可能去gzip更多(文本)类型的文件。html,脚本,样式,xml和json 等等都应该被gzip,而图片,pdf等等不应该被gzip,因为他们本身已经被压缩过,gzip他们只是浪费 cpu,甚至增加文件大小。
性能指标
抛开场景将性能优化,都是耍流氓
Performance API
Performance 接口可以获取到当前页面中与性能相关的信息。它是 High Resolution Time Api的一部分,同时也融合了 Performance Timeline Api、Navigation Timing Api 、User Timing API 和 Resource Timeing API。该类型的对象可以通过调用只读属性 window.performance 来获得。
const timingInfo = window.performance.timing console.log({ "TCP连接耗时": timingInfo.connectEnd - timingInfo.connectStart, "DNS查询耗时": timingInfo.domainLookupEnd - timingInfo.domainLookupStart, "获得首字节耗费时间,也叫TTFB": timingInfo.responseStart - timingInfo.navigationStart, "domReady时间": timingInfo.domContentLoadedEventStart - timingInfo.navigationStart, "DOM资源下载": timingInfo.responseEnd - timingInfo.responseStart });
lighthouse
性能检测神器
chrome插件,or npm install -g lighthouse,报告直接看,很方便
然后在命令行文件夹中 lighthouse www.baidu.com
TCP
1、IP负责找到
2、TCP负责数据完整性和有序性,三次握手,粘包,滑动窗口等机制
3、http应用层,负责应用层数据,数据终止时机
优化策略:
1、长连接
2、减少文件体积
js打包压缩
图片压缩
gzip
3、减少文件请求次数
雪碧图
js,css打包
缓存控制
懒加载
4、减少用户和服务器的距离
CDN
5、本地存储
图片
平时会用到的一些图片格式:PNG、JPG/JPEG、GIF、SVG、WEBP、Base64
PNG是一种无损压缩,比较适合小图(logo)
JPG是一种有损压缩的格式,可以尽可能的减少图片大小
GIF 动图
SVG:类似XML的语法,特点是图片高保真(地图),图片的显示过程是浏览器来做的
WEBP:全能的哥们,是谷歌开发的一种旨在加快图片加载速度的图片格式,聚齐了PNG和JPG的优点,缺点是目前兼容性不是很好
Base64:是一种基于64个可打印字符来表示二进制数据的方法。图片base64之后会变大,所以适合小的矢量图标。好处是不会发出额外的网络请求。
其他图片优化
图片渐进显示(两张图,先显示低分辨率的,然后显示高的)
懒加载
骨架图
有CDN的时候,到底怎么上线前端代码
比如某些CDN没更新到,导致访问的那些用户还是旧的静态资源
1、先上html,加载新的js还没有缓存生效,报错
2、如果先上js,老的html加载新的js,也会报错
使用hash,通过文件内容算出哈希
<html>
<script src="www.aliyun.com/xx_hash.js" />
</html>
每次文件变化,会生成新文件,达到了控制CND缓存的效果
新文件不断的往里面加
旧版本的HTML会访问旧的JS,新版本的HTML就可以访问新的JS了
唯一的缺点是文件会越来越多(通过半年或一年清理一次可以解决)
浏览器缓存
强缓存:expires 和 cachecontrol
如果两者同时存在,Cache-Control优先于 expires
expires是强制缓存策略的关键字段,expires是HTTP1.0中的字段,通过指定一个具体的绝对时间值作为缓存资源的过期时间,具体的设置方法如下:
我们在首次发起请求的时候,服务端会在 Response Header当中设置 expires字段,可以看到我们设置的过期时间是 Tue, 09 Jul 2019 06:16:01 GMT。那么如果在这个时间之前我们发起请求去请求资源,我们就不会发起新的请求,而是直接使用本地已经缓存好的资源,这样我们可以有效减少了不必要的 HTTP 请求,不仅提升了性能,而且节省了流量,减少网络资源的消耗。
expires作为最开始的强制缓存解决方案,看起来没什么问题,但它的时间和服务端的时间是保持一致的,可视我们最终比较的时候和我们本地的时间和expires设置的时间进行比较。如果服务端的时间和我们本地的时间存在误差,那么缓存这个时候很容易就失去了效果,这个时候功能更强大的 Cache-Control出现了。
Cache-Control
Cache-Control是HTTP1.1才有的字段,Cache-Control设置的是一个相对时间,可以更加精准地控制资源缓存。如下:
可以看到这里cache-control 设置的值为 max-age=315360000,这里的单位是秒,315360000代表缓存的时间跨度。由于这个是相对时间,所以不会受到服务端和本地时间不同意造成的缓存问题。315360000秒是一年,现在大多网站的静态资源设置的跨度都是315360000秒,也就是一年的事件跨度。
用法:
expires的字段值是一个时间戳,而Cache-Control可设置的字段值较多,下面来一一介绍:
public:设置了该字段的资源表示可以被任何对象(包括:发送请求的客户端、代理服务器等等)缓存。这个字段值不常用,一般还是使用 max-age= 来精确控制;
private:设置了该字段值得资源只能被用户浏览器缓存,不允许任何代理服务器缓存。在实际开发当中,对于一些含有用户信息的HTML,通常要设置这个字段值,避免代理服务器(CDN)缓存;
no-cache:设置了该字段需要先和服务端确认返回的资源是否发生了变化,如果资源未发生变化,则直接使用缓存好的资源;
no-store:设置了该字段表示禁止任何缓存,每次都会向服务端发起新的请求,拉取最新的资源;
max-age=:设置缓存的最大有效期,单位为秒;
s-maxage=:优先级高于max-age,仅适用于共享缓存(CDN),优先级高于 max-age 或者 Expires头;
max-stale[=]:设置了该字段表面客户端愿意接收已过期的资源,但是不能超过给定的时间限制。
协商缓存
如果命中强制缓存,我们无需发起新的请求,直接使用缓存内容,如果没有命中强制缓存,但是设置了协商缓存,这个时候协商缓存就会发挥作用了。
Last-Modified/If-Modified-Since
Last-Modified 从字面意思就可以看出是最后一次的修改时间,设置方法和我们上面讲的强制缓存的设置方法一样,都是设置一个时间戳,同样它也是由服务端放到 Response Headers 返回给我们,如下:
如果有设置协商缓存,我们在首次请求的时候,返回的 Response Headers 会带有 Last-Modified。当再次请求没有命中强制缓存的时候,这个时候我们的 Request Headers 就会携带 If-Modified-Since 字段,它的值就是我们第一次请求返回给我们的Last-Modified值。服务端接收到资源请求之后,根据 If-Modified-Since 的字段值和服务端资源最后修改的时间是否一致来判断资源是否有修改。如果没有修改,则返回304;如果有修改,则返回新的资源,状态码为200.
缺陷:
服务端对 Last-Modified 标注的最后修改时间只能精确到秒级,如果某些文件在1秒钟以内被修改多次的话,这个时候服务端无法准确标注文件的修改时间。
服务端有时候会定期生成一些文件,有时候文件的内容并没有任何变化,但这个时候Last-Modified会发生改变,导致文件无法使用缓存。
Etag/If-None-Match
可以看到Last-Modified/If-Modified-Since是由一定缺陷的,因此后来又增加了 Etag/If-None-Match,用法与Last-Modified/If-Modified-Since 相似,但是 Etag 更准确。它通常是根据文件的具体内容计算出一个hash值,只要文件的内容不变,它就不会发生改变,保证了唯一性,这一点可以类比人的指纹。
Etag/If-None-Match的用法这里对应Last-Modified/If-Modified-Since。如果我们有设置协商缓存,在首次请求的时候,返回 Response Headers 会带有 Etag值。当再次请求没有命中强制缓存的时候,这个时候我们的 Request Headers 就会携带 If-None-Match字段,它的值就是我们第一次请求返回给我们的 Etag值。服务端再用Etag来进行比较,如果相同就直接使用缓存,如果不同再从服务端拉取新的资源。
浏览器缓存的过程:
1、获取文件
2、文件返回 expires 或者 chche-control 设置过期时间,并且带上 etag 或者 lastModiied 字段
3、再次请求,浏览器查询 expires 或者chche-control是否过期,没过期,强缓存生效,不发出网络请求,直接用缓存
4、如果强缓存失效了,我们会带上 etag 或者 lastModiied数据,使用 if-none-match 或者 If-Modified-Since字段,咨询下后端,是否过期
5、如果没过期,返回304状态码,直接用缓存,过期了直接200,返回新资源
webpack打包和缓存的关系
1、hash,整个项目相关的hash
2、chunkhash,入口文件依赖的chunkhash
3、contenthash 文件内容的hash
其他
1、memory cache 内存缓存,比如存储在变量里,关闭tab就没了
2、Disk cache 硬盘上的缓存
3、Push cache 推送缓存 http2
4、service worker 浏览器背后的独立线程
渲染
首先是解析HTML,整个过程主要是把HTML文档解析为DOM树的过程。如果遇到 <script> 标签会停止解析,先执行标签当中的 JavaScript ;如果是外联方式,也需要等待下载并且执行完对应的JavaScript代码,然后才能够继续执行解析HTML的工作。HTML解析完成后出发 DOMContentLoaded 事件,这里我们就可以操作DOM了。
哪些任务属于 Microtask,哪些任务属于Macrotask呢?
Macrotask:setTimeout、setInterval、I/O、UI Rendering、script当中的所有代码、setImmediate(Node)
Microtask:process.nextTick(Node)、Promise、MutationObserver
优先级如下:
process.nextTick(Node)> Promise > MutationObserver
节流
const throttle = (func, wait=500) => { // 固定时间触发一次 let lastTime = 0 return function(...args){ let now = new Date() if(now - lastTime > wait){ // 大于间隔时间了 lastTime = now func.apply(this, args) } } } window.onscroll = throttle(function(){ console.log('节流'); })
防抖
// 防抖 全程执行一次 const debounce = (func, wait=500)=>{ // 缓存一个定时器id let timer = 0 // 这里返回的函数是每次用户实际调用的防抖函数 // 如果已经设定过定时器了,就清空上一次的定时器 // 开始一个新的定时器,延迟执行用户传入的方法 return function(...args){ if(timer) clearTimeout(timer) timer = setTimeout(()=>{ func.apply(this, args) }, wait) } }