性能优化
缓存
本地数据缓存
indexDB
localstorge
sesstionSorge
内存缓存
- 浏览器做的相关优化
Cache Api
- 一般配合server worker 配合使用
http
强缓存
cachecontrol
maxage
只有资源不过期都一直是这个,不够灵活
不发请求到服务器
协商缓存
是希望能通过先“问一问”服务器资源到底有没有过期,来避免无谓的资源下载。这伴随的往往会是 HTTP 请求中的 304 响应码
会发请求到服务器。
etag
Etag就像一个指纹,资源变化都会导致ETag变化,跟最后修改时间没有关系,ETag可以保证每一个资源是唯一的
ETag的优先级比Last-Modified更高
具体为什么要用ETag,主要出于下面几种情况考虑:
一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
某些服务器不能精确的得到文件的最后修改时间。
If-None-Match
- If-None-Match的header会将上次返回的Etag发送给服务器,询问该资源的Etag是否有更新,有变动就会发送新的资源回来
last-modified
- Last-Modified 表示本地文件最后修改日期
If-Modified-Since
- 浏览器会在request header加上If-Modified-Since(上次返回的Last-Modified的值),询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来
分布式系统
分布式系统里多台机器间文件的Last-Modified必须保持一致,以免负载均衡到不同机器导致比对失败;
分布式系统尽量关闭掉ETag(每台机器生成的ETag都会不一样);
5. Push Cache
Push Cache 其实是 HTTP/2 的 Push 功能所带来的。简言之,过去一个 HTTP 的请求连接只能传输一个资源,而现在你在请求一个资源的同时,服务端可以为你“推送”一些其他资源 —— 你可能在在不久的将来就会用到一些资源。
特点
当匹配上时,并不会在额外检查资源是否过期;
存活时间很短,甚至短过内存缓存(例如有文章提到,Chrome 中为 5min 左右);
只会被使用一次;
HTTP/2 连接断开将导致缓存直接失效;
发送请求
1. 避免多余重定向
。每次重定向都是有请求耗时的,建议避免过多的重定向。
301 永久重定向
- 服务迁移的情况下,使用 301 重定向。对 SEO 也会更友好。
302 临时重定向
2. DNS 预解析
不会直接使用服务的出口 IP,而是使用域名,dns域名解析
DNS Prefetch
- 。它可以告诉浏览器:过会我就可能要去 yourwebsite.com 上下载一个资源啦,帮我先解析一下域名吧。这样之后用户点击某个按钮,触发了 yourwebsite.com 域名下的远程请求时,就略去了 DNS 解析的步骤。使用方式很简单:当然,浏览器并不保证一定会去解析域名,可能会根据当前的网络、负载等状况做决定。标准里也明确写了👇
github.com 的大致解析流程
先检查本地 hosts 文件中是否有映射,有则使用;
查找本地 DNS 缓存,有则返回;
根据配置在 TCP/IP 参数中设置 DNS 查询服务器,并向其进行查询,这里先称为本地 DNS;
如果该服务器无法解析域名(没有缓存),且不需要转发,则会向根服务器请求;
根服务器根据域名类型判断对应的顶级域名服务器(.com),返回给本地 DNS,然后重复该过程,直到找到该域名;
当然,如果设置了转发,本地 DNS 会将请求逐级转发,直到转发服务器返回或者也不能解析。
3. 预先建立连接
。使用 Preconnect[3] 可以帮助你告诉浏览器:“我有一些资源会用到某个源(origin),你可以帮我预先建立连接。
使用 Preconnect 时,浏览器大致做了如下处理:
首先,解析 Preconnect 的 url;
其次,根据当前 link 元素中的属性进行 cors 的设置;
然后,默认先将 credential 设为 true,如果 cors 为 Anonymous 并且存在跨域,则将 credential 置为 false;
最后,进行连接。
4. 使用 CDN
- DNS 解析会将 CDN 资源的域名解析到 CDN 服务的负载均衡器上,负载均衡器可以通过请求的信息获取用户对应的地理区域,从而通过负载均衡算法,在背后的诸多服务器中,综合选择一台地理位置近、负载低的机器来提供服务。
服务器响应
1. 使用流进行响应
2. 业务聚合
BFF (服务于前端的后端)
业务聚合
多端应用
- 非必要,莫新增
3. 避免代码问题
页面解析与处理
1. 注意资源在页面文档中的位置
DOM 解析
javaScript 加载与执行
- JavaScript 会阻塞 DOM 构建,而 CSSOM 的构建又回阻塞 JavaScript 的执行。
、CSS 加载
- 而 CSSOM 的构建又回阻塞 JavaScript 的执行。
手段
- CSS 样式表放在 之中(即页面的头部),把 JavaScript 脚本放在 的最后(即页面的尾部)。
2. 使用 defer 和 async
可以使用 defer 或 async 属性。两者都会防止 JavaScript 脚本的下载阻塞 DOM 构建
defer
- defer 会在 HTML 解析完成后,按照脚本出现的次序再顺序执行
async
- async 则是下载完成就立即开始执行,同时阻塞页面解析,不保证脚本间的执行顺序。
页面文档压缩
一般会进行 HTML 内容压缩(uglify)的同
文本压缩算法(例如 gzip)进行文本的压缩
页面静态资源
总体原则
1.1. 减少不必要的请求
对于不需要使用的内容,其实不需要请求,否则相当于做了无用功;
- shaking
对于可以延迟加载的内容,不必要现在就立刻加载,最好就在需要使用之前再加载;
- 懒加载
对于可以合并的资源,进行资源合并也是一种方法。
- 合并
1.2. 减少包体大小
使用适合当前资源的压缩技术;
避免再响应包体里“塞入”一些不需要的内容。
1.3. 降低应用资源时的消耗
JavaScript 执行了一段 CPU 密集的计算
进行频繁的 DOM 操作
CSS 选择器匹配、图片的解析与处理等,都是要消耗 CPU 和内存的。
1.4. 利用缓存
JavaScript
- 减少不必要的请求
1.1. 代码拆分(code split)与按需加载
dynamic import[1] 来告诉 webpack 去做代码拆分
基于路由的代码拆分
import()
- 可以使用一些Promise的polyfill来实现兼容。可以看到,动态import()的方式不论在语意上还是语法使用上都是比较清晰简洁的。
require.ensure()
- Bundle Loader
- 使用require("bundle-loader!./file.js")来进行相应chunk的加载。该方法会返回一个function,这个function接受一个回调函数作为参数。
- 其实际上也是使用require.ensure()来实现
- 1.2. 代码合并
- 在很多流行的构建工具中(webpack/Rollup/Parcel),是默认会帮你把依赖打包到一起的
- 减少包体大小
2.1. 代码压缩
UglifyJS
做源码级别的压缩。它会通过将变量替换为短命名、去掉多余的换行符等方式,在尽量不改变源码逻辑的情况下,做到代码体积的压缩
在 webpack 的 production 模式下是默认开启的
在 Gulp 这样的任务流管理工具上也有 gulp-uglify 这样的功能插件。
另一个代码压缩的常用手段是使用一些文本压缩算法,gzip 就是常用的一种方式。
响应头的 Content-Encoding 表示其使用了 gzip。
一般服务器都会内置相应模块来进行 gzip 处理,不需要我们单独编写压缩算法模块。例如在 Nginx 中就包含了 ngx_http_gzip_module[3] 模块,通过简单的配置就可以开启
2.2. Tree Shaking
- 其本质是通过检测源码中不会被使用到的部分,将其删除,从而减小代码的体积。
2.3. 优化 polyfill 的使用
polyfill 也是有代价的,它增加了代码的体积。毕竟 polyfill 也是 JavaScript 写的,不是内置在浏览器中,引入的越多,代码体积也越大。所以,只加载真正所需的 polyfill 将会帮助你减小代码体积
browserslist 来帮忙,许多前端工具(babel-preset-env/autoprefixer/eslint-plugin-compat)都依赖于它。使用方式可以看这里。
其次,在 Chrome Dev Summit 2018 上还介绍了一种 Differential Serving[8] 的技术,通过浏览器原生模块化 API 来尽量避免加载无用 polyfill。
这样,在能够处理 module 属性的浏览器(具有很多新特性)上就只需加载 main.mjs(不包含 polyfill),而在老式浏览器下,则会加载 legacy.js(包含 polyfill)。
- Polyfill.io 就会根据请求头中的客户端特性与所需的 API 特性来按实际情况返回必须的 polyfill 集合。
- 2.4. webpack
- 。我们可以通过 webpack-bundle-analyzer 这个工具来查看打包代码里面各个模块的占用大小。
- 解析与执行
3.1. JavaScript 的解析耗时
- 解析与编译消耗了好几百毫秒。所以换一个角度来说,删除不必要的代码,对于降低 Parse 与 Compile 的负载也是很有帮助的
3.2. 避免 Long Task
3.3. 是否真的需要框架
- 对于一个复杂的整站应用,使用框架给你的既定编程范式将会在各个层面提升你工作的质量。但是,对于某些页面,我们是否可以反其道行之呢?
3.4. 针对代码的优化
- 这里要提到的就是 facebook 推出的 Prepack。例如下面一段代码:
缓存
4.1. 发布与部署
4.2. 将基础库代码打包合并
- webpack 在 v3.x 以及之前,可以通过 CommonChunkPlugin 来分离一些公共库。而升级到 v4.x 之后有了一个新的配置项 optimization.splitChunks:
4.3. 减少 webpack 编译不当带来的缓存失效
.3.1. 使用 Hash 来替代自增 ID
- 你可以使用 HashedModuleIdsPlugin 插件,它会根据模块的相对路径来计算 Hash 值。当然,你也可以使用 webpack 提供的 optimization.moduleIds,将其设置为 hash,或者选择其他合适的方式。
4.3.2. 将 runtime chunk 单独拆分出来
- 通过 optimization.runtimeChunk 配置可以让 webpack 把包含 manifest 的 runtime 部分单独分离出来,这样就可以尽可能限制变动影响的文件范围。
4.3.3. 使用 records
- 你可以通过 recordsPath 配置来让 webpack 产出一个包含模块信息记录的 JSON 文件,其中包含了一些模块标识的信息,可以用于之后的编译。这样在后续的打包编译时,对于被拆分出来的 Bundle,webpack 就可以根据 records 中的信息来尽量避免破坏缓存。
css
- 关键 CSS
们会更关注关键渲染路径(Critical Rendering Path,即 CRP),而不一定是最快加载完整个页面。
将关键 CSS 的内容通过