提升页面性能的方法有哪些?

  1. 资源压缩合并,减少 HTTP 请求 或者 避免同一时间的过多次数请求
  2. 非核心代码异步加载 -> 异步加载的方式 -> 异步加载的区别
  3. 利用浏览器缓存 -> 缓存的分类 -> 缓存的原理
  4. 利用 CDN
  5. 预解析 DNS

    资源压缩与合并

    主要包括这些方面:压缩代码HTML/CSS/JS,压缩图片与其他资源,Tree-Sharking消除无用代码,以上Webpack构建项目可以搞定

资源压缩可以从文件中去掉多余的字符,比如回车、空格。你在编辑器中写代码的时候,会使用缩进和注释,这些方法无疑会让你的代码简洁而且易读,但它们也会在文档中添加多余的字节

html 压缩

html 代码压缩就是压缩这些在文本文件中有意义,但是在 HTML 中不显示的字符,包括空格,制表符,换行符等,还有一些其他意义的字符,如 HTML 注释也可以被压缩。

  1. 使用在线网站进行压缩(开发过程中一般不用)
  2. nodejs 提供了 html-minifier 工具
  3. 后端模板引擎渲染压缩

css 压缩

css 代码压缩简单来说就是无效代码删除和 css 语义合并

  1. 使用在线网站进行压缩(开发过程中一般不用)
  2. 使用 html-minifier 工具
  3. 使用 clean-css css 压缩

js 的压缩和混乱

js 的压缩和混乱主要包括以下这几部分:

  • 无效字符的删除
  • 剔除注释
  • 代码语义的缩减和优化
  • 代码保护(代码逻辑变得混乱,降低代码的可读性,这点很重要)
  1. 使用在线网站进行压缩(开发过程中一般不用)
  2. 使用 html-minifier 工具
  3. 使用 uglifyjs2 js 进行压缩

避免同时间段请求过多

  • CSS 实现雪碧图:使用background-position共享一张图
  • 图片懒加载:监听滚动后offsetTop, 使用src 替换 src(真实路径)
  • 列表懒加载(分批加载):监听滚动后offsetTop, 发送请求加载下一页的数据
  • 路由懒加载
  • 代码分包分块加载(webpack)
  • 预加载技术
  • 小程序分包、预下载
  • 异步加载

    动态脚本创建

    在还没定义 defer 和 async 前,异步加载的方式是动态创建 script,通过 window.方法确保页面加载完毕再将 script 标签插入到 DOM 中。具体代码如下

    1. function loadJS(url, callback) {
    2. let script = document.createElement("script");
    3. script.type = "text/javascript";
    4. // IE
    5. if (script.readyState) {
    6. script.onreadystatechange = function () {
    7. if (script.readyState == "loaded" || script.readyState == "complete") {
    8. script.onreadystatechange = null;
    9. if (typeof callback === "function") {
    10. callback();
    11. }
    12. }
    13. };
    14. } else {
    15. script.onload = function () {
    16. if (typeof callback === "function") {
    17. callback();
    18. }
    19. };
    20. }
    21. script.src = url;
    22. document.getElementsByTagName("head")[0].appendChild(script);
    23. }
    24. window.onload = function () {
    25. loadJS("js/index.js", () => { });
    26. }

    defer

  • 兼容所有浏览器

  • defer 属性规定是否对脚本执行进行延迟,直到页面加载为止
  • 如果是多个脚本,该方法可以确保所有设置了 defer 属性的脚本按顺序执行
  • 如果脚本不会改变文档的内容,可将 defer 属性加入到 script 标签中,以便加快处理文档的速度

    1. <script defer type="text/javascript" src="js/index.js"></script>

    async

  • async 属性是 HTML5 新增属性,需要 Chrome、FireFox、IE9+浏览器支持

  • async 属性规定一旦脚本可用,则会异步执行
  • async 属性仅适用于外部脚本
  • 如果是多个脚本,该方法不能保证脚本按顺序执行

    1. <script async type="text/javascript" src="js/index.js"></script>

    注意

  • defer 是在 HTML 解析完之后才会执行,如果是多个,按照加载的顺序依次执行

  • async 是在加载完之后立即执行,如果是多个,执行顺序和加载顺序无关

浏览器缓存

浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:

  1. Memory Cache
  2. Service Worker Cache
  3. HTTP Cache
  4. Push Cache

HTTP 缓存是我们日常开发中最为熟悉的一种缓存机制。它又分为强缓存协商缓存
优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。

强缓存

不会向服务器发送请求,直接从缓存中读取资源,HTTP 状态码为 200

强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信。
image.png

expires

  1. expires: Sat, 29 Aug 2020 08:43:05 GMT

当services端返回响应时,在 **Response Header** 中讲过期时间写入 **expires** 字段中。
如上图所示:**expires** 存储里的过期时间 **expires: Sat, 29 Aug 2020 08:43:05 GMT**,当浏览器再次加载对应资源文件时,如果在这个过期时间内,则命中强缓存。反之重新获取。

**expires** 问题描述:

它最大的问题在于对 本地时间的依赖。 如果服务端和客户端的时间设置可能不同(例如时区),或用户手动调整客户端时间导致时间同步,那么 **expires** 将无法达到我们的预期效果。

Cache-Control

Cache-Control 可在请求报文与响应报文中设置

public 表时响应可以被客户端和代理服务器缓存
private 表示响应只可以被客户端缓存
max-age=xxx 缓存xx秒后就过期,需要重新请求
s-maxage=xxx 覆盖max-age,作用同上,只在代理服务器生效
no-store 不缓存如何响应
no-cache 资源被缓存,但是立即失效,下次会发起验证资源是否过期
max-stale=xxx xx秒内,即使缓存过期,也使用该缓存
min-fresh=xxx 希望在xx秒内获取最新的响应

max-age

  1. cache-control: max-age=600

如上图所示:**cache-control** 通过 **max-age** 来控制资源的有效期,value 值为时间长度,秒为单位,用数值表示。则代表在这个请求正确返回时间(浏览器也会记录下来)的 600 秒内再次加载资源,就会命中强缓存。完美地规避了时间戳带来的潜在问题。

s-maxage

细节注意: s-maxage 仅在代理服务器中生效,客户端中我们只考虑 max-age

  1. cache-control: max-age=600, s-maxage=3600

s-maxage 就是用于表示 cache 服务器上(比如 cache CDN)的缓存的有效时间的,并只对 public 缓存有效
s-maxage 优先级高于 max-age,当两者同时出现时,优先考虑 s-maxage
如果 s-maxage 未过期,则向代理服务器请求其缓存内容

public:所有内容都将被缓存(客户端和代理服务器都可缓存)
private:所有内容只有客户端可以缓存,Cache-Control的默认取值

简单概括:
两者同时存在的话,Cache-Control 优先级高于 Expires
**expires****cache-control** 两者区别就在于 **expires****http1.0** 的产物,**cache-control****http1.1** 的产物。
在某些不支持 **http1.1** 的环境下,**expires** 就会发挥用处。所以 **expires** 其实是过时的产物,现阶段它的存在是一种向下兼容。
因此,**cache-control** 可以视作是 **expires** 的完全替代方案。

思考:

强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容较客户端是否已经发生了更新呢?此时我们需要协商缓存策略。

协商缓存

协商缓存依赖于服务端与浏览器之间的通信

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,服务器会根据这个请求的
request header 的一些参数来判断是否命中协商缓存,如果命中,直接返回 304 状态码,内容为空,并带上新的 response header 通知浏览器从缓存中读取资源;另外协商缓存需要与 cache-control 共同使用。

Last-ModifiedIf-Modified-Since

  • 当第一次请求资源时,服务器将资源传递给客户端时,会将资源最后更改的时间以 Last-Modified 的形式加在实体首部上一起返回给客户端;
  • 随后接下每次请求时,都会带上 If-Modified-Since 字段的时间戳字段,值为上一次 response 返回给它的 Last-Modified 值;
  • 服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值;否则,返回如上图的 304 响应,Response Headers 不会再添加 Last-Modified 字段。 ```html Last-Modified: Sat, 29 Aug 2020 08:43:05 GMT

If-Modified-Since: Sat, 29 Aug 2020 08:43:05 GMT

  1. ![3174701-bb7148a4431ccda1.png](https://cdn.nlark.com/yuque/0/2020/png/1544252/1598696949277-6d5bf072-8c99-45bb-bdfe-fb27495b6811.png#crop=0&crop=0&crop=1&crop=1&height=380&id=qSZFS&margin=%5Bobject%20Object%5D&name=3174701-bb7148a4431ccda1.png&originHeight=380&originWidth=438&originalType=binary&ratio=1&rotation=0&showTitle=false&size=59648&status=done&style=none&title=&width=438)<br />但 `Last-Modified` 存在一些缺点:
  2. - 某些服务端不能获取精确的修改时间
  3. - 文件修改时间改了,但文件内容却没有变
  4. 既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?如下介绍
  5. <a name="fUveq"></a>
  6. ### ETag 和 If-None-Match
  7. Etag 是上一次加载资源时,服务器返回的 response header,是对该资源的一种唯一标识,只要资源有变化,Etag 就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的 Etag 值放到 request header 里的 If-None-Match 里,服务器只需要比较客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现 ETag 匹配不上,那么直接以常规 GET 200 回包形式将新的资源(当然也包括了新的 ETag)发给客户端;如果 ETag 是一致的,则直接返回 304 知会客户端直接使用本地缓存即可。<br />ETag If-None-Match<br />![面试-页面性能-ETagIf-None-Match.png](https://cdn.nlark.com/yuque/0/2020/png/1544252/1591780398833-7f9f9a7f-d885-4655-af87-7e45e3e072ef.png#crop=0&crop=0&crop=1&crop=1&height=348&id=EgwE1&margin=%5Bobject%20Object%5D&name=%E9%9D%A2%E8%AF%95-%E9%A1%B5%E9%9D%A2%E6%80%A7%E8%83%BD-ETag%E5%92%8CIf-None-Match.png&originHeight=348&originWidth=713&originalType=binary&ratio=1&rotation=0&showTitle=false&size=5428&status=done&style=none&title=&width=713)<br />两者之间对比:<br />首先在精确度上,Etag 要优于 Last-Modified。Last-Modified 的时间单位是秒,如果某个文件在 1 秒内改变了多次,那么他们的 Last-Modified 其实并没有体现出来修改,但是 Etag 每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的 Last-Modified 也有可能不一致。<br />第二在性能上,Etag 要逊于 Last-Modified,毕竟 Last-Modified 只需要记录时间,而 Etag 需要服务器通过算法来计算出一个 hash 值。<br />第三在优先级上,服务器校验优先考虑 Etag<br />缓存的机制<br />强制缓存优先于协商缓存进行,若强制缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回 304,继续使用缓存。主要过程如下:<br />![3174701-bb7148a4431ccda1.png](https://cdn.nlark.com/yuque/0/2020/png/1544252/1598697727738-29c80df7-2efa-4e2f-a2e3-4b0a539b5369.png#crop=0&crop=0&crop=1&crop=1&height=640&id=F1gWt&margin=%5Bobject%20Object%5D&name=3174701-bb7148a4431ccda1.png&originHeight=640&originWidth=944&originalType=binary&ratio=1&rotation=0&showTitle=false&size=59829&status=done&style=none&title=&width=944)<br />缓存的机制<br />用户行为对浏览器缓存的影响
  8. 1. 地址栏访问,链接跳转是正常用户行为,将会触发浏览器缓存机制;
  9. 2. F5 刷新,浏览器会设置 max-age=0,跳过强缓存判断,会进行协商缓存判断;
  10. 3. ctrl+F5 刷新,跳过强缓存和协商缓存,直接从服务器拉取资源。
  11. <a name="gf8Ci"></a>
  12. # 性能分析 - Performance API
  13. 市面上实现对网页性能监控工具提供,主要也是依靠 Performance API
  14. 侧重点查看方法使用
  15. 1. Performance.timing - 对象包含延迟相关的性能信息
  16. 2. Performance.getEntries() - 基于给定的 `filter` 返回一个 `[PerformanceEntry](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceEntry)` 对象的列表。
  17. 3. Performance.getEntriesByType() - 基于给定的 `entry type` 返回一个 `[PerformanceEntry](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceEntry)` 对象的列表。
  18. 4. Performance.now() - 返回一个表示从性能测量时刻开始经过的毫秒数 `[DOMHighResTimeStamp](https://developer.mozilla.org/zh-CN/docs/Web/API/DOMHighResTimeStamp)`
  19. <a name="Qb45c"></a>
  20. ## timing已弃用
  21. ```javascript
  22. function performancePrintTiming() {
  23. /**
  24. * timing 已经弃用
  25. * 接口 Performance 的只读属性 timeOrigin 返回一个表示 the performance measurement 开始时间的高精度 timestamp
  26. */
  27. if (!window?.performance?.timing) {
  28. throw new Error("Browsers do not support timing");
  29. }
  30. const {
  31. connectEnd,
  32. connectStart,
  33. domComplete,
  34. domContentLoadedEventEnd,
  35. domContentLoadedEventStart,
  36. domInteractive,
  37. domLoading,
  38. domainLookupEnd,
  39. domainLookupStart,
  40. fetchStart,
  41. loadEventEnd,
  42. loadEventStart,
  43. navigationStart,
  44. redirectEnd,
  45. redirectStart,
  46. requestStart,
  47. responseEnd,
  48. responseStart,
  49. secureConnectionStart,
  50. unloadEventEnd,
  51. unloadEventStart,
  52. } = window.performance.timing;
  53. console.log("准备新页面时间耗时: ", fetchStart - navigationStart);
  54. console.log("redirect 重定向耗时: ", redirectEnd - redirectStart);
  55. console.log("Appcache 耗时: ", domainLookupStart - fetchStart);
  56. console.log("unload 前文档耗时: ", unloadEventEnd - unloadEventStart);
  57. console.log("DNS 查询耗时: ", domainLookupEnd - domainLookupStart);
  58. console.log("TCP 连接耗时: ", connectEnd - connectStart);
  59. console.log("request 请求耗时: ", responseEnd - requestStart);
  60. console.log("请求完毕至 DOM 加载: ", domInteractive - responseEnd);
  61. console.log("解析 DOM 树耗时: ", domComplete - domInteractive);
  62. console.log("load事件耗时: ", loadEventEnd - loadEventStart);
  63. console.log("加载时间耗时: ", loadEventEnd - navigationStart);
  64. console.log("白屏时间: ", responseStart - navigationStart);
  65. }

getEntriesByType

  1. function performancePrint() {
  2. if (!window?.performance?.getEntriesByType) {
  3. throw new Error("Browsers do not support getEntriesByType");
  4. }
  5. const {
  6. connectEnd,
  7. connectStart,
  8. decodedBodySize,
  9. domComplete,
  10. domContentLoadedEventEnd,
  11. domContentLoadedEventStart,
  12. domInteractive,
  13. domainLookupEnd,
  14. domainLookupStart,
  15. duration,
  16. encodedBodySize,
  17. entryType,
  18. fetchStart,
  19. initiatorType,
  20. loadEventEnd,
  21. loadEventStart,
  22. name,
  23. nextHopProtocol,
  24. redirectCount,
  25. redirectEnd,
  26. redirectStart,
  27. requestStart,
  28. responseEnd,
  29. responseStart,
  30. secureConnectionStart,
  31. serverTiming = [],
  32. startTime,
  33. transferSize,
  34. type,
  35. unloadEventEnd,
  36. unloadEventStart,
  37. workerStart,
  38. workerTiming = [],
  39. } = window.performance.getEntriesByType("navigation")[0];
  40. console.log("准备新页面时间耗时: ", fetchStart - startTime);
  41. console.log("redirect 重定向耗时: ", redirectEnd - redirectStart);
  42. console.log("Appcache 耗时: ", domainLookupStart - fetchStart);
  43. console.log("unload 前文档耗时: ", unloadEventEnd - unloadEventStart);
  44. console.log("DNS 查询耗时: ", domainLookupEnd - domainLookupStart);
  45. console.log("TCP 连接耗时: ", connectEnd - connectStart);
  46. console.log("request 请求耗时: ", responseEnd - responseStart);
  47. console.log("请求完毕至 DOM 加载: ", domInteractive - responseEnd);
  48. console.log("解析 DOM 树耗时: ", domComplete - domInteractive);
  49. console.log("load事件耗时: ", loadEventEnd - loadEventStart);
  50. console.log("加载时间耗时: ", loadEventEnd - startTime);
  51. console.log("白屏时间: ", responseStart - startTime);
  52. }

页面性能类 - 图2
window.performance.timing 时间戳与页面整个加载流程中的关键时间节点有着一一对应的关系

getEntries

可以查询所有资源的耗时,也可以根据资源类型来查看某种类型的资源耗时,PerformanceResourceTiming 对象

image.png

其他

SSR 服务器渲染:解决 SPA 框架带来的 JS 动态渲染页面带来的延迟和白屏问题

参考:
Performance API MDN docs
Performance API W3 docs
彻底理解浏览器的缓存机制
前端性能优化原理与实践
设计一个无懈可击的浏览器缓存方案:关于思路,细节,ServiceWorker,以及HTTP/2