❝歌德说:”一旦你信任了你自己,你就会明白怎样生活“ ❞
大家好,我是「柒八九」。
今天,我们继续「前端面试」的知识点。我们来谈谈关于「Web性能优化」的相关知识点。
该系列的文章,大部分都是前面文章的知识点汇总,如果想具体了解相关内容,请移步相关系列,进行探讨。
好了,天不早了,干点正事哇。
你能所学到的知识点
❝
- 延迟和宽带
WebWorker
- 关键渲染路径
- React 应用中的优化处理
- 利用React-Profiler提升应用性能
- 从 URL 输入到页面加载整过程分析
- SPA 提速
- SPA: SEO
❞
延迟和宽带
对所有「网络通信」都有决定性影响的两个方面:
- 「延迟」 分组从信息源发送到目的地所需的时间 (单位:
ms
) - 「带宽」 逻辑或物理通信路径最大的「吞吐量」 (单位:
Mbit/s
)
延迟和带宽
延迟的构成
「延迟」是消息message 或分组packet从起点到终点经历的时间。
延迟主要分为4类。
- 「传播延迟」 :消息从发送端到接收端需要的时间
- 「传输延迟」 :把消息中的所有「比特」转移到链路中需要的时间
- 「处理延迟」 :处理分组首部、检查位错误及确定分组目标所需的时间
- 「排队延迟」 :到来的分组排队等待处理的时间
❝传播延迟/传输延迟/处理延迟/排队延迟的时间总和,就是客户端到服务器的「总延迟时间」 ❞
延迟最后一公里
延迟中相当大的一部分往往花在了「最后几公里」,而不是在横跨大洋或大陆时产生的,这就是所谓的「最后一公里」问题。
最后一公里的延迟与提供商、部署方法、网络拓扑,甚至一天中的哪个时段都有很大关系。作为最终用户,如果你想提高自己上网的速度,那选择延迟最短的 网络业务提供商Internet Service Provider(简称ISP)是最关键的。
WebWorker
❝JavaScript
环境实际上是运行在操作系统(OS
)中的「虚拟环境」 ❞
在浏览器中每打开一个页面,就会分配一个它「自己的环境」:即每个页面都有自己的内存、事件循环、DOM。并且每个页面就相当于一个「沙盒」,不会干扰其他页面。
而使用「Worker 线程」,浏览器可以在「原始页面环境之外」再分配一个完全独立的「二级子环境」。这个子环境不能与依赖单线程交互的 API
(如 DOM
)互操作,但「可以与父环境并行」执行代码。
Worker的类型 (DSS)
Worker
线程规范中定义了「三种主要」的工作者线程
- 专用工作线程Dedicated Web Worker
- 专用工作者线程,通常简称为工作者线程、
Web Worker
或Worker
,是一种实用的工具,可以让脚本「单独创建」一个 JS 线程,以执行委托的任务。
- 共享工作线程Shared Web Worker
- 服务工作线程Service Worker:
- 主要用途是「拦截」、「重定向」和「修改页面发出的请求」,充当「网络请求」的仲裁者的角色
专用工作线程Dedicated Web Worker
专用工作线程是最简单的 Web 工作者线程,网页中的脚本可以创建专用工作者线程来执行在「页面线程之外」的其他任务。这样的线程可以与父页面交换信息、发送网络请求、执行文件输入/输出、进行「密集计算」、处理「大量数据」,以及实现其他不适合在页面执行线程里做的任务(否则会导致页面响应迟钝)。
创建专用工作线程方式
- 「加载 JS 文件」
- 即把「文件路径」提供给
Worker
构造函数,然后构造函数再在「后台异步加载」脚本并实例化工作线程
worker.js
// 进行密集计算 bala bala
main.js
const worker = new Worker( 'worker.js');
console.log(worker); // Worker {} // {3}
- 行内创建工作线程
- 通过脚本字符串创建了
Blob
- 然后又通过
Blob
创建了URL
对象 - 最后把
URL
对象,传给了Worker()
构造函数
- 基于
Blob
- 基于函数序列化
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()
方法发送的请求,也能拦截对 JavaScript
、CSS
、图片和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));
};
从网络返回,缓存作后备
从缓存返回,网络作后备
关键渲染路径
通常一个页面有「三个阶段」
- 「加载阶段」
- 是指从「发出请求到渲染出完整页面」的过程
- 影响到这个阶段的主要因素有「网络」和 「JavaScript 脚本」
- 「交互阶段」
- 主要是从页面加载完成到「用户交互」的整个过程
- 影响到这个阶段的主要因素是 「JavaScript 脚本」
- 「关闭阶段」
- 主要是用户发出关闭指令后页面所做的一些「清理操作」
加载阶段关键数据
文档对象模型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都是「关于HTML
、CSS
和Javascript
的」 ❞
关键路径相关术语
- 关键资源Critical Resource:所有可能「阻碍页面渲染」的资源
- 关键路径长度Critical Path Length:获取构建页面所需的所有关键资源所需的 「RTT」(Round Trip Time)
- 关键字节Critical Bytes:作为完成和构建页面的一部分而传输的「字节总数」。
优化关键渲染路径
如果你希望优化任何框架中的关键渲染路径,你需要在上述指标上下功夫并加以改进。
❝
- 优化关键资源
- 将
JavaScript
和CSS
改成内联的形式 (性能提升不是很大) - 如果
JavaScript
代码没有DOM
或者CSSOM
的操作,则可以改成sync
或者defer
属性 - 首屏内容可以优先加载,非首屏内容采用「滚动加载」
- 优化关键路径长度
- 「压缩」
CSS
和JavaScript
资源 - 移除
HTML
、CSS
、JavaScript
文件中一些「注释内容」 - 优化关键字节
- 通过减少关键资源的「个数」和减少关键资源的「大小」搭配来实现
- 使用
CDN
来减少每次RTT
时长 - ❞
处理关键资源
懒加载
加载的关键是 "懒加载"。任何媒体资源、CSS
、JavaScript
、图像、甚至HTML
都可以被懒加载。每次加载「有限的页面的内容」,可以提高关键渲染路径。
- 不要在加载页面时加载这个整个页面的
CSS
、JavaScript
和HTML
。 - 相反,可以为一个
button
添加一个事件监听,只有在用户点击按钮时才加载脚本。 - 使用
Webpack
来完成懒加载功能。
Async, Defer, Preload
当使用Preload
时,它被用于HTML文件中没有的文件,但在渲染或解析JavaScript或CSS文件的时候。有了Preload
,浏览器就会下载资源,在资源可用的时候就会执行。
- 「只有在首屏页面需要的文件才可以预载」。
- 「预加载只用于
<link>
标签」。
编写原生(Vanilla) JS,避免使用第三方脚本
优化关键路径长度
HTTP缓存
- 强缓存
Expires
和Cache-control:max-age=x
(强缓存)
- 协商缓存
Etag
和If-None-Match
(协商缓存)
JS层面做缓存处理(ServerWorker)
React 应用中的优化处理
优化被分成两个阶段。
- 路由级别懒加载
React.lazy
+Suspense
- 在应用程序被加载之前
- 合理使用
useState
/setState
- 防止回流 - 利用
shouldComponentUpdate()
生命周期方法做浅对比 - 使用正确的状态管理方法
- 利用React.Memo
- 第二阶段是在应用加载后进行优化
利用React-Profiler提升应用性能
Profiler UI 界面
Profiler
的UI界面在逻辑上可分为4个主要部分。
- 「图表类型」
- 火焰图
- 排序图
- 「图表区域」--在应用程序的
剖析
切片中,代表某次commit
对应的组件渲染时间的相关信息。 - 「提交区域」--每个条形图代表应用程序在整个录制阶段所有的
commit
操作。每当你通过点击选择一个commit
,「图表区域」和「提交信息」就会相应地更新。 - 「提交信息面板」--关于单个选定的
commit
阶段或单个选定组件的细节。
从 URL 输入到页面加载整过程分析
整个过程大致可以分为「三个阶段」
- 客户端发起请求阶段
- 服务端数据处理请求阶段
- 客户端页面渲染阶段
客户端请求阶段的瓶颈点
客户端发起请求阶段
- 用户在浏览器输入
URL
- 经过本地缓存确认是否已经存在这个网站
- 如果没有,接着会由
DNS
查询从域名服务器获取这个IP
地址 - 客户端通过
TCP
的三次握手和TLS协商向服务器发起HTTP
请求建立连接的过程
在这个过程中
- 本地缓存
DNS
查询HTTP
请求
很容易成为影响前端性能的瓶颈点
本地缓存
本地缓存可以让静态资源加载更快,想要让本地缓存发挥作用,就需要「先在服务器上进行配置」。
本地缓存一般包括强缓存和协商缓存两种形式
「强缓存」是指浏览器在加载资源时,根据请求头的 expires
/cache-control
,判断是否命中客户端缓存。
- 如果命中,则直接从缓存读取资源。
「协商缓存」是指,浏览器会先发送一个请求到服务器,通过 etag
/last-modified
,验证资源是否命中客户端缓存。
- 如果命中,服务器会将这个请求返回,但不会返回这个资源的数据,依然是从缓存中读取资源;
- 如果没有命中,无论是资源过期或者没有相关资源,都需要向服务器发起请求,等待服务器返回这个资源
DNS 查询
每进行一次 DNS
查询,都要经历从手机到移动信号塔,再到认证 DNS
服务器的过程。要节省时间,一个办法就是让 DNS
查询走缓存,浏览器提供了 DNS
预获取的接口。
HTTP 请求
在 HTTP
请求阶段,最大的瓶颈点来源于「请求阻塞」。所谓请求阻塞,就是浏览器为保证访问速度,会默认对同一域下的资源保持一定的连接数,请求过多就会进行阻塞
浏览器同域名的连接数限制是一般是 6 个,如果当前请求书多于 6 个,只能 6 个并发,其余的得等最先返回的请求后,才能做下一次请求
解决方式
- 域名规划
- 当前页面需要用到哪些域名,最关键的首屏中需要用到哪些域名
- 规划一下这些域名发送的顺序
- 域名散列
- 通过不同的域名,增加请求并行连接数
- 将静态服务器地址
pic.google.com
,做成支持 pic0-5 的 6 个域名 - 每次请求时随机选一个域名地址进行请求
- 有 6 个域名同时可用,最多可以并行 36 个连接
- 域名个数不是越多越好,太分散的话,又会涉及多域名之间无法缓存的问题
服务端数据处理阶段的瓶颈点
服务端数据处理阶段,是指 WebServer
接收到请求后,从数据存储层取到数据,再返回给前端的过程。
这个过程中的瓶颈点,就在于是否做了
- 数据缓存处理
- Gzip 压缩
- 重定向
数据缓存
数据缓存分为两种
- 接口缓存
- 借助 Service Worker 的数据接口缓存
- 借助本地存储的接口缓存
- CDN(Content Delivery Network,内容分发网络)
接口缓存
Service Worker
是浏览器的一个高级属性,本质上是一个「请求代理层」。它存在的目的就是拦截和处理网络数据请求
借助本地存储的接口缓存,在一些对数据时效性要求不高的页面,第一次请求到数据后,程序将数据存储到本地存储
- localStorage
- 客户端本身的存储
下一次请求的时候,先去缓存里面取将取数据,如果没有的话,再向服务器发起请求
CDN
通过在网络各处放置节点服务器,构造一个「智能虚拟网络」。将用户的请求导向离用户最近的服务节点上
Gzip
Gzip 压缩是一种压缩技术,「服务器端通过使用 Gzip」,传输到浏览器端的文本类资源的大小可以变为原来的 1/3 左右
重定向
所谓重定向,是指网站资源迁移到其他位置后,用户访问站点时,程序自动将用户请求从一个页面转移到另外一个页面的过程。
在服务端处理阶段,重定向分为三类
- 服务端发挥的302重定向
- META 标签实现的重定向
- 前端 Javasript 通过window.location 实现的重定向
它们都会引发新的 DNS 查询,导致新的 TCP 三次握手和 TLS 协商,以及产生新的 HTTP 请求。
页面解析和渲染阶段的瓶颈点
所谓解析,就是 HTML 解析器把页面内容转换为 DOM 树和 CSSOM树的过程
解析阶段
- DOM树
- DOM 树全称为 Document Object Model 即文档对象模型
- 它描述了标签之间的层次和结构
- HTML 解析器通过词法分析获得开始和结束标签
- 生成相应的节点和创建节点之间的父子关系结构
- 直到完成 DOM 树的创建
- CSSOM树
- 即 CSS 对象模型
- 主要描述样式集的层次和结构
- HTML 解析器遇到内联的 style 标签时,会触发 CSS 解析器对样式内容进行解析
- CSS 解析器遍历其中每个规则,将 CSS 规则解析浏览器可解析和处理的样式集合
- 最终结合浏览器里面的默认样式,汇总形成具有父子关系的 CSSOM 树
渲染阶段
主线程会计算 DOM 节点的最终样式,生成布局树。布局树会记录参与页面布局的节点和样式 。完成布局后,紧跟着就是绘制。
绘制就是把各个节点绘制到屏幕上的过程,绘制结果以层的方式保存
构建 DOM 树的瓶颈点
解析器构建 DOM 树的过程中, 有三点会严重影响前端性能
- HTML 标签不满足 Web 语义化时
- 浏览器就需要更多时间去解析 DOM 标签的含义
- 比如将
<br>
写成了</br>
,又或者表格嵌套不标准,标签层次结构复杂等
- DOM 节点的数量多
- 文档中包含
<SCRIPT>
标签时的情况
- 无论是 DOM 或者 CSSOM 都可以被 JavaScript 所访问并修改
- 一旦在页面解析时遇到
<SCRIPT>
标签,DOM 的构造过程就会暂停,等待服务器请求脚本 - 在脚本加载完成后,还要等取回所有的
CSS
及完成CSSOM
之后才继续执行 - 可以通过使用
defer
和async
,告诉浏览器在等待脚本下载期间不阻止解析过程
布局中的瓶颈点--重排
SPA 提速
监控 SPA 性能
Lighthouse
:一个开源的「自动化工具」,用于改进网络应用的质量React Performance Devtools
:针对React.js
项目的优化插件
这些工具的弊端是,他们不能准确的测出 SPA
应用的「加载速度」。
为了能够真正的测出 SPA 的真实加载速度,在Chrome 中也存在一些「子工具」(如:Speed Index)用于模拟用户真正的上网过程。
但是,真实的用户操作受各种设备和网络影响,很难利用单一的插件和工具进行模拟。
所以,我们可以使用 真实用户模拟Real User Monitoring(RUM
)对应用就行处理。他能很好的跟踪用户在网页中的各种操作并且能够给出网站的实时加载数据情况。
这里列出一些针对SPA
的RUM
工具
Sentry
: 日志、性能收集 (多平台)- Dynatrace
- Catchpoint
- ....
提升 SPA 性能(6种)
- 优先渲染「首屏」页面信息
- 非必要数据的「懒加载」
- 「缓存静态内容」
- 对实时性较强的应用使用
WebSocket
- 使用
JSONP/CORS
绕过同源策略 CDN
处理
优先渲染首屏页面信息
- 针对非首屏页面的「惰性渲染」
- 每个组件赋予不同的「渲染优先级」
提高Frist Meaningful Paint (FMP
)的指标。 => 「缩短」了用户能够看到页面「核心内容」的时间。
通过对「不可见元素的过滤渲染」也能提高Time to Interactive(TTL
)的性能指标。
非必要数据的懒加载
发现「转换阶段」也可能存在性能瓶颈。在此阶段,SPA加载数据并且对数据进行序列化Normalizes处理,然后将处理完的数据「存入到内存」中。
可以使用一个「高优先级」调用来获取First Meaningful Paint
所需的数据,并使用另一个回调来「惰性加载」页面所需的其余数据。
一些SPA框架,例如(React/Vue
)是允许开发者将应用「代码分割」成很多bundles
。
❝所以,对一些非必要的bundles
进行「按需加载」或者「延迟处理」。该方法可以加速「第一次导航」。 ❞
缓存静态内容
对你的SPA进行审查,从中甄别出可以在用户设备中被「缓存」的图片或者其他的静态资源。
- 使用某种类型的分页并依赖于服务器来实现持久性
- 编写LRU算法来从存储中删除多余的项
- 使用Service Workers在SPA中缓存静态内容
- 使用IndexedDB API缓存大量「结构化」的数据
对实时性较强的应用使用WebSocket
❝WebSocket
可以实现客户端与服务器间双向、基于消息的文本或二进制数据传输。它是浏览器中最靠近套接字的 API。 ❞
与HTTP不同,客户端不必不断地向服务器发送请求以获取新消息。相反,浏览器只需监听服务器,并在准备好时接收消息。
使用JSONP/CORS绕过同源策略
大部分应用需要「从第三方获取数据」。
但是,由于同源策略,不能对非同源的第三方服务进行AJAX
调用。
为了能够访问第三方网站,应用需要利用origin server作为代理。
❝额外的「往返」意味着更多的延迟。
❞
如果不处理检索到的数据,也不将其存储在系统中,则可以直接请求资源。为此,可以使用JSONP
或跨来源资源共享(CORS)进行数据获取。
JSONP
- 第一步
- 「网页」添加一个
<script>
元素,向服务器请求一个脚本 - 请求的脚本网址有一个
callback
参数(?callback=bar
),用来告诉服务器,客户端的回调函数名称(bar
)
<script src="http://XX.com?callback=bar"></script>
- 第二步
- 服务器收到请求后,拼接一个字符串,将
JSON
数据放在函数名里面,作为字符串返回(bar({...})
)
- 第三步
- 客户端会将服务器返回的字符串,作为「代码解析」,因为浏览器认为,这是
<script>
标签请求的脚本内容。 - 这时,客户端只要定义了
bar()
函数,就能在该函数体内,拿到服务器返回的JSON
数据。
❝JSONP
只能是GET
请求 ❞
同时,我们可以使用async
和 defer
属性来对<script>
进行优化处理。
属性 | 解释 |
没有 defer 或 async | 浏览器会「立即加载并执行」指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素「之前」,也就是说不等待后续载入的文档元素,「读到就加载并执行」 |
async | 加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步) |
defer | 加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的「执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成」 |
CORS
CORS
是跨源资源分享Cross-Origin Resource Sharing的缩写。它是 W3C
标准,属于跨源 AJAX
请求的根本解决方法。
但是,「除了」GET
、HEAD
和POST
之外,使用任何方法的请求都会发起一个预检请求Preflight Check,以确认服务器已经为跨源请求做好了准备。
=> 预备请求
OPTIONS /resource.js HTTP/1.1 ①
Host: thirdparty.com
Origin: http://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: My-Custom-Header
...
<= 预备响应
HTTP/1.1 200 OK ②
Access-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
- JS框架 +
SSR
- 使用渐进增强和特性探测
- 列出网站完整的页面列表
Sitemap.xml
- 使用
rel=canonical
的连接 TDK
的优化处理
tilte/keywords/description
可以在HTML的<meta>
标签内定义。title
的「权重最高」,利用title
提高页面权重keywords
相对权重较低,作为页面的「辅助关键词」搜索description
的描述一般会直接显示在搜索结果的介绍中
后记
「分享是一种态度」。
「全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。」