likes
comments
collection
share

Web性能优化_知识点精讲

作者站长头像
站长
· 阅读数 152

歌德说:”一旦你信任了你自己,你就会明白怎样生活“

大家好,我是柒八九

今天,我们继续前端面试的知识点。我们来谈谈关于Web性能优化的相关知识点。

该系列的文章,大部分都是前面文章的知识点汇总,如果想具体了解相关内容,请移步相关系列,进行探讨。

如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。

文章list

  1. CSS重点概念精讲
  2. JS_基础知识点精讲
  3. 网络通信_知识点精讲
  4. JS_手写实现
  5. 前端工程化_知识点精讲
  6. 前端框架_React知识点精讲
  7. React实战精讲(React_TS/API)

好了,天不早了,干点正事哇。 Web性能优化_知识点精讲

你能所学到的知识点

  1. 延迟和宽带
  2. WebWorker
  3. 关键渲染路径
  4. React 应用中的优化处理
  5. 利用React-Profiler提升应用性能
  6. 从 URL 输入到页面加载整过程分析
  7. SPA 提速
  8. SPA: SEO

延迟和宽带

对所有网络通信都有决定性影响的两个方面:

  • 延迟 分组从信息源发送到目的地所需的时间 (单位:ms)
  • 带宽 逻辑或物理通信路径最大吞吐量 (单位:Mbit/s)

Web性能优化_知识点精讲

延迟的构成

延迟{消息|message}{分组|packet}从起点到终点经历的时间。

延迟主要分为4类。

  1. 传播延迟 :消息从发送端到接收端需要的时间
  2. 传输延迟 :把消息中的所有比特转移到链路中需要的时间
  3. 处理延迟 :处理分组首部、检查位错误及确定分组目标所需的时间
  4. 排队延迟 :到来的分组排队等待处理的时间

传播延迟/传输延迟/处理延迟/排队延迟的时间总和,就是客户端到服务器的总延迟时间

延迟最后一公里

延迟中相当大的一部分往往花在了最后几公里,而不是在横跨大洋或大陆时产生的,这就是所谓的最后一公里问题。

最后一公里的延迟与提供商、部署方法、网络拓扑,甚至一天中的哪个时段都有很大关系。作为最终用户,如果你想提高自己上网的速度,那选择延迟最短的 {网络业务提供商|Internet Service Provider}(简称ISP)是最关键的。


WebWorker

JavaScript 环境实际上是运行在操作系统(OS)中的虚拟环境

在浏览器中每打开一个页面,就会分配一个它自己的环境:即每个页面都有自己的内存事件循环DOM。并且每个页面就相当于一个沙盒,不会干扰其他页面。

而使用Worker 线程,浏览器可以在原始页面环境之外再分配一个完全独立二级子环境。这个子环境不能与依赖单线程交互的 API(如 DOM)互操作,但可以与父环境并行执行代码。

Worker的类型 (DSS)

Worker 线程规范中定义了三种主要的工作者线程

  1. {专用工作线程|Dedicated Web Worker}
    • 专用工作者线程,通常简称为工作者线程、Web WorkerWorker,是一种实用的工具,可以让脚本单独创建一个 JS 线程,以执行委托的任务。
  2. {共享工作线程|Shared Web Worker}
  3. {服务工作线程|Service Worker}:
    • 主要用途是拦截重定向修改页面发出的请求,充当网络请求的仲裁者的角色

{专用工作线程|Dedicated Web Worker}

专用工作线程是最简单的 Web 工作者线程,网页中的脚本可以创建专用工作者线程来执行在页面线程之外的其他任务。这样的线程可以与父页面交换信息、发送网络请求、执行文件输入/输出、进行密集计算、处理大量数据,以及实现其他不适合在页面执行线程里做的任务(否则会导致页面响应迟钝)。

创建专用工作线程方式

  1. 加载 JS 文件
    • 即把文件路径提供给 Worker 构造函数,然后构造函数再在后台异步加载脚本并实例化工作线程
worker.js
// 进行密集计算 bala bala

main.js
const worker = new Worker( 'worker.js');
console.log(worker); // Worker {} // {3}
  1. 行内创建工作线程
    1. 基于Blob
      • 通过脚本字符串创建了 Blob
      • 然后又通过 Blob 创建了 URL 对象
      • 最后把URL 对象,传给了 Worker()构造函数
    2. 基于函数序列化

worker 引用node_module中的包

通过行内构建工作线程有一个弊端,就是无法通过import/require引入一些第三方的包。

虽然在worker中可以使用importScripts()加载任意脚本,但是那些都是在worker同目录或者是利用绝对路径进行引用。很不方便。

Webpack最为打包工具下,使用指定的loader --worker-loader可以解决上面的问题。

进行本地按照

$ npm install worker-loader --save-dev

配置webpack -config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.worker\.js$/,
        use: { loader: "worker-loader" },
      },
    ],
  },
};

通过如上的配置,我们就可以像写常规的组件或者工具方法一些,肆无忌惮的通过import引入第三方包。

longTime.js

const A = require('A')
self.onmessage = function (e) {
     // A处理一些特殊场景
}

{服务工作线程|Service Worker}

{服务工作线程|Service Worker}是一种类似浏览器中代理服务器的线程,可以拦截外出请求缓存响应。这可以让网页在没有网络连接的情况下正常使用,因为部分或全部页面可以从服务工作线程缓存中提供服务。

服务工作线程在两个主要任务上最有用:充当网络请求的缓存层

在某种意义上

  • 服务工作线程就是用于把网页变成像原生应用程序一样的工具

线程缓存

服务工作线程的一个主要能力是可以通过编程方式实现真正的网络请求缓存机制

有如下特点:

  • 线程缓存不自动缓存任何请求
  • 线程缓存没有到期失效的概念
  • 线程缓存必须手动更新和删除
  • 缓存版本必须手动管理
  • 唯一的浏览器强制逐出策略基于线程缓存占用的空间。

拦截 fetch 事件

服务工作线程最重要的一个特性就是拦截网络请求

服务工作线程作用域中的网络请求会注册为 fetch 事件。这种拦截能力不限于 fetch()方法发送的请求,也能拦截对 JavaScriptCSS、图片和HTML(包括对主 HTML 文档本身)等资源发送的请求。

这些请求可以来自 JavaScript,也可以通过 <script><link><img>标签创建。

让服务工作线程能够决定如何处理 fetch 事件的方法是 event.respondWith()。该方法接收Promise,该Promise会解决为一个 Response 对象。该 Response对象实际上来自哪里完全由服务工作线程决定。可以来自网络,来自缓存,或者动态创建

从网络返回

这个策略就是简单地转发 fetch 事件

那些绝对需要发送到服务器的请求例如 POST 请求就适合该策略。

self.onfetch = (fetchEvent) => {
 fetchEvent.respondWith(fetch(fetchEvent.request));
};

从缓存返回

这个策略其实就是缓存检查

对于任何肯定有缓存的资源(如在安装阶段缓存的资源),可以采用该策略。

self.onfetch = (fetchEvent) => {
 fetchEvent.respondWith(caches.match(fetchEvent.request));
};

从网络返回,缓存作后备

从缓存返回,网络作后备


关键渲染路径

通常一个页面有三个阶段

  1. 加载阶段
    • 是指从发出请求到渲染出完整页面的过程
    • 影响到这个阶段的主要因素有网络JavaScript 脚本
  2. 交互阶段
    • 主要是从页面加载完成到用户交互的整个过程
    • 影响到这个阶段的主要因素是 JavaScript 脚本
  3. 关闭阶段
    • 主要是用户发出关闭指令后页面所做的一些清理操作

加载阶段关键数据

{文档对象模型| Document Object Model}

DOM:是HTML页面在解析后,基于对象的表现形式。

每个浏览器都需要一些时间解析HTML。并且,清晰的语义标记有助于减少浏览器解析HTML所需的时间。(不完整或者错误的语义标记,还需要浏览器根据上下文去分析和判断)

CSSOM Tree

CSSOM也是一个基于对象的树。它负责处理与DOM树相关的样式

一般来说,CSS被认为是一种{渲染阻断| Render-Blocking}资源。

什么是渲染阻断?渲染阻塞资源是一个组件,它将不允许浏览器渲染整个DOM树,直到给定的资源被完全加载CSS 是一种渲染阻断资源,因为在CSS完全加载之前,你无法渲染树。

起初,页面中所有CSS信息都被存放在一个文件中 。现在,开发人员通过一些技术手段,能够将CSS文件分割开来,只在渲染的早期阶段提供关键样式

JS

JavaScript 是一种用来操作DOM的语言。这些操作花费时间,并增加网站的整体加载时间。所有,

JavaScript 代码被称为 {解析器阻塞| Parser Blocking}资源。

什么是解析器阻塞?当需要下载执行JavaScript代码时,浏览器会暂停执行和构建DOM树。当JavaScript代码被执行完后,DOM树的构建才继续进行。

所以才有, JavaScript是一种昂贵的资源的说法。


记住,

{关键渲染路径| Critical Rendering Path}都是关于HTMLCSSJavascript

关键路径相关术语

  • {关键资源| Critical Resource}:所有可能阻碍页面渲染的资源
  • {关键路径长度|Critical Path Length}:获取构建页面所需的所有关键资源所需的 RTT(Round Trip Time)
  • {关键字节| Critical Bytes}:作为完成和构建页面的一部分而传输的字节总数

优化关键渲染路径

如果你希望优化任何框架中的关键渲染路径,你需要在上述指标上下功夫并加以改进。

  • 优化关键资源
    • JavaScriptCSS 改成内联的形式 (性能提升不是很大)
    • 如果 JavaScript 代码没有 DOM 或者 CSSOM 的操作,则可以改成 sync 或者 defer 属性
    • 首屏内容可以优先加载,非首屏内容采用滚动加载
  • 优化关键路径长度
    • 压缩 CSSJavaScript 资源
    • 移除 HTMLCSSJavaScript 文件中一些注释内容
  • 优化关键字节
    • 通过减少关键资源的个数和减少关键资源的大小搭配来实现
    • 使用 CDN 来减少每次 RTT 时长

处理关键资源

懒加载

加载的关键是 "懒加载"。任何媒体资源、CSSJavaScript、图像、甚至HTML都可以被懒加载。每次加载有限的页面的内容,可以提高关键渲染路径。

  • 不要在加载页面时加载这个整个页面的 CSSJavaScriptHTML
  • 相反,可以为一个button添加一个事件监听,只有在用户点击按钮时才加载脚本。
  • 使用Webpack来完成懒加载功能。

Async, Defer, Preload

Web性能优化_知识点精讲

当使用Preload时,它被用于HTML文件中没有的文件,但在渲染或解析JavaScript或CSS文件的时候。有了Preload,浏览器就会下载资源,在资源可用的时候就会执行。

  • 只有在首屏页面需要的文件才可以预载
  • 预加载只用于<link>标签

编写原生(Vanilla) JS,避免使用第三方脚本


优化关键路径长度

HTTP缓存

  1. 强缓存
    • ExpiresCache-control:max-age=x(强缓存)
  2. 协商缓存
    • EtagIf-None-Match (协商缓存)

JS层面做缓存处理(ServerWorker)


React 应用中的优化处理

优化被分成两个阶段。

    1. 在应用程序被加载之前
    • 路由级别懒加载
    • React.lazy + Suspense
    1. 第二阶段是在应用加载后进行优化
    • 使用正确的状态管理方法
      • 合理使用useState/setState- 防止回流
      • 利用shouldComponentUpdate()生命周期方法做浅对比
    • 利用React.Memo

利用React-Profiler提升应用性能

Profiler UI 界面

Profiler的UI界面在逻辑上可分为4个主要部分。

Web性能优化_知识点精讲

  1. 图表类型
    • 火焰图
    • 排序图
  2. 图表区域--在应用程序的剖析切片中,代表某次commit对应的组件渲染时间的相关信息。
  3. 提交区域--每个条形图代表应用程序在整个录制阶段所有的commit操作。每当你通过点击选择一个commit图表区域提交信息就会相应地更新。
  4. 提交信息面板--关于单个选定的commit阶段或单个选定组件的细节。

从 URL 输入到页面加载整过程分析

整个过程大致可以分为三个阶段

  1. 客户端发起请求阶段
  2. 服务端数据处理请求阶段
  3. 客户端页面渲染阶段

客户端请求阶段的瓶颈点

客户端发起请求阶段

  1. 用户在浏览器输入 URL
  2. 经过本地缓存确认是否已经存在这个网站
  3. 如果没有,接着会由 DNS 查询从域名服务器获取这个 IP 地址
  4. 客户端通过 TCP 的三次握手和TLS协商向服务器发起 HTTP 请求建立连接的过程

在这个过程中

  1. 本地缓存
  2. DNS查询
  3. HTTP 请求

很容易成为影响前端性能的瓶颈点

本地缓存

本地缓存可以让静态资源加载更快,想要让本地缓存发挥作用,就需要先在服务器上进行配置

本地缓存一般包括强缓存协商缓存两种形式

强缓存是指浏览器在加载资源时,根据请求头的 expires/cache-control,判断是否命中客户端缓存。

  • 如果命中,则直接从缓存读取资源。

协商缓存是指,浏览器会先发送一个请求到服务器,通过 etag/last-modified,验证资源是否命中客户端缓存。

  • 如果命中,服务器会将这个请求返回,但不会返回这个资源的数据,依然是从缓存中读取资源;
  • 如果没有命中,无论是资源过期或者没有相关资源,都需要向服务器发起请求,等待服务器返回这个资源

DNS 查询

每进行一次 DNS 查询,都要经历从手机到移动信号塔,再到认证 DNS 服务器的过程。要节省时间,一个办法就是让 DNS 查询走缓存,浏览器提供了 DNS 预获取的接口。

HTTP 请求

HTTP 请求阶段,最大的瓶颈点来源于请求阻塞。所谓请求阻塞,就是浏览器为保证访问速度,会默认对同一域下的资源保持一定的连接数,请求过多就会进行阻塞

浏览器同域名的连接数限制是一般是 6 个,如果当前请求书多于 6 个,只能 6 个并发,其余的得等最先返回的请求后,才能做下一次请求

解决方式

  1. 域名规划
    • 当前页面需要用到哪些域名,最关键的首屏中需要用到哪些域名
    • 规划一下这些域名发送的顺序
  2. 域名散列
    • 通过不同的域名,增加请求并行连接数
    • 将静态服务器地址 pic.google.com,做成支持 pic0-5 的 6 个域名
    • 每次请求时随机选一个域名地址进行请求
    • 有 6 个域名同时可用,最多可以并行 36 个连接
    • 域名个数不是越多越好,太分散的话,又会涉及多域名之间无法缓存的问题

服务端数据处理阶段的瓶颈点

服务端数据处理阶段,是指 WebServer 接收到请求后,从数据存储层取到数据,再返回给前端的过程。

这个过程中的瓶颈点,就在于是否做了

  1. 数据缓存处理
  2. Gzip 压缩
  3. 重定向

数据缓存

数据缓存分为两种

  1. 接口缓存
    • 借助 Service Worker 的数据接口缓存
    • 借助本地存储的接口缓存
  2. CDN(Content Delivery Network,内容分发网络)

接口缓存

Service Worker 是浏览器的一个高级属性,本质上是一个请求代理层。它存在的目的就是拦截和处理网络数据请求

借助本地存储的接口缓存,在一些对数据时效性要求不高的页面,第一次请求到数据后,程序将数据存储到本地存储

  1. localStorage
  2. 客户端本身的存储

下一次请求的时候,先去缓存里面取将取数据,如果没有的话,再向服务器发起请求

CDN

通过在网络各处放置节点服务器,构造一个智能虚拟网络。将用户的请求导向离用户最近的服务节点上


Gzip

Gzip 压缩是一种压缩技术,服务器端通过使用 Gzip,传输到浏览器端的文本类资源的大小可以变为原来的 1/3 左右

重定向

所谓重定向,是指网站资源迁移到其他位置后,用户访问站点时,程序自动将用户请求从一个页面转移到另外一个页面的过程。

在服务端处理阶段,重定向分为三类

  1. 服务端发挥的302重定向
  2. META 标签实现的重定向
  3. 前端 Javasript 通过window.location 实现的重定向

它们都会引发新的 DNS 查询,导致新的 TCP 三次握手和 TLS 协商,以及产生新的 HTTP 请求。


页面解析和渲染阶段的瓶颈点

所谓解析,就是 HTML 解析器把页面内容转换为 DOM 树和 CSSOM树的过程

解析阶段

  1. DOM树
    • DOM 树全称为 Document Object Model 即文档对象模型
    • 它描述了标签之间的层次和结构
    • HTML 解析器通过词法分析获得开始和结束标签
    • 生成相应的节点和创建节点之间的父子关系结构
    • 直到完成 DOM 树的创建
  2. CSSOM树
    • 即 CSS 对象模型
    • 主要描述样式集的层次和结构
    • HTML 解析器遇到内联的 style 标签时,会触发 CSS 解析器对样式内容进行解析
    • CSS 解析器遍历其中每个规则,将 CSS 规则解析浏览器可解析和处理的样式集合
    • 最终结合浏览器里面的默认样式,汇总形成具有父子关系的 CSSOM 树

渲染阶段

主线程会计算 DOM 节点的最终样式,生成布局树。布局树会记录参与页面布局的节点和样式 。完成布局后,紧跟着就是绘制。

绘制就是把各个节点绘制到屏幕上的过程,绘制结果以层的方式保存

构建 DOM 树的瓶颈点

解析器构建 DOM 树的过程中, 有三点会严重影响前端性能

  1. HTML 标签不满足 Web 语义化时
    • 浏览器就需要更多时间去解析 DOM 标签的含义
    • 比如将 <br> 写成了 </br>,又或者表格嵌套不标准,标签层次结构复杂等
  2. DOM 节点的数量多
  3. 文档中包含<SCRIPT> 标签时的情况
    • 无论是 DOM 或者 CSSOM 都可以被 JavaScript 所访问并修改
    • 一旦在页面解析时遇到 <SCRIPT> 标签,DOM 的构造过程就会暂停,等待服务器请求脚本
    • 在脚本加载完成后,还要等取回所有的 CSS 及完成 CSSOM 之后才继续执行
    • 可以通过使用 deferasync,告诉浏览器在等待脚本下载期间不阻止解析过程

布局中的瓶颈点--重排


SPA 提速

监控 SPA 性能

  1. Lighthouse:一个开源的自动化工具,用于改进网络应用的质量
  2. React Performance Devtools:针对 React.js 项目的优化插件

这些工具的弊端是,他们不能准确的测出 SPA 应用的加载速度

为了能够真正的测出 SPA 的真实加载速度,在Chrome 中也存在一些子工具(如:Speed Index)用于模拟用户真正的上网过程。

但是,真实的用户操作受各种设备和网络影响,很难利用单一的插件和工具进行模拟。

所以,我们可以使用 {真实用户模拟|Real User Monitoring}(RUM)对应用就行处理。他能很好的跟踪用户在网页中的各种操作并且能够给出网站的实时加载数据情况。

这里列出一些针对SPARUM工具

  1. Sentry: 日志、性能收集 (多平台)
  2. Dynatrace
  3. Catchpoint
  4. ....

提升 SPA 性能(6种)

  1. 优先渲染首屏页面信息
  2. 非必要数据的懒加载
  3. 缓存静态内容
  4. 对实时性较强的应用使用WebSocket
  5. 使用JSONP/CORS绕过同源策略
  6. CDN处理 Web性能优化_知识点精讲

优先渲染首屏页面信息

  1. 针对非首屏页面的惰性渲染
  2. 每个组件赋予不同的渲染优先级

提高Frist Meaningful Paint (FMP)的指标。 => 缩短了用户能够看到页面核心内容的时间。

通过对不可见元素的过滤渲染也能提高Time to InteractiveTTL)的性能指标。

非必要数据的懒加载

发现转换阶段也可能存在性能瓶颈。在此阶段,SPA加载数据并且对数据进行{序列化|Normalizes}处理,然后将处理完的数据存入到内存中。

可以使用一个高优先级调用来获取First Meaningful Paint所需的数据,并使用另一个回调来惰性加载页面所需的其余数据。

一些SPA框架,例如(React/Vue)是允许开发者将应用代码分割成很多bundles

所以,对一些非必要的bundles进行按需加载或者延迟处理。该方法可以加速第一次导航


缓存静态内容

对你的SPA进行审查,从中甄别出可以在用户设备中被缓存的图片或者其他的静态资源。

  1. 使用某种类型的分页并依赖于服务器来实现持久性
  2. 编写LRU算法来从存储中删除多余的项
  3. 使用Service Workers在SPA中缓存静态内容
  4. 使用IndexedDB API缓存大量结构化的数据

对实时性较强的应用使用WebSocket

WebSocket 可以实现客户端与服务器间双向、基于消息的文本或二进制数据传输。它是浏览器中最靠近套接字的 API。

与HTTP不同,客户端不必不断地向服务器发送请求以获取新消息。相反,浏览器只需监听服务器,并在准备好时接收消息。

Web性能优化_知识点精讲


使用JSONP/CORS绕过同源策略

大部分应用需要从第三方获取数据

但是,由于同源策略,不能对非同源的第三方服务进行AJAX调用。

为了能够访问第三方网站,应用需要利用origin server作为代理。

额外的往返意味着更多的延迟。 Web性能优化_知识点精讲

如果不处理检索到的数据,也不将其存储在系统中,则可以直接请求资源。为此,可以使用JSONP或跨来源资源共享(CORS)进行数据获取。

JSONP

  1. 第一步
    • 网页添加一个<script>元素,向服务器请求一个脚本
    • 请求的脚本网址有一个callback参数(?callback=bar),用来告诉服务器,客户端的回调函数名称(bar
<script src="http://XX.com?callback=bar"></script>
  1. 第二步

    • 服务器收到请求后,拼接一个字符串,将 JSON 数据放在函数名里面,作为字符串返回(bar({...})
  2. 第三步

    • 客户端会将服务器返回的字符串,作为代码解析,因为浏览器认为,这是<script>标签请求的脚本内容。
    • 这时,客户端只要定义了bar()函数,就能在该函数体内,拿到服务器返回的 JSON 数据。

JSONP 只能是GET请求

同时,我们可以使用asyncdefer 属性来对<script>进行优化处理。 Web性能优化_知识点精讲

属性解释
没有 deferasync浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行
async加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)
defer加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成

CORS

CORS{跨源资源分享|Cross-Origin Resource Sharing}的缩写。它是 W3C 标准,属于跨源 AJAX 请求的根本解决方法。

但是,除了GETHEADPOST之外,使用任何方法的请求都会发起一个{预检请求|Preflight Check},以确认服务器已经为跨源请求做好了准备。

=> 预备请求
OPTIONS /resource.js HTTP/1.1Host: thirdparty.com
     Origin: http://example.com
     Access-Control-Request-Method: POST
     Access-Control-Request-Headers: My-Custom-Header
     ...
     
<= 预备响应
HTTP/1.1 200 OKAccess-Control-Allow-Origin: http://example.com
     Access-Control-Allow-Methods: GET, POST, PUT
     Access-Control-Allow-Headers: My-Custom-Header
     ...
(正式的 HTTP 请求)  ③

  • ① 验证许可的预备 OPTIONS 请求
  • ② 第三方源的成功预备响应
  • ③ 实际的 CORS 请求

预检请求多了一次往返时间,无形中加大了请求的延迟时间。


CDN处理

CDN{内容交付网络|Content Delivery Networks} 的英文首字母缩写,是一组分布在不同地理位置的服务器,它Web 内容存放在更靠近用户的位置,从而加速 Web 内容的交付

CDN 将网页、图像和视频等内容缓存在靠近你的实际地点的代理服务器中。

CDN 想成是一部 ATM 机。如今几乎每个街角都有提款机,让我们可以快速高效地提取现金。

为SPA使用CDN意味着更快地加载脚本和减少交互时间


SPA: SEO

  1. JS框架 + SSR
  2. 使用渐进增强和特性探测
  3. 列出网站完整的页面列表 Sitemap.xml
  4. 使用rel=canonical的连接
  5. TDK 的优化处理
    • tilte/keywords/description可以在HTML的<meta>标签内定义。
    • title权重最高,利用title提高页面权重
    • keywords 相对权重较低,作为页面的辅助关键词搜索
    • description 的描述一般会直接显示在搜索结果的介绍中

后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

Web性能优化_知识点精讲

转载自:https://juejin.cn/post/7159356502765993998
评论
请登录