前端性能优化总结

性能优化

缓存

本地数据缓存

  • 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.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),是默认会帮你把依赖打包到一起的
    1. 减少包体大小
    • 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 这个工具来查看打包代码里面各个模块的占用大小。
    1. 解析与执行
    • 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

    1. 关键 CSS
    • 们会更关注关键渲染路径(Critical Rendering Path,即 CRP),而不一定是最快加载完整个页面。

    • 将关键 CSS 的内容通过