前言
前端发展的真滴快,无论从面试、webpack、还是各大框架打包编译及打包结构都可以看到一些对资源优化的蛛丝马迹,最经典的面试题就是:url输入到页面呈现过程,除了考察过程本身,更主要的是这个过程的优化,以此过程来聊一下如何处理资源
DNS
这里不说DNS怎么解析的,DNS本身解析的过程成也会有各种缓存,比如:浏览器缓存、系统缓存、路由器缓存等,这个过程前端还是蛮难去接触到,但是DNS解析的越快页面呈现的也越快,这个是毫无疑问的,所以前端能做的就是帮助尽快去解析DNS
这里就涉及到了DNS的预解析(多页面慎用):
<!-- 告知浏览器页面DNS需要预解析 -->
<meta http-equiv="x-dns-prefetch-control" content="on" />
<!-- 强制解析 -->
<link rel="dns-prefetch" href="//www.chasejourney.top" />
<link rel="dns-prefetch" href="//www.baidu.com" />
复制代码
看一下各平台的处理
- 京东
<!-- 太多了,随便copy俩 -->
<link rel="dns-prefetch" href="//static.360buyimg.com">
<link rel="dns-prefetch" href="//misc.360buyimg.com">
...
...
复制代码
- 百度
<link rel="dns-prefetch" href="//dss0.bdstatic.com">
<link rel="dns-prefetch" href="//dss1.bdstatic.com">
...
...
复制代码
不多举例了,可以找一些大型网站看一下
Http
tcp
握手和挥手是为了确认保证客户端和服务端接收、发送能力,以及数据传输的安全和完整性,这里一笔带过
前端资源包括图片、icon、js、css等均是通过http
请求获取的,如果能够加快资源的获取
速度可以极大的提高页面性能,注意:这里说的获取
而不是下载
限制条件
我们获取资源速度的时间有以下条件:
tcp
连接频率- 文件获取方式,缓存or下载
- 下载文件大小
- 下载数量
根据上述限制条件,接下来我们一一分析
TCP连接频率
说到tcp
链接就要聊到各http
版本的链接方式
http1.0
http1.0
规定客户端
和服务端
允许建立短暂链接,每次请求需要建立新的链接,服务端
处理完毕断开链接,当页面中资源多起来的时候,频繁的链接对服务端是极大的消耗,而且对客户端获取到资源的时间也会正常,一般情况下浏览器对同一个域名的请求上线是6-8个,超过这一数量可能会阻塞,即线头阻塞(HOLB: head of line blocking)
http1.1
http1.1
做了一些优化:
- 默认开启
keep-alive
http1.1
支持长连接,默认开启Connection: keep-alive
,不会关闭tcp
连接,可以继续发送http
请求,一定程度上弥补了频繁链接的问题,但长连接不能并行发起请求,各请求依赖上一个请求的响应,所以有时候依然会造成阻塞
- 管线化(pipelining)
http1.1
管线化(pipelining)支持在一个tcp
连接中多个http
请求一一发送,各请求不需要等待服务器对前一个请求的响应,不过客户端在接收响应的时候还是按照发送的顺序接收的,如果前一个请求阻塞后续的请求都需要等待,所以仍会造成阻塞
http2.0
http2.0
是基于SPDY设计的,主要有一下特点:
- 多路复用
即共享tcp
连接,一个request
请求对应一个id
,一个连接上可以有多个request
请求,每个连接的request
可以随机混杂在一块,接收方可以根据request
的id
将request
在归属到各自不同的服务器里
- 新的二进制格式
http1.x
的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑http2.0
的协议解析决定采用二进制格式,实现方便且健壮
- header压缩
http1.x
中header携带了很多重复复杂的信息,http2.0
使用算法压缩了header
,减小包的大小和数量
- 服务端推送
采用了SPDY
网页,如服务器向客户端推送style.css
的同时会推送一个style.js
,当客户端再次获取style.js
时会从缓存中获取,不需要在发请求
相对于1.x版本,http2.0
请求资源的速度会快很多
总结
so,使用http
的优先级如下:http2.0
> http1.1
> http1.0
文件获取方式
毫无疑问从缓存中拿资源的效率要高于下载,这里的缓存指的是http
缓存,而http
缓存又分为强缓存
和协商缓存
简单聊一下它们的区别:
第一次请求到资源后,浏览器会根据请求头中的缓存标识决定是否缓存、如何缓存资源,强缓存和协商缓存都是拉取的缓存数据(协商失败则是下载新资源),区别就在于是否和服务器有交流
强缓存
浏览器请求资源时会先获取缓存的请求头信息,如果Cache-Control
和expires
命中强缓存则拉取本地缓存
expires
expires
是http1.0的规范,它的值是一个绝对时间的GMT格式的时间字符串(如Wen, 18 Mar 2020 17:25:00 GMT),这个值表示资源的过期时间,未超过该时间则会从本地拉取缓存
缺点是expires
的值是绝对时间,这对客户端和服务端时间一致性要求就比较高,一旦修改了客户端时间或客户端和服务端时间差异较大就会出现缓存混乱的问题
Cache-Control
Cache-Control
是http1.1的规范,通过max-age
来判断(如max-age=1000,表示1秒后过期),这是一个相对时间,除了max-age还有以下属性:
- no-cache: 不使用强缓存,但可以使用协商缓存
- no-store: 强缓存和协商缓存均无法使用
- pubilc: 浏览器和代理服务器都可以对资源进行缓存
- privite: 只有浏览器可以缓存,代理服务器不能缓存
- s-maxage: 代理服务器的缓存有效期,同时设置max-age和s-maxage,客户端会使用max-age,代理服务器会使用s-maxage
其实还有一个Pragma,不过它已经逐渐被抛弃了,这里不做过多了解了
优先级
Cache-Control > expires
协商缓存
当强缓存没有命中,接下来就要看是否能够命中协商缓存,所谓协商就是浏览器与服务器协商,如果资源还是老的资源没有更新变动则返回304告诉浏览器从缓存中拉取数据,主要是通过请求头中的last-modified
和etag
来判断是否需要重新拉取数据
last-modified/if-modified-since
如果第一次请求服务器返回的头信息带有last-modified
信息后续请求会携带if-modified-since
,last-modified
记录的是资源的最后修改时间,if-modified-since
记录的是上次last-modified
,服务器会将浏览器传过来的if-modified-since
和资源修改时间做对比,如果时间一致则表示资源未修改返回304,拉取缓存的资源,如果时间不一致则向服务器请求新的资源,更新last-modified
为新的修改时间
缺点是只能精确定秒,秒以内修改无法,假如一个文件1s内修改了n次,last-modified
是无法捕获的。另外一个问题是只要文件被修改了,无论内容是否有变化,都会以最新的修改时间为判断依据,这就导致了一些没必要的请求,接下来的etag就是来解决这个问题的
etag/if-none-match
和last-modified
相同,第一次请求服务器返回的头信息携带了etag
后续的请求会携带if-none-match
,if-none-match
记录的是上次的eatg
,etag
和last-modified
判断同样过程相似,服务器对比浏览器传过来的if-none-match
和当前内容的标识字符串,不同则返回新的资源和新的标识字符串
与last-modified
区别:
etag
是唯一标识字符串,只要内容变动etag就会变化,更精确的感知内容的变化- 即使304,由于
etag
重新生成过,服务器还是会将etag
返回,即使这个etag
没有变化
优先级
etag > last-modified
下载资源
即使是下载新资源也有相应的优化方案
link
- preload
preload 提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),需要执行时再执行,使用如下:
<!-- 需要使用as属性指定特定的资源类型以便浏览器为其分配一定的优先级 -->
<link href="pre.css" rel="preload" as="style">
复制代码
不要随意使用preload
,一旦使用preload
无论资源是否被使用都会被提前加载,会给网页带来不必要的负担
- prefetch
它的作用是告诉浏览器加载下一页面可能会用到的资源,可以用来优化下一页面的加载速度,使用如下:
<link href="pre.css" rel="prefetch">
复制代码
preload
和prefetch
同样适用于js:
<link href="pre.js" rel="preload" as="script">
<link href="pre.js" rel="prefetch">
复制代码
script
JS可能会修改DOM或CSS,所以浏览器遇到 script 标记,会唤醒 JavaScript解析器,然后就停止解析HTML,所以日常开发通常会把script
标签放在最下边,通常遇到script
标签就会去加载资源,我们可以通过defer
和async
异步加载
- defer
js
脚本在文档加载解析完毕DOMContentLoaded执行之前完成
- async
加载和渲染后续文档元素的过程与js
脚本并行进行,有很多的不确定性,可能在文档解析完毕之前也可能之后
defer更符合我们日常的需求,保险起见建议还是要把script
标签放在最后
文件大小
减小文件大小同尺寸有如下操作:
- 压缩图片、css、js等资源
- 前端打包gzip,服务端开启gzip模式
- 拆包,将css资源抽离,同时方便缓存
- 抽离公共模块,同样也可以方便缓存
- webpack4+生成环境默认开启
tree-shake
和scope
模式,这些都依赖import
的引入 - 生产关闭
source-map
,但当你需要监听生产环境问题是需要自行打包一份source-map
- 代码优化(脑补中)
下载数量
同一域名下载数量有限,所以我们通常会尽量减少文件的数量,通常有如下操作:
- 小图片转base64(webpack配置),或使用icon代替图片,或者使用雪碧图
- 提供静态资源服务器存储静态资源或cdn
- 动态加载,通过
import
即可实现 defer
、async
同样可以减少同一时间下载文件数量
wbepack
上班已经涉及到webpack
的一些内容,这里介绍一下上述没有提到的内容
hash
正确处理webpack
的不同hash
值,配合后续缓存策略,简单介绍一个3个hash
值
hash
整体项目有变化就会变化
chunkhash
根据不同的入口文件解析、构建,生成哈希值,将一些共用模块和逻辑抽出,则不会受业务逻辑的变化影响
contenthash
当前文件内容变化才会变化,所以通常将css
抽离出js
,加上contenthash
,即使js
变化了,只要css
没变则不会发生变化
多进程
- 编译多进程
Happypack
可以开启多进程编译,加快打包编译速度
- 压缩多进程
UglifyjsWebpackPlugin
压缩是单进程,可以使用TerserWebpackPlugin
代替
公共资源抽离
你可以通过dllPlugin
或 externals
进行静态依赖包的分离,当然我认为通过externals
方式并通过cdn
方式更简单、更有效一些
预渲染、seo
可以通过prerender-spa-plugin
去做一些与渲染、seo的优化,又或者是骨架屏
结语
前端资源都可以算是静态资源,可以做的优化也非常多,实际情况又能落地多少既要看项目合不合适这些优化方案,也要看是否有精力去做这些优化,可以确定的是一旦形成习惯,一切水到渠成