在讨论具体如何优化以前,先思考一个经典问题,从输入 URL 到页面加载完成到底发生了什么?html
URL 通过 DNS 解析为 IP 地址,而后与 IP 地址进行 TCP 链接,随后发出 HTTP 请求,服务器处理完请求以后将内容经过 HTTP 发送给客户端,拿到数据后浏览器就开始渲染流程。简单的说分为如下几个步骤:DNS 解析,TCP 链接,HTTP 请求,HTTP响应,浏览器解析并渲染。react
这个问题解决以后,就能够从各个层面分析如何作性能优化了。webpack
DNS 预解析是指浏览器视图在用户访问连接以前解析域名。那接下来用户若是确实访问了该域名,那 DNS 的解析时间将不会有延迟。浏览器对网站第一次的域名 DNS 查找流程为:浏览器缓存 => 系统缓存 => 路由器缓存 => ISP DNS 缓存 => DNS 服务器。因此咱们不须要对和当前页面中同一个域的域名进行预获取(浏览器会缓存解析结果)。web
使用起来也很简单:json
// 若是须要禁用隐式的 DNS prefetch 设置 content = "off"
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//ha.aa.bb">
复制代码
先来一个例子你们直观感觉下二者的差别,能够看到 HTTP/2 性能有大幅提升。这里就必须提到其引入的多路复用技术,在这个技术的支持下,同一域名下的全部请求都在一个通道内完成。这也是引入了帧和流的概念,帧是数据最小传输单位,且标记了属于哪一个流,流就是多个数据帧组成的数据流。多路复用就是一个链接下存在多个流。而 HTTP/1 中每一个请求都必须建立一个 TCP 链接,浏览器还限制了同一个域名下的请求数量,当请求资源较多的时候会出现队头阻塞。浏览器
且 HTTP/2 采用二进制传输代替了 HTTP/1 中的文本传输,解析更加高效。缓存
缓存是性能优化中性价比很高的一种优化方式,显著的减小了网络传输带来的损耗。性能优化
浏览器的缓存机制有四种,按优先级排列以下:服务器
当以上都没有命中资源的时候才去作网络请求。babel
Service Worker 是运行在浏览器背后的独立线程,且脱离浏览器窗口,所以没法直接访问 DOM。也正是独立的特色,咱们每每能够经过它实现离线缓存,消息推送和网络代理等功能。但因为涉及到网络代理的,使用 Service Worker 时,传输协议必须为 HTTPS。
使用步骤分为三步:注册 Service Worker;监听 install 事件,并缓存须要的文件;在下次请求的时候经过拦截请求的方式查询是否存在缓存,存在的话直接读取缓存文件,不然请求资源。
有一点须要注意的是,当咱们没有在 Service Worker 命中缓存,须要调用 fetch
函数获取数据时,浏览器会依次根据缓存优先级继续查找,但此时找到的数据依旧会显示是从 Service Worker 中获取的。
Memory Cache 是指内存中的缓存,是速度很是快的一种缓存。可是虽然读取效率高,其生存时间较短,一旦 Tab 页关闭,内存中的缓存就被释放了。可是具体哪部份内容会被缓存并不肯定,须要根据系统内存的具体状况判断。
Disk Cache 是指存在硬盘中的缓存,读取速度较慢,可是时效性较高。即便在跨站点的状况下,相同地址的资源一旦被缓存下来就不会再次去请求数据。
HTTP/2 中的内容,不太了解~~有了解的同窗能够一块儿交流下呀
前面提到了各类类型的缓存,可是究竟要不要缓存,怎么判断缓存过时时间,这些问题都要从缓存策略中找到答案。
浏览器的缓存策略分为两种:强缓存和协商缓存。
强缓存的实现依赖 Expires
和 Cache-Control
两个字段来控制。强缓存表示缓存期间不须要再次请求,返回状态码 200。
强缓存的早期实现是靠 Expires
字段,这个字段是一个时间戳,表示在这个事件前的缓存都是有效的。能够看到这个字段十分依赖本地时间,若是修改客户端时间可能就会出问题。
HTTP/1.1 出现的 Cache-Control
能够彻底替代 Expires
并提供更丰富的功能。它提供了不少指令:
指令 | 做用 |
---|---|
public | 表示响应能够被客户端和代理服务器缓存 |
private | 表示响应只能被客户端缓存 |
max-age=30 | 缓存 30 s 后过时 |
s-maxage=30 | 覆盖 max-age ,但只在代理服务器中生效 |
no-store | 不缓存任何响应 |
no-cache | 资源被缓存可是当即失效,下次会发起请求验证资源是否过时 |
max-stale=30 | 30s 内及时缓存过时也使用该缓存 |
min-fresh=30 | 但愿在 30s 内获取最新的响应 |
若是缓存过时了或是设置了 no-cache
,则进入协商缓存阶段。协商缓存的实现依赖于两个字段::Etag
以及 Last-modified
。当浏览器发起验证请求资源时,若是资源没有改动,就返回 304 状态码,并更新缓存有效期。
当浏览器发起请求时,会带上 If-Modified-Since
字段,它的值是上次请求资源时 Last-modified
提供的时间戳。服务器再判断在这个时间戳以后是否有改动。可是这个机制仍是存在弊端的,由于时间戳是以秒为单位计算的,若是再 1s 内的改动是没法被感知到的。
Etag
就是为了解决上述问题出现的,浏览器会将上次请求资源返回结果携带的 Etag
做为 If-None-Match
的值发送给服务器,有变更的话就返回新的资源。Etag
的缺陷在于服务器须要有额外的开销,可能会影响性能。
那当没有设置缓存策略时,浏览器会怎么办?一般会取响应头中的 DATE
减去 Last-modified
值的 10% 做为缓存时间。
大体了解了浏览器缓存机制以后,要怎么利用它们来提升性能呢?这才是咱们真正要解决的问题。
对于频繁变更的资源,能够设置 Cache-Control: no-cache
,让浏览器每次都请求服务器验证资源是否有效。若是有效,能够有效减小响应数据大小。
对于一些打包事后的代码文件,好比 webpack ,一般咱们都会对文件名作哈希处理,通常只要文件改动过了,文件名就会改动。因此对于这类文件,能够采用强缓存策略,设置较长时间的缓存时间,好比 Cache-Control: max-age=31536000
。
优化 Loader
就拿 Babel 举例,首先优化 Loader 的文件搜索范围,合理利用 test
,include
,exclude
;缓存编译过的文件,下次只须要编译更改过的代码便可:
loader: 'babel-loader?cacheDirectory=true'
复制代码
HappyPack
HappyPack 将 loader 的同步执行转换为并行执行
plugins: [
new HappyPack({
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory'],
// 开启 4 个线程
threads: 4
})
]
复制代码
DllPlugin
DllPlugin 将制定的库提早打包后引入,下次打包时只有当库版本更新后才须要从新打包。
// 单独配置在一个文件中
// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 想统一打包的类库
vendor: ['react']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
library: '[name]-[hash]'
},
plugins: [
new webpack.DllPlugin({
// name 必须和 output.library 一致
name: '[name]-[hash]',
// 该属性须要与 DllReferencePlugin 中一致
context: __dirname,
path: path.join(__dirname, 'dist', '[name]-manifest.json')
})
]
}
// 先执行上面的配置文件生成依赖文件,再使用 DllReferencePlugin 将依赖文件引入项目中
// webpack.conf.js
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest 就是以前打包出来的 json 文件
manifest: require('./dist/vendor-manifest.json'),
})
]
}
复制代码
按需加载
在开发 SPA 项目时,比较好的一个实践是,可使用按需加载将每一个路由页面单独打包为一个文件,避免首页加载文件过大,固然对于大型类库也一样适用。
Scope Hoisting
// a.js
export const a = 1
// index.js
export {a} from './a.js'
复制代码
有以上两个文件,使用 webpack打包后会变为:
[
/* 0 */
function (module, exports, require) {
//...
},
/* 1 */
function (module, exports, require) {
//...
}
]
复制代码
使用了 Scope Hoisting 以后,代码会尽可能合并到一个函数中,变为:
[
/* 0 */
function (module, exports, require) {
//...
}
]
复制代码
能够看到代码量会减小不少,咱们能够经过在 webpack 中配置 optimization.concatenateModules
来开启 Scope Hoisting。
Tree Shaking
Tree Shaking 用于删除应用中未被引用的代码,webpack4 默认开启了这个功能。
web 应用中图片几乎是必不可少的资源,也是十分损耗性能的一个点。图片优化的最好切入点在于根据业务场景作好图片选型方案。
JPG
JPG 的特色是有损压缩,体积小,加载快,但不支持透明。因此一般能够用于大的背景图或是轮播图等色彩丰富的图片中。不适用于一些矢量图形或是对比比较鲜明的图片。
PNG
PNG 的特色是无损压缩,质量高,体积大。一般用于透明图片,小 LOGO,或是颜色简单但对比强烈的图片。
SVG
SVG 的特色是体积小,不失真,兼容性好。通常页面上的图标均可以用 SVG 制做,只是渲染成本较高,对性能可能略有影响。
Base64
Base64 的特色是文本文件,可用于页面上的小图标。
WebP
WebP 的特色是支持透明,支持动态图片,支持有损压缩和无损压缩。可是兼容性太差,可是对于兼容 WebP 的浏览器能够尽可能多使用。
CSS
不少时候一些效果能够直接经过 CSS 实现,这个时候就大胆的放弃使用图片吧。
预加载
<link rel="preload" href="http://example.com">
复制代码
若是页面某些资源可能不会立刻用到,可是但愿尽早获取,可使用预加载。它会强制浏览器请求资源,但不会阻塞 onload
事件。预加载能够下降首屏加载时间,由于能够将一些不影响首屏但很重要的文件延后加载。
预渲染
<link rel="prerender" href="http://example.com">
复制代码
预渲染是Chrome中的一项功能,能够改善用户可见的页面加载时间。预渲染由引用页面中的 <link rel =“prerender">
元素触发。为预渲染的URL建立一个隐藏页面,该页面将彻底加载全部相关资源,以及执行 JS 文件。若是用户进入该页面,则隐藏页面将被交换到当前选项卡中并使其可见。
懒加载
大多数人应该都接触过图片懒加载,实际上就是将不关键的资源延后加载。举个例子,当图片没有出如今可视区域内,咱们能够先统一用一张占位图来显示,将真实的 src
存入自定义属性中,当进入到到可视区域后,再替换 src
属性。
懒执行
将某些比较耗时的且不须要在首屏中使用的逻辑延后到使用时再计算,通常用于首屏优化中。
CDN 是指一组分布在各个地区的服务器。这些服务器存储数据的副本,所以服务器能够根据那些服务器距离用户最近来知足数据的请求。CDN 适合被用于存放静态资源。